feat: 添加 TOTP 2FA 功能

This commit is contained in:
mofeng-git
2026-07-17 16:34:06 +08:00
parent fdea52d59d
commit 8bd16df6f8
27 changed files with 1505 additions and 54 deletions

View File

@@ -1,4 +1,5 @@
use super::*;
use crate::auth::server_time_unix_ms;
use crate::state::ShutdownAction;
/// Change password request
@@ -8,6 +9,133 @@ pub struct ChangePasswordRequest {
pub new_password: String,
}
#[derive(Serialize)]
pub struct TotpStatusResponse {
pub enabled: bool,
pub server_time_unix_ms: u64,
}
#[derive(Deserialize)]
pub struct BeginTotpEnrollmentRequest {
pub current_password: String,
}
#[derive(Serialize)]
pub struct TotpEnrollmentResponse {
pub enrollment_id: String,
pub secret: String,
pub otpauth_uri: String,
pub expires_at_unix_ms: u64,
pub server_time_unix_ms: u64,
}
#[derive(Deserialize)]
pub struct ConfirmTotpEnrollmentRequest {
pub enrollment_id: String,
pub code: String,
}
#[derive(Deserialize)]
pub struct DisableTotpRequest {
pub current_password: String,
pub code: String,
}
pub async fn totp_status(
State(state): State<Arc<AppState>>,
axum::Extension(session): axum::Extension<Session>,
) -> Result<Json<TotpStatusResponse>> {
Ok(Json(TotpStatusResponse {
enabled: state.two_factor.is_enabled(&session.user_id).await?,
server_time_unix_ms: server_time_unix_ms(),
}))
}
pub async fn begin_totp_enrollment(
State(state): State<Arc<AppState>>,
axum::Extension(session): axum::Extension<Session>,
Json(req): Json<BeginTotpEnrollmentRequest>,
) -> Result<Json<TotpEnrollmentResponse>> {
let user = authenticated_user(&state, &session).await?;
verify_current_password(&state, &user, &req.current_password).await?;
let enrollment = state
.two_factor
.begin_enrollment(&session.id, &user.id, &user.username)
.await?;
Ok(Json(TotpEnrollmentResponse {
enrollment_id: enrollment.id,
secret: enrollment.secret,
otpauth_uri: enrollment.otpauth_uri,
expires_at_unix_ms: enrollment.expires_at_unix_ms,
server_time_unix_ms: server_time_unix_ms(),
}))
}
pub async fn confirm_totp_enrollment(
State(state): State<Arc<AppState>>,
axum::Extension(session): axum::Extension<Session>,
Json(req): Json<ConfirmTotpEnrollmentRequest>,
) -> Result<Json<LoginResponse>> {
authenticated_user(&state, &session).await?;
state
.two_factor
.confirm_enrollment(&session.id, &session.user_id, &req.enrollment_id, &req.code)
.await?;
revoke_other_sessions(&state, &session.id).await?;
Ok(Json(LoginResponse {
success: true,
message: None,
}))
}
pub async fn disable_totp(
State(state): State<Arc<AppState>>,
axum::Extension(session): axum::Extension<Session>,
Json(req): Json<DisableTotpRequest>,
) -> Result<Json<LoginResponse>> {
let user = authenticated_user(&state, &session).await?;
verify_current_password(&state, &user, &req.current_password).await?;
state.two_factor.disable(&user.id, &req.code).await?;
revoke_other_sessions(&state, &session.id).await?;
Ok(Json(LoginResponse {
success: true,
message: None,
}))
}
async fn authenticated_user(state: &Arc<AppState>, session: &Session) -> Result<crate::auth::User> {
state
.users
.single_user()
.await?
.filter(|user| user.id == session.user_id)
.ok_or_else(|| AppError::AuthError("Invalid session".to_string()))
}
async fn verify_current_password(
state: &Arc<AppState>,
user: &crate::auth::User,
password: &str,
) -> Result<()> {
if state
.users
.verify(&user.username, password)
.await?
.is_none()
{
return Err(AppError::AuthError(
"Current password is incorrect".to_string(),
));
}
Ok(())
}
async fn revoke_other_sessions(state: &Arc<AppState>, current_session_id: &str) -> Result<()> {
let revoked = state.sessions.delete_all_except(current_session_id).await?;
state.remember_revoked_sessions(revoked).await;
Ok(())
}
/// Change current user's password
pub async fn change_password(
State(state): State<Arc<AppState>>,