Skip to content
Clone
e1df092
225 lines 7.4 KB Rust
use axum::Json;
use axum::extract::State;
use axum::http::HeaderMap;
use export::rest::{AppResponseData, NoData};
use jwt::Session;
use jwt::share::session;
use kenauth::proto::common::v1::MfaStatus;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use crate::errors::ApiError;
use crate::services::AuthService;
use crate::share::rpc;
#[derive(Debug, Serialize, ToSchema)]
pub struct TotpStatusResp {
pub enabled: bool,
}
#[derive(Debug, Serialize, ToSchema)]
pub struct TotpSetupResp {
pub secret: String,
pub otpauth_uri: String,
}
#[derive(Debug, Deserialize, ToSchema)]
pub struct TotpEnableCmd {
pub code: String,
pub password: String,
}
#[derive(Debug, Deserialize, ToSchema)]
pub struct TotpCodeCmd {
pub code: String,
}
#[derive(Debug, Deserialize, ToSchema)]
pub struct RegenerateRecoveryCodesCmd {
pub password: String,
}
#[derive(Debug, Serialize, ToSchema)]
pub struct RecoveryCodesResp {
pub total: u32,
pub used: u32,
pub codes: Vec<String>,
}
#[utoipa::path(
get,
path = "/auth/2fa/status",
tag = "auth",
description = "查询当前账号是否已启用两步验证。",
responses(
(status = 200, description = "two-factor status", body = AppResponseData<TotpStatusResp>),
(status = 401, description = "authentication required", body = AppResponseData<NoData>)
)
)]
pub async fn totp_status(
State(service): State<AuthService>,
session: Session,
) -> Result<AppResponseData<TotpStatusResp>, ApiError> {
let uid = session::require_uid(&session)?;
let client = rpc::client(&service.state)?;
let status = client
.get_mfa_status(uid)
.await
.map_err(|error| ApiError::internal(error.to_string()))?;
let enabled = status
.factors
.iter()
.any(|factor| factor.status == MfaStatus::Enabled as i32);
Ok(AppResponseData::ok(TotpStatusResp { enabled }))
}
#[utoipa::path(
post,
path = "/auth/2fa/setup",
tag = "auth",
description = "生成新的 TOTP 密钥并返回 otpauth URI,密钥明文只返回一次。",
responses(
(status = 200, description = "totp secret provisioned", body = AppResponseData<TotpSetupResp>),
(status = 400, description = "request failed validation", body = AppResponseData<NoData>),
(status = 401, description = "authentication required", body = AppResponseData<NoData>)
)
)]
pub async fn totp_setup(
State(service): State<AuthService>,
session: Session,
headers: HeaderMap,
) -> Result<AppResponseData<TotpSetupResp>, ApiError> {
let uid = session::require_uid(&session)?;
let client = rpc::client(&service.state)?;
let configured = client
.configure_totp(uid, "TOTP", &rpc::client_ip(&headers))
.await
.map_err(|error| ApiError::bad_request(error.to_string()))?;
Ok(AppResponseData::ok(TotpSetupResp {
secret: configured.secret,
otpauth_uri: configured.otpauth_uri,
}))
}
#[utoipa::path(
post,
path = "/auth/2fa/enable",
tag = "auth",
description = "用动态码与账号口令确认启用两步验证,返回一次性恢复码。",
request_body = TotpEnableCmd,
responses(
(status = 200, description = "two-factor enabled and recovery codes issued", body = AppResponseData<RecoveryCodesResp>),
(status = 400, description = "request failed validation", body = AppResponseData<NoData>),
(status = 401, description = "authentication required", body = AppResponseData<NoData>)
)
)]
pub async fn totp_enable(
State(service): State<AuthService>,
session: Session,
headers: HeaderMap,
Json(cmd): Json<TotpEnableCmd>,
) -> Result<AppResponseData<RecoveryCodesResp>, ApiError> {
let uid = session::require_uid(&session)?;
let client = rpc::client(&service.state)?;
let enabled = client
.enable_totp(
uid,
cmd.code.trim(),
&rpc::digest(&cmd.password),
&rpc::client_ip(&headers),
)
.await
.map_err(|error| ApiError::bad_request(error.to_string()))?;
let total = enabled.recovery_codes.len() as u32;
Ok(AppResponseData::ok(RecoveryCodesResp {
total,
used: 0,
codes: enabled.recovery_codes,
}))
}
#[utoipa::path(
post,
path = "/auth/2fa/disable",
tag = "auth",
description = "用动态码或一枚恢复码关闭两步验证,并吊销全部恢复码。",
request_body = TotpCodeCmd,
responses(
(status = 200, description = "two-factor disabled", body = AppResponseData<NoData>),
(status = 400, description = "request failed validation", body = AppResponseData<NoData>),
(status = 401, description = "authentication required", body = AppResponseData<NoData>)
)
)]
pub async fn totp_disable(
State(service): State<AuthService>,
session: Session,
headers: HeaderMap,
Json(cmd): Json<TotpCodeCmd>,
) -> Result<AppResponseData<NoData>, ApiError> {
let uid = session::require_uid(&session)?;
let client = rpc::client(&service.state)?;
client
.disable_totp(uid, cmd.code.trim(), &rpc::client_ip(&headers))
.await
.map_err(|error| ApiError::bad_request(error.to_string()))?;
Ok(AppResponseData::empty())
}
#[utoipa::path(
get,
path = "/auth/2fa/recovery-codes",
tag = "auth",
description = "查看恢复码批次用量。明文只在生成时返回一次,这里不会回显。",
responses(
(status = 200, description = "recovery code usage", body = AppResponseData<RecoveryCodesResp>),
(status = 401, description = "authentication required", body = AppResponseData<NoData>)
)
)]
pub async fn recovery_codes(
State(service): State<AuthService>,
session: Session,
) -> Result<AppResponseData<RecoveryCodesResp>, ApiError> {
let uid = session::require_uid(&session)?;
let client = rpc::client(&service.state)?;
let status = client
.get_mfa_status(uid)
.await
.map_err(|error| ApiError::internal(error.to_string()))?;
Ok(AppResponseData::ok(RecoveryCodesResp {
total: status.recovery_codes_total,
used: status.recovery_codes_used,
codes: Vec::new(),
}))
}
#[utoipa::path(
put,
path = "/auth/2fa/recovery-codes",
tag = "auth",
description = "校验账号口令后重建恢复码批次,返回新的一批明文。",
request_body = RegenerateRecoveryCodesCmd,
responses(
(status = 200, description = "recovery codes regenerated", body = AppResponseData<RecoveryCodesResp>),
(status = 400, description = "request failed validation", body = AppResponseData<NoData>),
(status = 401, description = "authentication required", body = AppResponseData<NoData>)
)
)]
pub async fn regenerate_recovery_codes(
State(service): State<AuthService>,
session: Session,
headers: HeaderMap,
Json(cmd): Json<RegenerateRecoveryCodesCmd>,
) -> Result<AppResponseData<RecoveryCodesResp>, ApiError> {
let uid = session::require_uid(&session)?;
let client = rpc::client(&service.state)?;
let regenerated = client
.regenerate_recovery_codes(uid, &rpc::digest(&cmd.password), &rpc::client_ip(&headers))
.await
.map_err(|error| ApiError::bad_request(error.to_string()))?;
let total = regenerated.recovery_codes.len() as u32;
Ok(AppResponseData::ok(RecoveryCodesResp {
total,
used: 0,
codes: regenerated.recovery_codes,
}))
}