use crate::auth::domain::otp::{OtpCacheRecord, OtpRepository}; use crate::auth::domain::AuthService; use crate::auth::domain::types::{ AuthTokens, AuthUserDetail, LoginInput, LoginOutput, NewPasswordInput, RefreshTokenInput, RegisterInput, ResendOtpInput, VerifyEmailInput, }; use crate::roles::domain::RoleRepository; use crate::users::domain::{UserEntity, UserRepository}; use async_trait::async_trait; use imphnen_email::send_email; use imphnen_entities::{RolesDetailQueryDto, users::UserProfileExtensionDto}; use imphnen_libs::{ decode_access_token, decode_refresh_token, encode_access_token, encode_refresh_token, encode_reset_password_token, environment, hash_password, verify_password, }; use imphnen_utils::generate_otp::OtpManager; use imphnen_utils::{AppError, get_iso_date}; use std::sync::Arc; use tracing::error; use uuid::Uuid; pub struct AuthServiceImpl { user_repo: Arc, role_repo: Arc, otp_repo: Arc, } impl AuthServiceImpl { pub fn new( user_repo: Arc, role_repo: Arc, otp_repo: Arc, ) -> Self { Self { user_repo, role_repo, otp_repo, } } } fn entity_to_user_detail(user: UserEntity) -> AuthUserDetail { AuthUserDetail { id: user.id, email: user.email, fullname: user.fullname, legal_name: user.legal_name, avatar: user.avatar, is_active: user.is_active, role: user.role, profile_extension: user.profile_extension, created_at: user.created_at, updated_at: user.updated_at, } } #[async_trait] impl AuthService for AuthServiceImpl { async fn login(&self, payload: LoginInput) -> Result { let user = self .user_repo .find_by_email(payload.email.clone()) .await .map_err(|_| { AppError::AuthenticationError("Email or password not correct".into()) })?; if !user.is_active { return Err(AppError::AuthenticationError( "Account not active, please verify your email".into(), )); } let valid = verify_password(&payload.password, &user.password).map_err(|_| { AppError::InternalServerError("Password verification failed".into()) })?; if !valid { return Err(AppError::AuthenticationError( "Email or password not correct".into(), )); } let access_token = encode_access_token(payload.email.clone(), user.id.clone()) .map_err(|_| { AppError::InternalServerError("Failed to generate access token".into()) })?; let refresh_token = encode_refresh_token(payload.email.clone(), user.id.clone()) .map_err(|_| { AppError::InternalServerError("Failed to generate refresh token".into()) })?; Ok(LoginOutput { user: entity_to_user_detail(user), token: AuthTokens { access_token, refresh_token, }, }) } async fn login_mentor( &self, payload: LoginInput, ) -> Result { let user = self .user_repo .find_by_email(payload.email.clone()) .await .map_err(|_| { AppError::AuthenticationError("Email or password not correct".into()) })?; if !user.is_active { return Err(AppError::AuthenticationError( "Account not active, please verify your email".into(), )); } let valid = verify_password(&payload.password, &user.password).map_err(|_| { AppError::InternalServerError("Password verification failed".into()) })?; if !valid { return Err(AppError::AuthenticationError( "Email or password not correct".into(), )); } if user.role.name != "Mentor" { return Err(AppError::ForbiddenError( "User does not have mentor privileges".into(), )); } let access_token = encode_access_token(payload.email.clone(), user.id.clone()) .map_err(|_| { AppError::InternalServerError("Failed to generate access token".into()) })?; let refresh_token = encode_refresh_token(payload.email.clone(), user.id.clone()) .map_err(|_| { AppError::InternalServerError("Failed to generate refresh token".into()) })?; Ok(LoginOutput { user: entity_to_user_detail(user), token: AuthTokens { access_token, refresh_token, }, }) } async fn register(&self, payload: RegisterInput) -> Result<(), AppError> { let role = self .role_repo .find_by_name("User".into()) .await .map_err(|_| AppError::NotFoundError("Role not found".into()))?; if self .user_repo .find_by_email(payload.email.clone()) .await .is_ok() { return Err(AppError::BadRequestError("User already exists".into())); } let hashed = hash_password(&payload.password).map_err(|e| { error!("Failed to hash password: {}", e); AppError::InternalServerError("Failed to hash password".into()) })?; let otp = OtpManager::generate_otp(); send_email( &payload.email, "OTP Verification", &format!("your otp code is {}", otp.code), ) .map_err(|e| AppError::InternalServerError(e.to_string()))?; self .user_repo .create(UserEntity { id: Uuid::new_v4().to_string(), email: payload.email.clone(), fullname: payload.fullname.clone(), password: hashed, is_active: false, is_deleted: false, role: RolesDetailQueryDto { id: role.id.to_string(), name: role.name, ..Default::default() }, profile_extension: Some(UserProfileExtensionDto { phone_number: payload.phone_number, ..Default::default() }), created_at: get_iso_date(), updated_at: get_iso_date(), ..Default::default() }) .await?; // Persist OTP only after user creation succeeded, so a failed // registration leaves no orphan OTP record behind. self .otp_repo .save(OtpCacheRecord { email: payload.email.clone(), otp_hash: otp.hash.clone(), expires_at: otp.expires_at, }) .await?; Ok(()) } async fn resend_otp(&self, payload: ResendOtpInput) -> Result<(), AppError> { self .user_repo .find_by_email(payload.email.clone()) .await .map_err(|_| AppError::NotFoundError("User not found".into()))?; let otp = OtpManager::generate_otp(); send_email( &payload.email, "OTP Verification", &format!("Your OTP code is {}", otp.code), ) .map_err(|e| AppError::BadRequestError(e.to_string()))?; // Overwrite stored OTP only after the email was actually sent, so a // failed resend never invalidates the previous (still valid) code. self .otp_repo .save(OtpCacheRecord { email: payload.email.clone(), otp_hash: otp.hash.clone(), expires_at: otp.expires_at, }) .await?; Ok(()) } async fn refresh_token( &self, payload: RefreshTokenInput, ) -> Result { let email = decode_refresh_token(&payload.refresh_token) .map_err(|_| AppError::AuthenticationError("Invalid refresh token".into()))? .claims .sub; let user = self .user_repo .find_by_email(email.clone()) .await .map_err(|_| AppError::AuthenticationError("User not found".into()))?; let access_token = encode_access_token(user.email.clone(), user.id.clone()) .map_err(|_| { AppError::InternalServerError("Failed to generate access token".into()) })?; let refresh_token = encode_refresh_token(user.email.clone(), user.id.clone()) .map_err(|_| { AppError::InternalServerError("Failed to generate refresh token".into()) })?; Ok(AuthTokens { access_token, refresh_token, }) } async fn forgot_password(&self, payload: ResendOtpInput) -> Result<(), AppError> { let user_repo = Arc::clone(&self.user_repo); tokio::spawn(async move { if let Ok(user) = user_repo.find_by_email(payload.email.clone()).await { match encode_reset_password_token(user.email.clone(), user.id.clone()) { Ok(token) => { let fe_url = environment::ENV.fe_url.clone(); let msg = format!( "You have requested a password reset. Please click the link below: {fe_url}/auth/reset-password?token={token}" ); if let Err(e) = send_email(&payload.email, "Reset Password Request", &msg) { error!("Failed to send reset password email: {}", e); } } Err(e) => error!("Failed to generate reset token: {:?}", e), } } }); Ok(()) } async fn verify_email(&self, payload: VerifyEmailInput) -> Result<(), AppError> { let user = self .user_repo .find_by_email(payload.email.clone()) .await .map_err(|_| AppError::NotFoundError("User not found".into()))?; if user.is_active { return Err(AppError::BadRequestError("User already active".into())); } let stored = self .otp_repo .find_by_email(&payload.email) .await .map_err(|_| { AppError::BadRequestError( "No OTP issued for this email, request a new code".into(), ) })?; if !OtpManager::validate_otp_hash( &stored.otp_hash, &stored.expires_at, payload.otp, ) { return Err(AppError::BadRequestError( "Invalid or expired OTP".into(), )); } self .user_repo .update(UserEntity { is_active: true, ..user }) .await?; // OTP is single-use — consume it on successful verification. self.otp_repo.delete_by_email(&payload.email).await?; Ok(()) } async fn new_password(&self, payload: NewPasswordInput) -> Result<(), AppError> { let email = decode_access_token(&payload.token) .map_err(|_| AppError::BadRequestError("Invalid or missing token".into()))? .claims .sub; let user = self .user_repo .find_by_email(email) .await .map_err(|e| AppError::BadRequestError(e.to_string()))?; let hashed = hash_password(&payload.password).map_err(|e| { error!("Failed to hash new password: {}", e); AppError::InternalServerError("Failed to hash password".into()) })?; self .user_repo .update(UserEntity { password: hashed, ..user }) .await?; Ok(()) } }