use crate::api::context; use crate::api::envelope::Envelope; use crate::auth; use crate::error::{AppError, ErrorCode}; use crate::services::{credentials, mail, rate_limit, settings}; use crate::state::AppState; use axum::{ extract::{ConnectInfo, State}, http::HeaderMap, routing::post, Json, Router, }; use chrono::{DateTime, Duration, Utc}; use serde::{Deserialize, Serialize}; use std::net::SocketAddr; use uuid::Uuid; pub fn router() -> Router { Router::new() .route("/register", post(register)) .route("/login", post(login)) .route("/send-verification", post(send_verification)) .route("/verify-email", post(verify_email)) .route("/forgot-password", post(forgot_password)) .route("/reset-password", post(reset_password)) } #[derive(Debug, Deserialize)] struct RegisterRequest { email: String, password: String, username: String, } #[derive(Debug, Serialize)] struct RegisterResponse { user: UserView, token: String, message: String, } #[derive(Debug, Deserialize)] struct LoginRequest { email: String, password: String, } #[derive(Debug, Serialize)] struct LoginResponse { token: String, expires_at: DateTime, user: UserView, } #[derive(Debug, Serialize)] struct UserView { id: Uuid, email: String, username: String, role: String, email_verified: bool, } #[derive(Debug, sqlx::FromRow)] struct UserRow { id: Uuid, email: String, username: String, password_hash: String, role: String, is_active: bool, email_verified_at: Option>, token_version: i32, } async fn register( State(state): State, ConnectInfo(addr): ConnectInfo, headers: HeaderMap, Json(req): Json, ) -> Result>, AppError> { let ip = context::client_ip(&headers, addr.ip()); let policy = settings::runtime_policy(&state).await?; if !policy.features.registration_enabled { return Err(AppError::new( ErrorCode::Forbidden, "用户注册功能当前已关闭", )); } rate_limit::enforce( &state, "auth_register_ip", &ip.to_string(), policy.rate_limits.register_ip_per_hour, 60 * 60, "注册请求过于频繁,请稍后再试", ) .await?; credentials::validate_email(&req.email)?; credentials::validate_username(&req.username)?; credentials::validate_password(&req.password)?; let password_hash = credentials::hash_password(&req.password).await?; let verification_required = policy.auth.email_verification_required; let verified_at = (!verification_required).then(Utc::now); let user = sqlx::query_as::<_, UserRow>( r#" INSERT INTO users (email, username, password_hash, email_verified_at) VALUES ($1, $2, $3, $4) RETURNING id, email, username, password_hash, role::text AS role, is_active, email_verified_at, token_version "#, ) .bind(req.email.to_lowercase()) .bind(&req.username) .bind(password_hash) .bind(verified_at) .fetch_one(&state.db) .await .map_err(map_unique_violation)?; let (token, _expires_at) = auth::issue_jwt( &state.config.jwt_secret, state.config.jwt_expiry_hours, user.id, &user.role, user.token_version, )?; if verification_required { let verification_token = credentials::generate_token(); let token_hash = credentials::sha256_hex(&verification_token); let expires_at_db = Utc::now() + Duration::hours(24); sqlx::query( r#" INSERT INTO email_verifications (user_id, token_hash, expires_at) VALUES ($1, $2, $3) "#, ) .bind(user.id) .bind(token_hash) .bind(expires_at_db) .execute(&state.db) .await .map_err(|err| { AppError::new(ErrorCode::Internal, "创建邮箱验证记录失败").with_source(err) })?; let verification_url = format!( "{}/verify-email?token={}", state.config.public_base_url, verification_token ); mail::send_verification_email(&state, &user.email, &user.username, &verification_url) .await .map_err(|err| { AppError::new(ErrorCode::MailSendFailed, "验证邮件发送失败").with_source(err) })?; } let body = RegisterResponse { user: UserView { id: user.id, email: user.email, username: user.username, role: user.role, email_verified: user.email_verified_at.is_some(), }, token, message: if verification_required { "注册成功,验证邮件已发送至您的邮箱".to_string() } else { "注册成功".to_string() }, }; Ok(Json(Envelope { success: true, data: body, })) } async fn login( State(state): State, ConnectInfo(addr): ConnectInfo, headers: HeaderMap, Json(req): Json, ) -> Result>, AppError> { let identity = req.email.trim(); if identity.is_empty() { return Err(AppError::new( ErrorCode::InvalidRequest, "邮箱或用户名不能为空", )); } let ip = context::client_ip(&headers, addr.ip()); let policy = settings::runtime_policy(&state).await?; rate_limit::enforce( &state, "auth_login_ip", &ip.to_string(), policy.rate_limits.login_ip_per_5_minutes, 5 * 60, "登录请求过于频繁,请稍后再试", ) .await?; rate_limit::enforce( &state, "auth_login_identity", &identity.to_lowercase(), policy.rate_limits.login_identity_per_5_minutes, 5 * 60, "该账号登录尝试过于频繁,请稍后再试", ) .await?; let user = if identity.contains('@') { credentials::validate_email(identity)?; sqlx::query_as::<_, UserRow>( r#" SELECT id, email, username, password_hash, role::text AS role, is_active, email_verified_at, token_version FROM users WHERE email = $1 "#, ) .bind(identity.to_lowercase()) .fetch_optional(&state.db) .await } else { credentials::validate_username(identity)?; sqlx::query_as::<_, UserRow>( r#" SELECT id, email, username, password_hash, role::text AS role, is_active, email_verified_at, token_version FROM users WHERE username = $1 "#, ) .bind(identity) .fetch_optional(&state.db) .await } .map_err(|err| AppError::new(ErrorCode::Internal, "查询用户失败").with_source(err))?; let Some(user) = user else { credentials::consume_dummy_password_work(&req.password).await?; return Err(AppError::new(ErrorCode::Unauthorized, "账号或密码错误")); }; if !credentials::verify_password(&req.password, &user.password_hash).await? { return Err(AppError::new(ErrorCode::Unauthorized, "账号或密码错误")); } if !user.is_active { return Err(AppError::new(ErrorCode::Forbidden, "账号已被禁用")); } let verification_required = policy.auth.email_verification_required; let (token, expires_at) = auth::issue_jwt( &state.config.jwt_secret, state.config.jwt_expiry_hours, user.id, &user.role, user.token_version, )?; Ok(Json(Envelope { success: true, data: LoginResponse { token, expires_at, user: UserView { id: user.id, email: user.email, username: user.username, role: user.role, email_verified: user.email_verified_at.is_some() || !verification_required, }, }, })) } #[derive(Debug, Serialize)] struct MessageResponse { message: String, } async fn send_verification( State(state): State, headers: HeaderMap, ) -> Result>, AppError> { let claims = auth::require_jwt(&state.config.jwt_secret, &headers)?; let policy = settings::runtime_policy(&state).await?; if !policy.auth.email_verification_required { return Ok(Json(Envelope { success: true, data: MessageResponse { message: "邮箱验证功能当前已关闭,无需验证".to_string(), }, })); } rate_limit::enforce( &state, "auth_send_verification_user", &claims.sub.to_string(), policy.rate_limits.verification_email_per_minute, 60, "发送过于频繁,请稍后再试", ) .await?; let user = sqlx::query_as::<_, UserRow>( r#" SELECT id, email, username, password_hash, role::text AS role, is_active, email_verified_at, token_version FROM users WHERE id = $1 "#, ) .bind(claims.sub) .fetch_optional(&state.db) .await .map_err(|err| AppError::new(ErrorCode::Internal, "查询用户失败").with_source(err))? .ok_or_else(|| AppError::new(ErrorCode::Unauthorized, "用户不存在或未登录"))?; if claims.ver != user.token_version { return Err(AppError::new( ErrorCode::Unauthorized, "登录状态已失效,请重新登录", )); } if user.email_verified_at.is_some() { return Ok(Json(Envelope { success: true, data: MessageResponse { message: "邮箱已验证,无需重复验证".to_string(), }, })); } let verification_token = credentials::generate_token(); let token_hash = credentials::sha256_hex(&verification_token); let expires_at_db = Utc::now() + Duration::hours(24); sqlx::query( r#" INSERT INTO email_verifications (user_id, token_hash, expires_at) VALUES ($1, $2, $3) "#, ) .bind(user.id) .bind(token_hash) .bind(expires_at_db) .execute(&state.db) .await .map_err(|err| AppError::new(ErrorCode::Internal, "创建邮箱验证记录失败").with_source(err))?; let verification_url = format!( "{}/verify-email?token={}", state.config.public_base_url, verification_token ); mail::send_verification_email(&state, &user.email, &user.username, &verification_url) .await .map_err(|err| { AppError::new(ErrorCode::MailSendFailed, "验证邮件发送失败").with_source(err) })?; Ok(Json(Envelope { success: true, data: MessageResponse { message: "验证邮件已发送,请查收".to_string(), }, })) } #[derive(Debug, Deserialize)] struct VerifyEmailRequest { token: String, } #[derive(Debug, Serialize)] struct VerifyEmailResponse { message: String, session_invalidated: bool, } struct EmailChangeConfirmation { old_email: String, } async fn verify_email( State(state): State, ConnectInfo(addr): ConnectInfo, headers: HeaderMap, Json(req): Json, ) -> Result>, AppError> { if req.token.trim().is_empty() { return Err(AppError::new(ErrorCode::InvalidRequest, "token 不能为空")); } let ip = context::client_ip(&headers, addr.ip()); let policy = settings::runtime_policy(&state).await?; rate_limit::enforce( &state, "auth_verify_email_ip", &ip.to_string(), policy.rate_limits.email_verify_ip_per_15_minutes, 15 * 60, "验证请求过于频繁,请稍后再试", ) .await?; let token_hash = credentials::sha256_hex(&req.token); let now = Utc::now(); if let Some(confirmation) = confirm_email_change(&state, &token_hash, now).await? { if let Err(err) = mail::send_email_change_notice(&state, &confirmation.old_email).await { tracing::warn!(error = ?err, "email change notice delivery failed"); } return Ok(Json(Envelope { success: true, data: VerifyEmailResponse { message: "新邮箱确认成功,请重新登录".to_string(), session_invalidated: true, }, })); } let updated = sqlx::query( r#" WITH v AS ( SELECT user_id FROM email_verifications WHERE token_hash = $1 AND verified_at IS NULL AND expires_at > $2 LIMIT 1 ), u AS ( UPDATE users SET email_verified_at = $2 WHERE id = (SELECT user_id FROM v) AND email_verified_at IS NULL RETURNING id ) UPDATE email_verifications SET verified_at = $2 WHERE token_hash = $1 AND verified_at IS NULL AND expires_at > $2 AND user_id IN (SELECT id FROM u) "#, ) .bind(token_hash) .bind(now) .execute(&state.db) .await .map_err(|err| AppError::new(ErrorCode::Internal, "邮箱验证失败").with_source(err))?; if updated.rows_affected() == 0 { return Err(AppError::new(ErrorCode::InvalidToken, "Token 无效或已过期")); } Ok(Json(Envelope { success: true, data: VerifyEmailResponse { message: "邮箱验证成功".to_string(), session_invalidated: false, }, })) } async fn confirm_email_change( state: &AppState, token_hash: &str, now: DateTime, ) -> Result, AppError> { let user_id: Option = sqlx::query_scalar( r#" SELECT user_id FROM email_change_requests WHERE token_hash = $1 AND confirmed_at IS NULL AND canceled_at IS NULL AND expires_at > $2 "#, ) .bind(token_hash) .bind(now) .fetch_optional(&state.db) .await .map_err(|err| AppError::new(ErrorCode::Internal, "查询邮箱变更请求失败").with_source(err))?; let Some(user_id) = user_id else { return Ok(None); }; let mut tx = state .db .begin() .await .map_err(|err| AppError::new(ErrorCode::Internal, "开启事务失败").with_source(err))?; let user: Option<(String,)> = sqlx::query_as("SELECT email FROM users WHERE id = $1 FOR UPDATE") .bind(user_id) .fetch_optional(&mut *tx) .await .map_err(|err| AppError::new(ErrorCode::Internal, "锁定用户失败").with_source(err))?; let Some((old_email,)) = user else { tx.rollback().await.ok(); return Ok(None); }; let request: Option<(Uuid, String)> = sqlx::query_as( r#" SELECT id, new_email FROM email_change_requests WHERE token_hash = $1 AND user_id = $2 AND confirmed_at IS NULL AND canceled_at IS NULL AND expires_at > $3 FOR UPDATE "#, ) .bind(token_hash) .bind(user_id) .bind(now) .fetch_optional(&mut *tx) .await .map_err(|err| AppError::new(ErrorCode::Internal, "锁定邮箱变更请求失败").with_source(err))?; let Some((request_id, new_email)) = request else { tx.rollback().await.ok(); return Ok(None); }; let changed = sqlx::query( r#" UPDATE users u SET email = $2, email_verified_at = $3, token_version = token_version + 1, updated_at = NOW() WHERE u.id = $1 AND NOT EXISTS ( SELECT 1 FROM users other WHERE other.email = $2 AND other.id <> u.id ) "#, ) .bind(user_id) .bind(&new_email) .bind(now) .execute(&mut *tx) .await .map_err(|err| { if matches!(&err, sqlx::Error::Database(db) if db.code().as_deref() == Some("23505")) { AppError::new(ErrorCode::InvalidRequest, "新邮箱已被其他账号使用") } else { AppError::new(ErrorCode::Internal, "确认新邮箱失败").with_source(err) } })?; if changed.rows_affected() == 0 { return Err(AppError::new( ErrorCode::InvalidRequest, "新邮箱已被其他账号使用", )); } sqlx::query( "UPDATE email_change_requests SET confirmed_at = $2 WHERE id = $1 AND confirmed_at IS NULL AND canceled_at IS NULL", ) .bind(request_id) .bind(now) .execute(&mut *tx) .await .map_err(|err| AppError::new(ErrorCode::Internal, "完成邮箱变更请求失败").with_source(err))?; sqlx::query( "UPDATE email_change_requests SET canceled_at = $2 WHERE user_id = $1 AND id <> $3 AND confirmed_at IS NULL AND canceled_at IS NULL", ) .bind(user_id) .bind(now) .bind(request_id) .execute(&mut *tx) .await .map_err(|err| AppError::new(ErrorCode::Internal, "撤销旧邮箱变更请求失败").with_source(err))?; sqlx::query("UPDATE password_resets SET used_at = $2 WHERE user_id = $1 AND used_at IS NULL") .bind(user_id) .bind(now) .execute(&mut *tx) .await .map_err(|err| { AppError::new(ErrorCode::Internal, "撤销密码重置请求失败").with_source(err) })?; sqlx::query( "UPDATE email_verifications SET verified_at = $2 WHERE user_id = $1 AND verified_at IS NULL", ) .bind(user_id) .bind(now) .execute(&mut *tx) .await .map_err(|err| AppError::new(ErrorCode::Internal, "撤销旧邮箱验证请求失败").with_source(err))?; tx.commit() .await .map_err(|err| AppError::new(ErrorCode::Internal, "提交邮箱变更失败").with_source(err))?; Ok(Some(EmailChangeConfirmation { old_email })) } #[derive(Debug, Deserialize)] struct ForgotPasswordRequest { email: String, } async fn forgot_password( State(state): State, ConnectInfo(addr): ConnectInfo, headers: HeaderMap, Json(req): Json, ) -> Result>, AppError> { credentials::validate_email(&req.email)?; let ip = context::client_ip(&headers, addr.ip()); let policy = settings::runtime_policy(&state).await?; rate_limit::enforce( &state, "auth_forgot_ip", &ip.to_string(), policy.rate_limits.forgot_password_ip_per_15_minutes, 15 * 60, "找回密码请求过于频繁,请稍后再试", ) .await?; rate_limit::enforce( &state, "auth_forgot_email", &req.email.to_lowercase(), policy.rate_limits.forgot_password_email_per_15_minutes, 15 * 60, "找回密码请求过于频繁,请稍后再试", ) .await?; let requested_email = req.email.to_lowercase(); let mut tx = state .db .begin() .await .map_err(|err| AppError::new(ErrorCode::Internal, "开启事务失败").with_source(err))?; let user = sqlx::query_as::<_, UserRow>( r#" SELECT id, email, username, password_hash, role::text AS role, is_active, email_verified_at, token_version FROM users WHERE email = $1 FOR UPDATE "#, ) .bind(&requested_email) .fetch_optional(&mut *tx) .await .map_err(|err| AppError::new(ErrorCode::Internal, "查询用户失败").with_source(err))?; let delivery = if let Some(user) = user { let reset_token = credentials::generate_token(); let token_hash = credentials::sha256_hex(&reset_token); let expires_at_db = Utc::now() + Duration::hours(1); sqlx::query( r#" INSERT INTO password_resets (user_id, token_hash, expires_at) VALUES ($1, $2, $3) "#, ) .bind(user.id) .bind(token_hash) .bind(expires_at_db) .execute(&mut *tx) .await .map_err(|err| { AppError::new(ErrorCode::Internal, "创建密码重置记录失败").with_source(err) })?; let reset_url = format!( "{}/reset-password?token={}", state.config.public_base_url, reset_token ); Some((user.email, user.username, reset_url)) } else { None }; tx.commit().await.map_err(|err| { AppError::new(ErrorCode::Internal, "提交密码找回事务失败").with_source(err) })?; if let Some((email, username, reset_url)) = delivery { let _ = mail::send_password_reset_email(&state, &email, &username, &reset_url).await; } Ok(Json(Envelope { success: true, data: MessageResponse { message: "如果该邮箱已注册,您将收到重置邮件".to_string(), }, })) } #[derive(Debug, Deserialize)] struct ResetPasswordRequest { token: String, new_password: String, } async fn reset_password( State(state): State, ConnectInfo(addr): ConnectInfo, headers: HeaderMap, Json(req): Json, ) -> Result>, AppError> { if req.token.trim().is_empty() { return Err(AppError::new(ErrorCode::InvalidRequest, "token 不能为空")); } credentials::validate_password(&req.new_password)?; let ip = context::client_ip(&headers, addr.ip()); let policy = settings::runtime_policy(&state).await?; rate_limit::enforce( &state, "auth_reset_ip", &ip.to_string(), policy.rate_limits.password_reset_ip_per_15_minutes, 15 * 60, "重置密码请求过于频繁,请稍后再试", ) .await?; rate_limit::enforce( &state, "auth_reset_token", &req.token, policy.rate_limits.password_reset_token_per_15_minutes, 15 * 60, "该重置链接尝试次数过多,请重新申请", ) .await?; let token_hash = credentials::sha256_hex(&req.token); let now = Utc::now(); let reset_user_id: Option = sqlx::query_scalar( r#" SELECT user_id FROM password_resets WHERE token_hash = $1 AND used_at IS NULL AND expires_at > $2 "#, ) .bind(&token_hash) .bind(now) .fetch_optional(&state.db) .await .map_err(|err| AppError::new(ErrorCode::Internal, "重置密码失败").with_source(err))?; let Some(reset_user_id) = reset_user_id else { return Err(AppError::new(ErrorCode::InvalidToken, "Token 无效或已过期")); }; let password_hash = credentials::hash_password(&req.new_password).await?; let mut tx = state .db .begin() .await .map_err(|err| AppError::new(ErrorCode::Internal, "开启事务失败").with_source(err))?; let locked_user: Option = sqlx::query_scalar("SELECT id FROM users WHERE id = $1 FOR UPDATE") .bind(reset_user_id) .fetch_optional(&mut *tx) .await .map_err(|err| AppError::new(ErrorCode::Internal, "锁定用户失败").with_source(err))?; if locked_user.is_none() { return Err(AppError::new(ErrorCode::InvalidToken, "Token 无效或已过期")); } let user_id: Option = sqlx::query_scalar( r#" SELECT user_id FROM password_resets WHERE token_hash = $1 AND used_at IS NULL AND expires_at > $2 FOR UPDATE "#, ) .bind(&token_hash) .bind(now) .fetch_optional(&mut *tx) .await .map_err(|err| AppError::new(ErrorCode::Internal, "重置密码失败").with_source(err))?; let Some(user_id) = user_id else { return Err(AppError::new(ErrorCode::InvalidToken, "Token 无效或已过期")); }; if user_id != reset_user_id { return Err(AppError::new(ErrorCode::InvalidToken, "Token 无效或已过期")); } sqlx::query( "UPDATE users SET password_hash = $1, token_version = token_version + 1, updated_at = NOW() WHERE id = $2", ) .bind(password_hash) .bind(user_id) .execute(&mut *tx) .await .map_err(|err| AppError::new(ErrorCode::Internal, "更新密码失败").with_source(err))?; credentials::invalidate_account_recovery(&mut tx, user_id, now).await?; tx.commit() .await .map_err(|err| AppError::new(ErrorCode::Internal, "提交事务失败").with_source(err))?; Ok(Json(Envelope { success: true, data: MessageResponse { message: "密码重置成功".to_string(), }, })) } fn map_unique_violation(err: sqlx::Error) -> AppError { if let sqlx::Error::Database(db_err) = &err { if let Some(code) = db_err.code() { if code == "23505" { return AppError::new(ErrorCode::InvalidRequest, "邮箱或用户名已存在"); } } } AppError::new(ErrorCode::Internal, "数据库操作失败").with_source(err) }