hexagonal refactor
This commit is contained in:
+337
@@ -0,0 +1,337 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::domain::{DomainError, DomainResult, GroceryList, Item, SessionUser, User};
|
||||
use crate::ports::{
|
||||
CategoryRepository, InvitationRepository, ItemRepository, ListRepository, PasswordHasher,
|
||||
RealtimeNotifier, SessionRepository, TokenGenerator, UserRepository,
|
||||
};
|
||||
use crate::sqlite::SqliteDatabase;
|
||||
|
||||
pub struct AuthService {
|
||||
db: SqliteDatabase,
|
||||
users: Arc<dyn UserRepository>,
|
||||
sessions: Arc<dyn SessionRepository>,
|
||||
invitations: Arc<dyn InvitationRepository>,
|
||||
hasher: Arc<dyn PasswordHasher>,
|
||||
registration_mode: RegistrationMode,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum RegistrationMode {
|
||||
Open,
|
||||
InviteOnly,
|
||||
}
|
||||
|
||||
impl AuthService {
|
||||
pub fn new(
|
||||
db: SqliteDatabase,
|
||||
users: Arc<dyn UserRepository>,
|
||||
sessions: Arc<dyn SessionRepository>,
|
||||
invitations: Arc<dyn InvitationRepository>,
|
||||
hasher: Arc<dyn PasswordHasher>,
|
||||
registration_mode: RegistrationMode,
|
||||
) -> Self {
|
||||
Self {
|
||||
db,
|
||||
users,
|
||||
sessions,
|
||||
invitations,
|
||||
hasher,
|
||||
registration_mode,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn can_register(&self, invite: Option<&str>) -> DomainResult<bool> {
|
||||
if self.registration_mode == RegistrationMode::Open {
|
||||
return Ok(true);
|
||||
}
|
||||
let users = Arc::clone(&self.users);
|
||||
let invitations = Arc::clone(&self.invitations);
|
||||
let invite = invite.map(str::to_owned);
|
||||
self.db
|
||||
.run(move |txn| {
|
||||
Box::pin(async move {
|
||||
if !users.has_users(txn).await? {
|
||||
return Ok(true);
|
||||
}
|
||||
let Some(invite) = invite.filter(|invite| !invite.is_empty()) else {
|
||||
return Ok(false);
|
||||
};
|
||||
invitations.invitation(txn, invite).await
|
||||
})
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn register(
|
||||
&self,
|
||||
display_name: String,
|
||||
email: String,
|
||||
password: String,
|
||||
invite: Option<&str>,
|
||||
) -> DomainResult<(User, String)> {
|
||||
if !self.can_register(invite).await? {
|
||||
return Err(DomainError::Conflict);
|
||||
}
|
||||
let password_hash = self.hasher.hash(&password).map_err(DomainError::Database)?;
|
||||
let users = Arc::clone(&self.users);
|
||||
let sessions = Arc::clone(&self.sessions);
|
||||
self.db
|
||||
.run(move |txn| {
|
||||
Box::pin(async move {
|
||||
let user = users
|
||||
.create_user(txn, email, display_name, password_hash)
|
||||
.await?;
|
||||
let (session_token, _) = sessions.create_session(txn, user.id).await?;
|
||||
Ok((user, session_token))
|
||||
})
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn login(
|
||||
&self,
|
||||
email: String,
|
||||
password: String,
|
||||
) -> DomainResult<Option<(User, String)>> {
|
||||
let users = Arc::clone(&self.users);
|
||||
let sessions = Arc::clone(&self.sessions);
|
||||
let hasher = Arc::clone(&self.hasher);
|
||||
self.db
|
||||
.run(move |txn| {
|
||||
Box::pin(async move {
|
||||
let Some((user, password_hash)) = users.find_user_by_email(txn, email).await?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
let valid = hasher
|
||||
.verify(&password, &password_hash)
|
||||
.map_err(DomainError::Database)?;
|
||||
if !valid {
|
||||
return Ok(None);
|
||||
}
|
||||
let (session_token, _) = sessions.create_session(txn, user.id).await?;
|
||||
Ok(Some((user, session_token)))
|
||||
})
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn session_user(&self, session_token: String) -> DomainResult<Option<SessionUser>> {
|
||||
let sessions = Arc::clone(&self.sessions);
|
||||
self.db
|
||||
.run(move |txn| {
|
||||
Box::pin(async move { sessions.session_user(txn, session_token).await })
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn logout(&self, session_token: String) -> DomainResult<()> {
|
||||
let sessions = Arc::clone(&self.sessions);
|
||||
self.db
|
||||
.run(move |txn| {
|
||||
Box::pin(async move { sessions.delete_session(txn, session_token).await })
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ListService {
|
||||
db: SqliteDatabase,
|
||||
lists: Arc<dyn ListRepository>,
|
||||
categories: Arc<dyn CategoryRepository>,
|
||||
items: Arc<dyn ItemRepository>,
|
||||
realtime: Arc<dyn RealtimeNotifier>,
|
||||
}
|
||||
|
||||
impl ListService {
|
||||
pub fn new(
|
||||
db: SqliteDatabase,
|
||||
lists: Arc<dyn ListRepository>,
|
||||
categories: Arc<dyn CategoryRepository>,
|
||||
items: Arc<dyn ItemRepository>,
|
||||
realtime: Arc<dyn RealtimeNotifier>,
|
||||
) -> Self {
|
||||
Self {
|
||||
db,
|
||||
lists,
|
||||
categories,
|
||||
items,
|
||||
realtime,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list_summaries(&self) -> DomainResult<Vec<GroceryList>> {
|
||||
let lists = Arc::clone(&self.lists);
|
||||
self.db
|
||||
.run(move |txn| Box::pin(async move { lists.list_summaries(txn).await }))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn create_list(&self, name: String) -> DomainResult<GroceryList> {
|
||||
let lists = Arc::clone(&self.lists);
|
||||
self.db
|
||||
.run(move |txn| Box::pin(async move { lists.create_list(txn, name).await }))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_list(&self, list_id: i64) -> DomainResult<Option<GroceryList>> {
|
||||
let lists = Arc::clone(&self.lists);
|
||||
self.db
|
||||
.run(move |txn| Box::pin(async move { lists.get_list(txn, list_id).await }))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn items(&self, list_id: i64) -> DomainResult<Vec<Item>> {
|
||||
let items = Arc::clone(&self.items);
|
||||
self.db
|
||||
.run(move |txn| Box::pin(async move { items.items(txn, list_id).await }))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn categories(&self, list_id: i64) -> DomainResult<Vec<crate::domain::Category>> {
|
||||
let categories = Arc::clone(&self.categories);
|
||||
self.db
|
||||
.run(move |txn| Box::pin(async move { categories.categories(txn, list_id).await }))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn add_item(
|
||||
&self,
|
||||
list_id: i64,
|
||||
name: String,
|
||||
quantity: String,
|
||||
note: String,
|
||||
category_id: Option<i64>,
|
||||
) -> DomainResult<i64> {
|
||||
let items = Arc::clone(&self.items);
|
||||
let revision = self
|
||||
.db
|
||||
.run(move |txn| {
|
||||
Box::pin(async move {
|
||||
items
|
||||
.add_item(txn, list_id, name, quantity, note, category_id)
|
||||
.await
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
self.realtime.publish_list_changed(list_id, revision).await;
|
||||
Ok(revision)
|
||||
}
|
||||
|
||||
pub async fn set_item_checked(
|
||||
&self,
|
||||
list_id: i64,
|
||||
item_id: i64,
|
||||
checked: bool,
|
||||
) -> DomainResult<i64> {
|
||||
let items = Arc::clone(&self.items);
|
||||
let revision = self
|
||||
.db
|
||||
.run(move |txn| {
|
||||
Box::pin(
|
||||
async move { items.set_item_checked(txn, list_id, item_id, checked).await },
|
||||
)
|
||||
})
|
||||
.await?;
|
||||
self.realtime.publish_list_changed(list_id, revision).await;
|
||||
Ok(revision)
|
||||
}
|
||||
|
||||
pub async fn update_item(
|
||||
&self,
|
||||
list_id: i64,
|
||||
item_id: i64,
|
||||
name: String,
|
||||
quantity: String,
|
||||
note: String,
|
||||
category_id: Option<i64>,
|
||||
) -> DomainResult<i64> {
|
||||
let items = Arc::clone(&self.items);
|
||||
let revision = self
|
||||
.db
|
||||
.run(move |txn| {
|
||||
Box::pin(async move {
|
||||
items
|
||||
.update_item(txn, list_id, item_id, name, quantity, note, category_id)
|
||||
.await
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
self.realtime.publish_list_changed(list_id, revision).await;
|
||||
Ok(revision)
|
||||
}
|
||||
|
||||
pub async fn delete_item(&self, list_id: i64, item_id: i64) -> DomainResult<i64> {
|
||||
let items = Arc::clone(&self.items);
|
||||
let revision = self
|
||||
.db
|
||||
.run(move |txn| Box::pin(async move { items.delete_item(txn, list_id, item_id).await }))
|
||||
.await?;
|
||||
self.realtime.publish_list_changed(list_id, revision).await;
|
||||
Ok(revision)
|
||||
}
|
||||
|
||||
pub async fn create_category(&self, list_id: i64, name: String) -> DomainResult<i64> {
|
||||
let categories = Arc::clone(&self.categories);
|
||||
let revision = self
|
||||
.db
|
||||
.run(move |txn| {
|
||||
Box::pin(async move { categories.create_category(txn, list_id, name).await })
|
||||
})
|
||||
.await?;
|
||||
self.realtime.publish_list_changed(list_id, revision).await;
|
||||
Ok(revision)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InvitationService {
|
||||
db: SqliteDatabase,
|
||||
invitations: Arc<dyn InvitationRepository>,
|
||||
tokens: Arc<dyn TokenGenerator>,
|
||||
}
|
||||
|
||||
impl InvitationService {
|
||||
pub fn new(
|
||||
db: SqliteDatabase,
|
||||
invitations: Arc<dyn InvitationRepository>,
|
||||
tokens: Arc<dyn TokenGenerator>,
|
||||
) -> Self {
|
||||
Self {
|
||||
db,
|
||||
invitations,
|
||||
tokens,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_invitation(&self, created_by: i64) -> DomainResult<String> {
|
||||
let token = self.tokens.generate();
|
||||
let invitations = Arc::clone(&self.invitations);
|
||||
self.db
|
||||
.run(move |txn| {
|
||||
Box::pin(async move {
|
||||
invitations
|
||||
.create_invitation(txn, created_by, token.clone())
|
||||
.await?;
|
||||
Ok(token)
|
||||
})
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn invitation(&self, token: String) -> DomainResult<bool> {
|
||||
let invitations = Arc::clone(&self.invitations);
|
||||
self.db
|
||||
.run(move |txn| Box::pin(async move { invitations.invitation(txn, token).await }))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn accept_invitation(&self, token: String) -> DomainResult<()> {
|
||||
let invitations = Arc::clone(&self.invitations);
|
||||
self.db
|
||||
.run(move |txn| {
|
||||
Box::pin(async move { invitations.accept_invitation(txn, token).await })
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user