backend sliced by modules

This commit is contained in:
2026-05-02 18:24:57 +07:00
parent b6306d5aa9
commit 693e48af59
42 changed files with 1602 additions and 779 deletions
+48
View File
@@ -0,0 +1,48 @@
use axum::{extract::State, routing::post, Json, Router};
use serde::Deserialize;
use serde_json::{json, Value};
use tracing::instrument;
use crate::{api::ApiState, error::AppError};
#[derive(Deserialize, Debug)]
pub struct TgWidgetPayload {
pub id: i64,
pub first_name: Option<String>,
pub last_name: Option<String>,
pub username: Option<String>,
pub photo_url: Option<String>,
pub auth_date: i64,
pub hash: String,
}
pub fn router() -> Router<ApiState> {
Router::new().route("/telegram", post(auth_telegram_api))
}
#[instrument(skip(state, payload), fields(tg_id = payload.id, username = ?payload.username))]
async fn auth_telegram_api(
State(state): State<ApiState>,
Json(payload): Json<TgWidgetPayload>,
) -> Result<Json<Value>, AppError> {
state.auth_service.verify_tg_widget_auth(
&state.bot_token,
payload.id,
payload.first_name.as_deref(),
payload.last_name.as_deref(),
payload.username.as_deref(),
payload.photo_url.as_deref(),
payload.auth_date,
&payload.hash,
)?;
let user = state
.auth_service
.process_login(payload.id, payload.username.clone(), None)
.await?;
let token = state
.auth_service
.generate_auth_token(user.id, user.tg_id, &state.jwt_secret)?;
Ok(Json(json!({ "status": "success", "token": token })))
}
+60
View File
@@ -0,0 +1,60 @@
use crate::{api::ApiState, db::models::User, error::AppError};
use axum::{
extract::{Request, State},
middleware::Next,
response::Response,
};
use tracing::warn;
/// Проверяет JWT и инжектит User в запрос.
/// Аналог AuthGuard в NestJS.
pub async fn auth_guard(
State(state): State<ApiState>,
mut req: Request,
next: Next,
) -> Result<Response, AppError> {
let auth_header = req
.headers()
.get("Authorization")
.and_then(|h| h.to_str().ok())
.filter(|h| h.starts_with("Bearer "))
.map(|h| &h[7..])
.ok_or_else(|| AppError::Unauthorized("Access token required".into()))?;
let token_data = jsonwebtoken::decode::<super::service::Claims>(
auth_header,
&jsonwebtoken::DecodingKey::from_secret(state.jwt_secret.as_bytes()),
&jsonwebtoken::Validation::default(),
)
.map_err(|_| AppError::Unauthorized("Invalid session signature".into()))?;
let user = state
.repo
.get_user_by_tg_id(token_data.claims.tg_id)
.await?
.ok_or_else(|| AppError::NotFound("Operative not found in grid".into()))?;
req.extensions_mut().insert(user);
Ok(next.run(req).await)
}
/// Проверяет права администратора.
/// Должен идти строго после auth_guard.
pub async fn admin_guard(req: Request, next: Next) -> Result<Response, AppError> {
let user = req
.extensions()
.get::<User>()
.ok_or_else(|| AppError::Unauthorized("Context lost: Auth required".into()))?;
if user.role != "admin" {
warn!(
"SECURITY_ALERT: Unauthorized admin access attempt by @{:?}",
user.tg_username
);
return Err(AppError::Unauthorized(
"Insufficient clearance level".into(),
));
}
Ok(next.run(req).await)
}
+6
View File
@@ -0,0 +1,6 @@
pub mod controller;
pub mod guard;
pub mod service;
pub use guard::{admin_guard, auth_guard};
pub use service::{AuthService, Claims};
+135
View File
@@ -0,0 +1,135 @@
use chrono::Utc;
use hmac::{Hmac, KeyInit, Mac};
use jsonwebtoken::{encode, EncodingKey, Header};
use sha2::{Digest, Sha256};
use std::sync::Arc;
use tracing::{error, info, instrument};
use uuid::Uuid;
use crate::{
db::{models::User, repository::AppRepository},
error::AppError,
};
#[derive(serde::Serialize, serde::Deserialize, Debug)]
pub struct Claims {
pub sub: String,
pub tg_id: i64,
pub exp: usize,
}
#[derive(Clone)]
pub struct AuthService {
repo: Arc<dyn AppRepository>,
}
impl AuthService {
pub fn new(repo: Arc<dyn AppRepository>) -> Self {
Self { repo }
}
#[instrument(skip(self, secret))]
pub fn generate_auth_token(
&self,
user_id: Uuid,
tg_id: i64,
secret: &str,
) -> Result<String, AppError> {
let claims = Claims {
sub: user_id.to_string(),
tg_id,
exp: (Utc::now() + chrono::Duration::hours(24)).timestamp() as usize,
};
encode(
&Header::default(),
&claims,
&EncodingKey::from_secret(secret.as_bytes()),
)
.map_err(|e| {
error!("[Auth] Failed to generate JWT: {}", e);
AppError::Internal
})
}
#[instrument(skip(self, bot_token, hash))]
pub fn verify_tg_widget_auth(
&self,
bot_token: &str,
id: i64,
first_name: Option<&str>,
last_name: Option<&str>,
username: Option<&str>,
photo_url: Option<&str>,
auth_date: i64,
hash: &str,
) -> Result<(), AppError> {
let mut data_check_arr = vec![format!("auth_date={}", auth_date), format!("id={}", id)];
if let Some(fn_) = first_name {
data_check_arr.push(format!("first_name={}", fn_));
}
if let Some(ln) = last_name {
data_check_arr.push(format!("last_name={}", ln));
}
if let Some(pu) = photo_url {
data_check_arr.push(format!("photo_url={}", pu));
}
if let Some(un) = username {
data_check_arr.push(format!("username={}", un));
}
data_check_arr.sort();
let data_check_string = data_check_arr.join("\n");
let mut hasher = Sha256::new();
hasher.update(bot_token.trim().as_bytes());
let secret_key = hasher.finalize();
let mut mac =
Hmac::<Sha256>::new_from_slice(&secret_key).map_err(|_| AppError::Internal)?;
mac.update(data_check_string.as_bytes());
if hex::encode(mac.finalize().into_bytes()) != hash {
return Err(AppError::Unauthorized("Invalid Telegram hash".into()));
}
if Utc::now().timestamp() - auth_date > 86400 {
return Err(AppError::Unauthorized("Auth date expired".into()));
}
Ok(())
}
pub async fn process_login(
&self,
tg_id: i64,
username: Option<String>,
ref_code: Option<String>,
) -> Result<User, AppError> {
let is_new = self.repo.get_user_by_tg_id(tg_id).await?.is_none();
let user = self.repo.upsert_user(tg_id, username).await?;
if is_new {
if let Some(code) = ref_code {
if let Ok(referrer_tg) = code.parse::<i64>() {
if referrer_tg != tg_id {
let _ = self.handle_referral(referrer_tg, user.id).await;
}
}
}
}
Ok(user)
}
async fn handle_referral(&self, referrer_tg: i64, new_user_id: Uuid) -> Result<(), AppError> {
if let Some(referrer) = self.repo.get_user_by_tg_id(referrer_tg).await? {
info!(
"[Auth] Creating syndicate link: {} -> {}",
referrer.id, new_user_id
);
self.repo
.create_referral(referrer.id, new_user_id)
.await
.ok();
}
Ok(())
}
}