//! 认证 HTTP 处理器 use axum::{extract::State, http::StatusCode, Json}; use secrecy::ExposeSecret; use crate::state::AppState; use crate::error::{SaasError, SaasResult}; use super::{ jwt::create_token, password::{hash_password, verify_password}, types::{AuthContext, LoginRequest, LoginResponse, RegisterRequest, AccountPublic}, }; /// POST /api/v1/auth/register pub async fn register( State(state): State, Json(req): Json, ) -> SaasResult<(StatusCode, Json)> { if req.username.len() < 3 { return Err(SaasError::InvalidInput("用户名至少 3 个字符".into())); } if req.password.len() < 8 { return Err(SaasError::InvalidInput("密码至少 8 个字符".into())); } let existing: Vec<(String,)> = sqlx::query_as( "SELECT id FROM accounts WHERE username = ?1 OR email = ?2" ) .bind(&req.username) .bind(&req.email) .fetch_all(&state.db) .await?; if !existing.is_empty() { return Err(SaasError::AlreadyExists("用户名或邮箱已存在".into())); } let password_hash = hash_password(&req.password)?; let account_id = uuid::Uuid::new_v4().to_string(); let role = "user".to_string(); // 注册固定为普通用户,角色由管理员分配 let display_name = req.display_name.unwrap_or_default(); let now = chrono::Utc::now().to_rfc3339(); sqlx::query( "INSERT INTO accounts (id, username, email, password_hash, display_name, role, status, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, 'active', ?7, ?7)" ) .bind(&account_id) .bind(&req.username) .bind(&req.email) .bind(&password_hash) .bind(&display_name) .bind(&role) .bind(&now) .execute(&state.db) .await?; log_operation(&state.db, &account_id, "account.create", "account", &account_id, None, None).await?; Ok((StatusCode::CREATED, Json(AccountPublic { id: account_id, username: req.username, email: req.email, display_name, role, status: "active".into(), totp_enabled: false, created_at: now, }))) } /// POST /api/v1/auth/login pub async fn login( State(state): State, Json(req): Json, ) -> SaasResult> { let row: Option<(String, String, String, String, String, String, bool, String)> = sqlx::query_as( "SELECT id, username, email, display_name, role, status, totp_enabled, created_at FROM accounts WHERE username = ?1 OR email = ?1" ) .bind(&req.username) .fetch_optional(&state.db) .await?; let (id, username, email, display_name, role, status, totp_enabled, created_at) = row.ok_or_else(|| SaasError::AuthError("用户名或密码错误".into()))?; if status != "active" { return Err(SaasError::Forbidden(format!("账号已{},请联系管理员", status))); } let (password_hash,): (String,) = sqlx::query_as( "SELECT password_hash FROM accounts WHERE id = ?1" ) .bind(&id) .fetch_one(&state.db) .await?; if !verify_password(&req.password, &password_hash)? { return Err(SaasError::AuthError("用户名或密码错误".into())); } let permissions = get_role_permissions(&state.db, &role).await?; let config = state.config.read().await; let token = create_token( &id, &role, permissions.clone(), state.jwt_secret.expose_secret(), config.auth.jwt_expiration_hours, )?; let now = chrono::Utc::now().to_rfc3339(); sqlx::query("UPDATE accounts SET last_login_at = ?1 WHERE id = ?2") .bind(&now).bind(&id) .execute(&state.db).await?; log_operation(&state.db, &id, "account.login", "account", &id, None, None).await?; Ok(Json(LoginResponse { token, account: AccountPublic { id, username, email, display_name, role, status, totp_enabled, created_at, }, })) } /// POST /api/v1/auth/refresh pub async fn refresh( State(state): State, axum::extract::Extension(ctx): axum::extract::Extension, ) -> SaasResult> { let config = state.config.read().await; let token = create_token( &ctx.account_id, &ctx.role, ctx.permissions.clone(), state.jwt_secret.expose_secret(), config.auth.jwt_expiration_hours, )?; Ok(Json(serde_json::json!({ "token": token }))) } /// GET /api/v1/auth/me — 返回当前认证用户的公开信息 pub async fn me( State(state): State, axum::extract::Extension(ctx): axum::extract::Extension, ) -> SaasResult> { let row: Option<(String, String, String, String, String, String, bool, String)> = sqlx::query_as( "SELECT id, username, email, display_name, role, status, totp_enabled, created_at FROM accounts WHERE id = ?1" ) .bind(&ctx.account_id) .fetch_optional(&state.db) .await?; let (id, username, email, display_name, role, status, totp_enabled, created_at) = row.ok_or_else(|| SaasError::NotFound("账号不存在".into()))?; Ok(Json(AccountPublic { id, username, email, display_name, role, status, totp_enabled, created_at, })) } pub(crate) async fn get_role_permissions(db: &sqlx::SqlitePool, role: &str) -> SaasResult> { let row: Option<(String,)> = sqlx::query_as( "SELECT permissions FROM roles WHERE id = ?1" ) .bind(role) .fetch_optional(db) .await?; let permissions_str = row .ok_or_else(|| SaasError::Internal(format!("角色 {} 不存在", role)))? .0; let permissions: Vec = serde_json::from_str(&permissions_str)?; Ok(permissions) } /// 检查权限 (admin:full 自动通过所有检查) pub fn check_permission(ctx: &AuthContext, permission: &str) -> SaasResult<()> { if ctx.permissions.contains(&"admin:full".to_string()) { return Ok(()); } if !ctx.permissions.contains(&permission.to_string()) { return Err(SaasError::Forbidden(format!("需要 {} 权限", permission))); } Ok(()) } /// 记录操作日志 pub async fn log_operation( db: &sqlx::SqlitePool, account_id: &str, action: &str, target_type: &str, target_id: &str, details: Option, ip_address: Option<&str>, ) -> SaasResult<()> { let now = chrono::Utc::now().to_rfc3339(); sqlx::query( "INSERT INTO operation_logs (account_id, action, target_type, target_id, details, ip_address, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)" ) .bind(account_id) .bind(action) .bind(target_type) .bind(target_id) .bind(details.map(|d| d.to_string())) .bind(ip_address) .bind(&now) .execute(db) .await?; Ok(()) }