use fnd::totp::TOTP;
use serde::{Deserialize, Serialize};
use crate::{
fnd::authn::{Authn, CaptchaPair, TotpData, UserHandle, password::Password},
prelude::*,
};
#[allow(non_camel_case_types)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(tag = "signup_type", content = "signup_data")]
#[allow(clippy::exhaustive_enums)]
pub enum SignupArgs {
username {
username: String,
password: String,
captcha: CaptchaPair,
},
phone {
username: String,
password: String,
phone: String,
totp_token: String,
},
email {
username: String,
password: String,
email: String,
totp_token: String,
},
}
#[derive(Debug, Default, Serialize, Deserialize)]
#[allow(clippy::exhaustive_structs)]
pub struct SignupUserInfo {
pub username: String,
pub email: Option<String>,
pub phone: Option<String>,
}
impl SignupArgs {
pub fn into_user_info(self) -> SignupUserInfo {
let mut info = SignupUserInfo::default();
match self {
SignupArgs::username { username, .. } => info.username = username,
SignupArgs::phone { username, phone, .. } => {
info.username = username;
info.phone = Some(phone)
}
SignupArgs::email { username, email, .. } => {
info.username = username;
info.email = Some(email)
}
}
info
}
}
#[derive(Debug, Serialize, Deserialize)]
#[allow(clippy::exhaustive_structs)]
pub struct SignupReply {
pub password_hash: String,
pub otp_secret: String,
pub otp_qr: String,
}
impl<U> Authn<U>
where
U: UserHandle,
{
#[inline(always)]
pub async fn signup_totp_token(&self, args: TotpData<()>) -> anyhow::Result<String> {
self.obtain_pub_totp_token(args).await
}
pub async fn check_sign_up(&self, args: SignupArgs) -> Result<SignupReply, ApiError> {
match args {
SignupArgs::username {
username,
password,
captcha,
} => {
self.verify_captcha(&captcha).await?;
self.new_password_and_secret(username, password)
}
SignupArgs::phone {
username,
password,
phone: _phone,
totp_token,
} => {
let matched = self.verify_pub_totp_token(TotpData::sms(totp_token.as_str())).await?;
if !matched {
return Err(api_err!(ET_VERIF_CODE, "OTP token is not matched.", &ERR_PATH_AUTHN));
}
self.new_password_and_secret(username, password)
}
SignupArgs::email {
username,
password,
email: _email,
totp_token,
} => {
let matched = self.verify_pub_totp_token(TotpData::email(totp_token.as_str())).await?;
if !matched {
return Err(api_err!(ET_VERIF_CODE, "OTP token is not matched.", &ERR_PATH_AUTHN));
}
self.new_password_and_secret(username, password)
}
}
}
fn new_password_and_secret(&self, username: String, password: String) -> Result<SignupReply, ApiError> {
let password_hash = Password(password)
.password_hash()
.map_err(|err| api_err!(ET_PWD_VERIF_FAIL, &ERR_PATH_AUTHN).with_source(err.into_error(), true))?;
let otp_secret = TOTP::gen_secret_base32();
let totp = TOTP::default_otp();
let otp_qr = totp
.qr_base64(otp_secret.clone(), self.app_name().clone(), username)
.map_err(|_| api_err!(ET_SYS_ERR, "Failed to generate OTP QR code.", &ERR_PATH_AUTHN))?;
Ok(SignupReply {
password_hash,
otp_secret,
otp_qr,
})
}
}