dipper 0.5.3

An out-of-the-box modular dependency injection web application framework.
Documentation
use std::fmt::Debug;

use fnd::identifier::Uid;
use serde::{Deserialize, Serialize};

use super::{Authn, JwtTokenPair, SigninArgs, SignupArgs, UserHandle, UserIdentifier};
use crate::prelude::*;

#[derive(Clone)]
#[non_exhaustive]
pub struct DefaultAuthn<U: UserAuth> {
    authn: Authn<U>,
}

impl<U: UserAuth> DefaultAuthn<U> {
    pub const fn new(authn: Authn<U>) -> Self {
        Self { authn }
    }
}

#[async_trait]
pub trait UserAuth: UserHandle + Clone {
    type User: User;
    type SignupRequestBody: SignupRequest;
    async fn create_user(
        &self,
        depot: &mut Depot,
        user_info: Self::SignupRequestBody,
        password_hash: String,
        otp_secret: String,
    ) -> Result<Self::User, ApiError>;
    async fn get_user(&self, depot: &mut Depot, user_ident: UserIdentifier) -> Result<Self::User, ApiError>;
}

pub trait User: Send + Debug + Clone + for<'d> Deserialize<'d> + Serialize + ToSchema {
    fn user_id(&self) -> Uid;
}

pub trait SignupRequest: Send + Debug + Clone + for<'d> Deserialize<'d> + ToSchema {
    fn signup_args(&self) -> SignupArgs;
}

#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct SignupResponse<T: User> {
    jwt: JwtTokenPair,
    user: T,
    otp_qr: String,
}

#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct SigninResponse<T: User> {
    jwt: JwtTokenPair,
    user: T,
}

#[dipper]
impl<U: Debug> DefaultAuthn<U>
where
    U: UserAuth,
{
    /// Register a new user.
    // #[dipper(endpoint)]
    #[dipper(handler)]
    pub async fn sign_up_goal(
        &self,
        depot: &mut Depot,
        user_info: JsonBody<U::SignupRequestBody>,
        res: &mut Response,
    ) -> ApiResponse<SignupResponse<<U as UserAuth>::User>> {
        let args = user_info.signup_args();
        let reply = self.authn.check_sign_up(args).await?;
        let user = self
            .authn
            .user_handle()
            .create_user(depot, user_info.into_inner(), reply.password_hash, reply.otp_secret)
            .await?;
        let jwt = self.authn.new_jwt_token_pair(user.user_id().clone(), None).await?;
        self.authn.set_jwt_auth_cookie(jwt.auth_token.clone(), res);
        SignupResponse {
            jwt,
            user,
            otp_qr: reply.otp_qr,
        }
        .api_response_without_meta()
    }

    /// User login.
    // #[dipper(endpoint)]
    #[dipper(handler)]
    pub async fn sign_in_goal(
        &self,
        depot: &mut Depot,
        args: JsonBody<SigninArgs>,
        res: &mut Response,
    ) -> ApiResponse<SigninResponse<<U as UserAuth>::User>> {
        let user_identifier = args.user_identifier();
        let jwt = self.authn.check_sign_in(depot, args.into_inner()).await?;
        let user = self.authn.user_handle().get_user(depot, user_identifier).await?;
        self.authn.set_jwt_auth_cookie(jwt.auth_token.clone(), res);
        SigninResponse { jwt, user }.api_response_without_meta()
    }

    /// Authentication SDK.
    pub const fn inner_ref(&self) -> &Authn<U> {
        &self.authn
    }

    /// Obtain the user's TOTP QR code.
    pub fn get_user_totp_qr_goal(&self) -> impl Handler {
        self.authn.get_user_totp_qr_goal()
    }

    /// Refresh the user's TOTP QR code.
    pub fn refresh_user_totp_qr_goal(&self) -> impl Handler {
        self.authn.refresh_user_totp_qr_goal()
    }

    /// Hoop for checking login.
    pub fn jwt_verifying_hoop(&self) -> impl Handler {
        self.authn.jwt_verifying_hoop()
    }

    /// Refresh JWT authorization.
    /// NOTE: Require the `jwt_verifying_hoop` hoop.
    // #[dipper(endpoint)]
    #[dipper(handler)]
    pub async fn refresh_jwt_token_goal(&self, depot: &Depot, res: &mut Response) -> ApiResponse<JwtTokenPair> {
        let Some(token_data) = self.authn.obtain_jwt_claims(depot) else {
            return api_err!(
                ET_ACCESS_AUTHZ,
                "There is a problem with the authentication hoop.",
                &ERR_PATH_AUTHN
            )
            .api_response_without_meta();
        };
        let jwt = self
            .authn
            .new_jwt_token_pair(token_data.claims.uid.clone(), None)
            .await?;
        self.authn.set_jwt_auth_cookie(jwt.auth_token.clone(), res);
        jwt.api_response_without_meta()
    }

    /// Reset the user's password.
    /// NOTE: The `jwt_verifying_hoop` hoop should not exist.
    #[dipper(handler)]
    pub async fn reset_user_password_goal(
        &self,
        depot: &mut Depot,
        args: JsonBody<ResetPasswordArgs>,
    ) -> ApiResponse<()> {
        let args = args.into_inner();
        let user_identifier = args.sign.user_identifier();
        let _ = self.authn.check_sign_in(depot, args.sign).await?;
        self.authn
            .reset_user_password(depot, &user_identifier, args.new_password)
            .await
            .into()
    }

    /// Judge whether the JWT authentication is valid and obtain user
    /// information. NOTE: Require the `jwt_verifying_hoop` hoop.
    // #[dipper(endpoint)]
    #[dipper(handler)]
    pub async fn get_user_goal(&self, depot: &mut Depot) -> ApiResponse<<U as UserAuth>::User> {
        let Some(token_data) = self.authn.obtain_jwt_claims(depot) else {
            return api_err!(
                ET_ACCESS_AUTHZ,
                "There is a problem with the authentication hoop.",
                &ERR_PATH_AUTHN
            )
            .api_response_without_meta();
        };
        let user = self
            .authn
            .user_handle()
            .get_user(depot, UserIdentifier::id(token_data.claims.uid.clone()))
            .await?;
        user.api_response_without_meta()
    }
}

impl SigninArgs {
    pub fn user_identifier(&self) -> UserIdentifier {
        match self {
            SigninArgs::username { username, .. } => UserIdentifier::username(username.clone()),
            SigninArgs::email { email, .. } => UserIdentifier::email(email.clone()),
            SigninArgs::phone { phone, .. } => UserIdentifier::phone(phone.clone()),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResetPasswordArgs {
    #[serde(flatten)]
    sign: SigninArgs,
    new_password: String,
}