271 lines
7.8 KiB
Rust
271 lines
7.8 KiB
Rust
/*!
|
|
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<R, N>
|
|
where
|
|
R: UserRepository + OAuthRepository,
|
|
N: Email,
|
|
{
|
|
repo: R,
|
|
notifier: N,
|
|
}
|
|
|
|
impl<R, N> Service<R, N>
|
|
where
|
|
R: UserRepository + OAuthRepository,
|
|
N: Email,
|
|
{
|
|
pub fn new(repo: R, notifier: N) -> Self {
|
|
Self { repo, notifier }
|
|
}
|
|
}
|
|
|
|
impl<R, N> ApiService for Service<R, N>
|
|
where
|
|
R: UserRepository + OAuthRepository,
|
|
N: Email,
|
|
{
|
|
async fn create_user(&self, req: CreateUserRequest) -> Result<User, CreateUserError> {
|
|
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<User> {
|
|
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<User, ActivateUserError> {
|
|
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<User, UserLoginError> {
|
|
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<User, ResetPasswordError> {
|
|
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<User> {
|
|
self.repo
|
|
.find_user_by_password_reset_token(token)
|
|
.await
|
|
.unwrap_or(None)
|
|
}
|
|
|
|
async fn find_client_by_id(&self, id: uuid::Uuid) -> Option<Client> {
|
|
self.repo.find_client_by_id(id).await.ok().flatten()
|
|
}
|
|
|
|
async fn generate_authorization_code(
|
|
&self,
|
|
user: &User,
|
|
req: AuthorizeRequest,
|
|
) -> Result<AuthorizationResponse, anyhow::Error> {
|
|
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<Option<TokenSubject>, 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))
|
|
}
|
|
}
|