use axum::Extension; use axum::extract::{FromRef, State}; use axum::http::HeaderMap; use axum::response::Json; use validator::Validate; use erp_core::error::AppError; use erp_core::types::{ApiResponse, TenantContext}; use crate::auth_state::AuthState; use crate::dto::{ChangePasswordReq, LoginReq, LoginResp, RefreshReq}; use crate::service::auth_service::{AuthService, JwtConfig, RequestInfo}; /// 从请求头中提取客户端信息。 fn extract_request_info(headers: &HeaderMap) -> RequestInfo { let ip = headers .get("x-forwarded-for") .or_else(|| headers.get("x-real-ip")) .and_then(|v| v.to_str().ok()) .map(|s| s.split(',').next().unwrap_or(s).trim().to_string()); let user_agent = headers .get("user-agent") .and_then(|v| v.to_str().ok()) .map(|s| s.to_string()); RequestInfo { ip, user_agent } } #[utoipa::path( post, path = "/api/v1/auth/login", request_body = LoginReq, responses( (status = 200, description = "登录成功", body = ApiResponse), (status = 400, description = "请求参数错误"), (status = 401, description = "用户名或密码错误"), ), tag = "认证" )] /// POST /api/v1/auth/login /// /// Authenticates a user with username and password, returning access and refresh tokens. /// /// During the bootstrap phase, the tenant_id is taken from `AuthState::default_tenant_id`. /// In production, this will come from a tenant-resolution middleware. pub async fn login( State(state): State, headers: HeaderMap, Json(req): Json, ) -> Result>, AppError> where AuthState: FromRef, S: Clone + Send + Sync + 'static, { req.validate() .map_err(|e| AppError::Validation(e.to_string()))?; let req_info = extract_request_info(&headers); let tenant_id = state.default_tenant_id; let jwt_config = JwtConfig { secret: &state.jwt_secret, access_ttl_secs: state.access_ttl_secs, refresh_ttl_secs: state.refresh_ttl_secs, }; let resp = AuthService::login( tenant_id, &req.username, &req.password, &state.db, &jwt_config, &state.event_bus, Some(&req_info), ) .await?; Ok(Json(ApiResponse::ok(resp))) } #[utoipa::path( post, path = "/api/v1/auth/refresh", request_body = RefreshReq, responses( (status = 200, description = "刷新成功", body = ApiResponse), (status = 401, description = "刷新令牌无效或已过期"), ), tag = "认证" )] /// POST /api/v1/auth/refresh /// /// Validates an existing refresh token, revokes it (rotation), and issues /// a new access + refresh token pair. pub async fn refresh( State(state): State, Json(req): Json, ) -> Result>, AppError> where AuthState: FromRef, S: Clone + Send + Sync + 'static, { let jwt_config = JwtConfig { secret: &state.jwt_secret, access_ttl_secs: state.access_ttl_secs, refresh_ttl_secs: state.refresh_ttl_secs, }; let resp = AuthService::refresh(&req.refresh_token, &state.db, &jwt_config).await?; Ok(Json(ApiResponse::ok(resp))) } #[utoipa::path( post, path = "/api/v1/auth/logout", responses( (status = 200, description = "已成功登出"), (status = 401, description = "未授权"), ), security(("bearer_auth" = [])), tag = "认证" )] /// POST /api/v1/auth/logout /// /// Revokes all refresh tokens for the authenticated user, effectively /// logging them out on all devices. pub async fn logout( State(state): State, headers: HeaderMap, Extension(ctx): Extension, ) -> Result>, AppError> where AuthState: FromRef, S: Clone + Send + Sync + 'static, { let req_info = extract_request_info(&headers); AuthService::logout(ctx.user_id, ctx.tenant_id, &state.db, Some(&req_info)).await?; Ok(Json(ApiResponse { success: true, data: None, message: Some("已成功登出".to_string()), })) } #[utoipa::path( post, path = "/api/v1/auth/change-password", request_body = ChangePasswordReq, responses( (status = 200, description = "密码修改成功,需重新登录"), (status = 400, description = "当前密码不正确"), (status = 401, description = "未授权"), ), security(("bearer_auth" = [])), tag = "认证" )] /// POST /api/v1/auth/change-password /// /// 修改当前登录用户的密码。修改成功后所有已签发的 refresh token 将被吊销, /// 用户需要在所有设备上重新登录。 pub async fn change_password( State(state): State, headers: HeaderMap, Extension(ctx): Extension, Json(req): Json, ) -> Result>, AppError> where AuthState: FromRef, S: Clone + Send + Sync + 'static, { req.validate() .map_err(|e| AppError::Validation(e.to_string()))?; let req_info = extract_request_info(&headers); AuthService::change_password( ctx.user_id, ctx.tenant_id, &req.current_password, &req.new_password, &state.db, Some(&req_info), ) .await?; Ok(Json(ApiResponse { success: true, data: None, message: Some("密码修改成功,请重新登录".to_string()), })) }