- 登录成功/失败均写入审计日志(含 IP、User-Agent) - 登出、密码修改添加审计日志 - 用户/角色 update 记录变更前后值(old_value/new_value) - 插件数据 CRUD(create/update/delete)添加审计日志 - auth handler 提取 X-Forwarded-For/X-Real-IP/User-Agent
192 lines
5.5 KiB
Rust
192 lines
5.5 KiB
Rust
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<LoginResp>),
|
|
(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<S>(
|
|
State(state): State<AuthState>,
|
|
headers: HeaderMap,
|
|
Json(req): Json<LoginReq>,
|
|
) -> Result<Json<ApiResponse<LoginResp>>, AppError>
|
|
where
|
|
AuthState: FromRef<S>,
|
|
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<LoginResp>),
|
|
(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<S>(
|
|
State(state): State<AuthState>,
|
|
Json(req): Json<RefreshReq>,
|
|
) -> Result<Json<ApiResponse<LoginResp>>, AppError>
|
|
where
|
|
AuthState: FromRef<S>,
|
|
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<S>(
|
|
State(state): State<AuthState>,
|
|
headers: HeaderMap,
|
|
Extension(ctx): Extension<TenantContext>,
|
|
) -> Result<Json<ApiResponse<()>>, AppError>
|
|
where
|
|
AuthState: FromRef<S>,
|
|
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<S>(
|
|
State(state): State<AuthState>,
|
|
headers: HeaderMap,
|
|
Extension(ctx): Extension<TenantContext>,
|
|
Json(req): Json<ChangePasswordReq>,
|
|
) -> Result<Json<ApiResponse<()>>, AppError>
|
|
where
|
|
AuthState: FromRef<S>,
|
|
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()),
|
|
}))
|
|
}
|