Initial Commit
This commit is contained in:
193
src/lib/domain/api/service.rs
Normal file
193
src/lib/domain/api/service.rs
Normal file
@@ -0,0 +1,193 @@
|
||||
/*!
|
||||
Module `service` provides the canonical implementation of the [ApiService] port. All
|
||||
user-domain logic is defined here.
|
||||
*/
|
||||
use axum_session::SessionAnySession;
|
||||
|
||||
use super::models::user::*;
|
||||
|
||||
use super::ports::{ApiService, UserNotifier, UserRepository};
|
||||
|
||||
pub trait Repository = UserRepository;
|
||||
pub trait Email = UserNotifier;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Service<R, N>
|
||||
where
|
||||
R: Repository,
|
||||
N: Email,
|
||||
{
|
||||
repo: R,
|
||||
notifier: N,
|
||||
}
|
||||
|
||||
impl<R, N> Service<R, N>
|
||||
where
|
||||
R: Repository,
|
||||
N: Email,
|
||||
{
|
||||
pub fn new(repo: R, notifier: N) -> Self {
|
||||
Self { repo, notifier }
|
||||
}
|
||||
}
|
||||
|
||||
impl<R, N> ApiService for Service<R, N>
|
||||
where
|
||||
R: UserRepository,
|
||||
N: Email,
|
||||
{
|
||||
async fn create_user(&self, req: CreateUserRequest) -> Result<User, CreateUserError> {
|
||||
let result = self.repo.create_user(req).await;
|
||||
|
||||
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 Some(user_id) = session.get("user") else {
|
||||
return None;
|
||||
};
|
||||
|
||||
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.into())),
|
||||
};
|
||||
|
||||
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.into()));
|
||||
}
|
||||
};
|
||||
|
||||
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.into()));
|
||||
}
|
||||
};
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user