/*! Module `service` provides the canonical implementation of the [ApiService] port. All user-domain logic is defined here. */ use axum_session::SessionAnySession; use crate::inbound::http::handlers::oauth::AuthorizationCodeRequest; use crate::inbound::http::handlers::oauth::GrantType; use super::models::oauth::Client; use super::models::oauth::*; use super::models::user::*; use super::ports::{ApiService, OAuthRepository, UserNotifier, UserRepository}; // pub trait Repository = UserRepository + OAuthRepository; pub trait Email = UserNotifier; #[derive(Debug, Clone)] pub struct Service where R: UserRepository + OAuthRepository, N: Email, { repo: R, notifier: N, } impl Service where R: UserRepository + OAuthRepository, N: Email, { pub fn new(repo: R, notifier: N) -> Self { Self { repo, notifier } } } impl ApiService for Service where R: UserRepository + OAuthRepository, N: Email, { async fn create_user(&self, req: CreateUserRequest) -> Result { let result = self.repo.create_user(req).await; #[allow(clippy::question_mark)] if result.is_err() { // something went wrong, log the error // but keep passing on the result to the requester (http server) return result; } let user = result.as_ref().unwrap(); let token = self.repo.create_activation_token(user).await?; // generate a activation token and send an email self.notifier.user_created(user, &token).await; result } async fn get_user_session(&self, session: &SessionAnySession) -> Option { let user_id = session.get("user")?; self.repo.find_user_by_id(user_id).await.unwrap_or(None) } async fn activate_user_account( &self, token: ActivationToken, ) -> Result { let user = match self.repo.find_user_by_activation_token(&token).await { Ok(u) => u, Err(e) => return Err(ActivateUserError::Unknown(e)), }; let Some(user) = user else { return Err(ActivateUserError::NotFound { token }); }; let req = UpdateUserRequest::new().set_verified(Verified::new(true)); let update = self.repo.update_user(&user, req).await; if let Err(e) = update { return Err(ActivateUserError::Unknown(e.into())); } self.repo.delete_activation_token_for_user(&user).await?; let (_, user) = update.unwrap(); Ok(user) } async fn user_login(&self, req: UserLoginRequest) -> Result { let email = req.email(); let user = match self.repo.find_user_by_email(email).await { Ok(u) => u, Err(e) => { tracing::error!("{:#?}", e); return Err(UserLoginError::Unknown(e)); } }; let Some(user) = user else { tracing::warn!("User not found"); return Err(UserLoginError::NotFound { email: email.clone(), }); }; if !user.verified().is_verified() { return Err(UserLoginError::NotActive { email: email.clone(), }); } if let Err(e) = user.password().verify(req.password()) { tracing::warn!("{:#?}", e); return Err(UserLoginError::NotFound { email: email.clone(), }); } // Invalidate any reset tokens that might exist let _ = self.repo.delete_password_reset_tokens_for_user(&user).await; Ok(user) } async fn forgot_password(&self, email: &EmailAddress) { let user = match self.repo.find_user_by_email(email).await { Ok(u) => u, Err(e) => { tracing::error!("{:#?}", e); return; } }; let Some(user) = user else { tracing::warn!("User not found"); return; }; let token = match self.repo.create_password_reset_token(&user).await { Ok(t) => t, Err(e) => { tracing::error!("{:#?}", e); return; } }; self.notifier.forgot_password(&user, &token).await; } async fn reset_password( &self, token: &PasswordResetToken, password: &Password, ) -> Result { let user = match self.repo.find_user_by_password_reset_token(token).await { Ok(u) => u, Err(e) => { tracing::error!("{:#?}", e); return Err(ResetPasswordError::Unknown(e)); } }; let Some(user) = user else { tracing::warn!("User not found"); return Err(ResetPasswordError::NotFound); }; let req = UpdateUserRequest::new().set_password(password.clone()); self.repo .update_user(&user, req) .await .map_err(|e| ResetPasswordError::Unknown(e.into()))?; // Invalidate any reset tokens that might exist let _ = self.repo.delete_password_reset_tokens_for_user(&user).await; Ok(user) } async fn find_user_by_password_reset_token(&self, token: &PasswordResetToken) -> Option { self.repo .find_user_by_password_reset_token(token) .await .unwrap_or(None) } async fn find_client_by_id(&self, id: uuid::Uuid) -> Option { self.repo.find_client_by_id(id).await.ok().flatten() } async fn generate_authorization_code( &self, user: &User, req: AuthorizeRequest, ) -> Result { let Some(client) = self.repo.find_client_by_id(req.client_id()).await? else { return Err(anyhow::anyhow!("Client not found")); }; if client.redirect_uri() != &req.redirect_uri() { return Err(anyhow::anyhow!("Invalid redirect uri")); } let code = self .repo .create_authorization_code( user.id(), client.id(), req.code_challenge(), req.code_challenge_method().unwrap_or_default(), ) .await?; Ok(AuthorizationResponse::new(code, req.state())) } async fn create_token( &self, req: AuthorizationCodeRequest, ) -> Result, TokenError> { if req.grant_type() != GrantType::AuthorizationCode { return Err(TokenError::InvalidRequest); } let code = req.code(); let Some(token) = self.repo.get_token_subject(code).await? else { return Err(TokenError::InvalidRequest); }; let code_verifier = req.code_verifier(); let code_challenge = match token.code_challenge_method() { CodeChallengeMethod::Plain => { use base64::prelude::*; BASE64_URL_SAFE_NO_PAD.encode(code_verifier.to_string()) } CodeChallengeMethod::Sha256 => { use base64::prelude::*; BASE64_URL_SAFE_NO_PAD.encode(sha256::digest(code_verifier.to_string())) } }; if token.code_challenge() != code_challenge { return Err(TokenError::InvalidRequest); } let Some(client) = self.repo.find_client_by_id(token.client_id()).await? else { return Err(TokenError::InvalidRequest); // no such client }; if &req.redirect_uri() != client.redirect_uri() { return Err(TokenError::InvalidRequest); // invalid redirect uri } let _ = self.repo.delete_token(req.code()).await; Ok(Some(token)) } }