full backend rewrite.
calling this v0.4.0
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
use crate::api::auth::{Claims, Session, TokenScope};
|
||||
use crate::error::{ApiResult, AppError};
|
||||
use crate::model::auth::AuthResponse;
|
||||
use crate::svc::auth_svc::AuthService;
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
use rocket::State;
|
||||
use totp_rs::{Algorithm, TOTP};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct TOTPSixDigitCode {
|
||||
code: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, sqlx::Type, Clone, Copy, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[sqlx(type_name = "totp_status", rename_all = "lowercase")]
|
||||
pub enum TotpStatus {
|
||||
Enabled,
|
||||
Pending,
|
||||
Disabled,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct QrResponse {
|
||||
qr_code: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct TotpVerifyRequest {
|
||||
pub code: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct PasswordConfirmation {
|
||||
password: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct PasswordAnd2fa {
|
||||
pub password: String,
|
||||
pub totp_code: String,
|
||||
}
|
||||
|
||||
pub fn totp_gen(user_id: i64, secret: &[u8]) -> ApiResult<TOTP> {
|
||||
TOTP::new(
|
||||
Algorithm::SHA1,
|
||||
6,
|
||||
1,
|
||||
30,
|
||||
secret.to_owned(),
|
||||
Some("chat.zxq5.dev".to_string()),
|
||||
format!("{}", user_id),
|
||||
)
|
||||
.map_err(|_| AppError::internal("failed to generate totp"))
|
||||
}
|
||||
|
||||
#[post("/totp", data = "<form>")]
|
||||
pub async fn confirm_totp(
|
||||
user: Session,
|
||||
form: Json<TOTPSixDigitCode>,
|
||||
svc: &State<AuthService>,
|
||||
) -> ApiResult<()> {
|
||||
svc.confirm_totp(user.uid, &form.code).await
|
||||
}
|
||||
|
||||
#[post("/totp.jpg", data = "<form>")]
|
||||
pub async fn get_totp(
|
||||
user: Session,
|
||||
form: Json<PasswordConfirmation>,
|
||||
svc: &State<AuthService>,
|
||||
) -> ApiResult<Json<QrResponse>> {
|
||||
let secret = svc.get_or_create_totp_secret(user.uid, &form.password).await?;
|
||||
|
||||
let qr_b64 = totp_gen(user.uid, secret.as_bytes())
|
||||
.map_err(|_| AppError::internal("invalid totp secret"))?
|
||||
.get_qr_base64()
|
||||
.map_err(|_| AppError::internal("failed to generate qr code"))?;
|
||||
|
||||
Ok(Json(QrResponse {
|
||||
qr_code: format!("data:image/png;base64,{}", qr_b64),
|
||||
}))
|
||||
}
|
||||
|
||||
#[get("/totp/status")]
|
||||
pub async fn get_totp_status(
|
||||
user: Session,
|
||||
svc: &State<AuthService>,
|
||||
) -> ApiResult<Json<TotpStatus>> {
|
||||
Ok(Json(
|
||||
svc.get_totp_status(user.uid).await?
|
||||
.then_some(TotpStatus::Enabled)
|
||||
.unwrap_or(TotpStatus::Disabled),
|
||||
))
|
||||
}
|
||||
|
||||
#[delete("/totp", data = "<form>")]
|
||||
pub async fn disable_totp(
|
||||
user: Session,
|
||||
form: Json<PasswordAnd2fa>,
|
||||
svc: &State<AuthService>,
|
||||
) -> ApiResult<Json<AuthResponse>> {
|
||||
let response = svc.disable_totp(user.uid, &form.password, &form.totp_code).await?;
|
||||
Ok(Json(response))
|
||||
}
|
||||
|
||||
#[post("/totp/verify", data = "<body>")]
|
||||
pub async fn verify_totp(
|
||||
claims: Claims,
|
||||
body: Json<TotpVerifyRequest>,
|
||||
svc: &State<AuthService>,
|
||||
) -> ApiResult<Json<AuthResponse>> {
|
||||
// reject if they somehow got here with a full token
|
||||
if claims.scope != TokenScope::TotpPending {
|
||||
return Err(AppError::Forbidden);
|
||||
}
|
||||
|
||||
let response = svc.login_totp(claims.sub as i64, &body.code).await?;
|
||||
Ok(Json(response))
|
||||
}
|
||||
Reference in New Issue
Block a user