mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-07-29 12:41:45 +08:00
feat: 添加 TOTP 2FA 功能
This commit is contained in:
@@ -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>>,
|
||||
|
||||
Reference in New Issue
Block a user