use std::sync::Arc; use crate::domain::{ DomainError, DomainResult, GroceryList, Item, ListMeal, Meal, MealCategory, RewardsCard, SessionUser, User, }; use crate::ports::{ CategoryRepository, InvitationRepository, ItemRepository, ListMealRepository, ListRepository, MealCategoryRepository, MealIngredientRepository, MealRepository, NewItem, PasswordHasher, RealtimeNotifier, RewardsCardRepository, SessionRepository, TokenGenerator, UserRepository, }; use crate::sqlite::SqliteDatabase; pub struct AuthService { db: SqliteDatabase, users: Arc, sessions: Arc, invitations: Arc, hasher: Arc, registration_mode: RegistrationMode, } #[derive(Clone, Copy, PartialEq, Eq)] pub enum RegistrationMode { Open, InviteOnly, } impl AuthService { pub fn new( db: SqliteDatabase, users: Arc, sessions: Arc, invitations: Arc, hasher: Arc, registration_mode: RegistrationMode, ) -> Self { Self { db, users, sessions, invitations, hasher, registration_mode, } } pub async fn can_register(&self, invite: Option<&str>) -> DomainResult { 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)?; 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> { 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)?; 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> { 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 find_user_by_email(&self, email: String) -> DomainResult> { let users = Arc::clone(&self.users); self.db .run(move |txn| Box::pin(async move { users.find_user_by_email(txn, email).await })) .await } pub async fn create_session_for_user(&self, user_id: i64) -> DomainResult<(String, String)> { let sessions = Arc::clone(&self.sessions); self.db .run(move |txn| Box::pin(async move { sessions.create_session(txn, user_id).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 } /// Replaces the user's password hash with a freshly hashed new password. /// No current-password check is performed because the account page is /// already authenticated and this app has no email capabilities. pub async fn change_password(&self, user_id: i64, new_password: String) -> DomainResult<()> { let users = Arc::clone(&self.users); let hasher = Arc::clone(&self.hasher); self.db .run(move |txn| { Box::pin(async move { let new_hash = hasher.hash(&new_password)?; users.update_password_hash(txn, user_id, new_hash).await }) }) .await } } pub struct ListService { db: SqliteDatabase, lists: Arc, categories: Arc, items: Arc, realtime: Arc, } impl ListService { pub fn new( db: SqliteDatabase, lists: Arc, categories: Arc, items: Arc, realtime: Arc, ) -> Self { Self { db, lists, categories, items, realtime, } } pub async fn list_summaries(&self) -> DomainResult> { let lists = Arc::clone(&self.lists); self.db .run(move |txn| Box::pin(async move { lists.list_summaries(txn).await })) .await } pub async fn list_archived_summaries(&self) -> DomainResult> { let lists = Arc::clone(&self.lists); self.db .run(move |txn| Box::pin(async move { lists.list_archived_summaries(txn).await })) .await } pub async fn create_list(&self, name: String) -> DomainResult { 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> { 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 archive_list(&self, list_id: i64) -> DomainResult<()> { let lists = Arc::clone(&self.lists); self.db .run(move |txn| Box::pin(async move { lists.set_archived(txn, list_id, true).await })) .await } pub async fn unarchive_list(&self, list_id: i64) -> DomainResult<()> { let lists = Arc::clone(&self.lists); self.db .run(move |txn| Box::pin(async move { lists.set_archived(txn, list_id, false).await })) .await } pub async fn items(&self, list_id: i64) -> DomainResult> { 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) -> DomainResult> { let categories = Arc::clone(&self.categories); self.db .run(move |txn| Box::pin(async move { categories.categories(txn).await })) .await } pub async fn add_item( &self, list_id: i64, name: String, quantity: String, note: String, category_id: Option, ) -> DomainResult { 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 { 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, ) -> DomainResult { 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 { 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, name: String) -> DomainResult { let categories = Arc::clone(&self.categories); self.db .run(move |txn| Box::pin(async move { categories.create_category(txn, name).await })) .await } } pub struct MealService { db: SqliteDatabase, meals: Arc, ingredients: Arc, meal_categories: Arc, lists: Arc, items: Arc, list_meals: Arc, realtime: Arc, } impl MealService { pub fn new( db: SqliteDatabase, meals: Arc, ingredients: Arc, meal_categories: Arc, lists: Arc, items: Arc, list_meals: Arc, realtime: Arc, ) -> Self { Self { db, meals, ingredients, meal_categories, lists, items, list_meals, realtime, } } pub async fn create_meal( &self, name: String, description: String, category_id: Option, ) -> DomainResult { let meals = Arc::clone(&self.meals); self.db .run(move |txn| { Box::pin( async move { meals.create_meal(txn, name, description, category_id).await }, ) }) .await } pub async fn get_meal(&self, meal_id: i64) -> DomainResult> { let meals = Arc::clone(&self.meals); self.db .run(move |txn| Box::pin(async move { meals.get_meal(txn, meal_id).await })) .await } pub async fn list_meals(&self, query: &str) -> DomainResult> { let meals = Arc::clone(&self.meals); let query = query.to_owned(); self.db .run(move |txn| Box::pin(async move { meals.list_meals(txn, &query).await })) .await } pub async fn update_meal( &self, meal_id: i64, name: String, description: String, category_id: Option, ) -> DomainResult<()> { let meals = Arc::clone(&self.meals); self.db .run(move |txn| { Box::pin(async move { meals .update_meal(txn, meal_id, name, description, category_id) .await }) }) .await } pub async fn list_meal_categories(&self) -> DomainResult> { let meal_categories = Arc::clone(&self.meal_categories); self.db .run(move |txn| Box::pin(async move { meal_categories.meal_categories(txn).await })) .await } pub async fn create_meal_category(&self, name: String) -> DomainResult { let meal_categories = Arc::clone(&self.meal_categories); self.db .run(move |txn| { Box::pin(async move { meal_categories.create_meal_category(txn, name).await }) }) .await } pub async fn delete_meal_category(&self, category_id: i64) -> DomainResult<()> { let meal_categories = Arc::clone(&self.meal_categories); self.db .run(move |txn| { Box::pin( async move { meal_categories.delete_meal_category(txn, category_id).await }, ) }) .await } pub async fn delete_meal(&self, meal_id: i64) -> DomainResult<()> { let meals = Arc::clone(&self.meals); self.db .run(move |txn| Box::pin(async move { meals.delete_meal(txn, meal_id).await })) .await } pub async fn add_ingredient( &self, meal_id: i64, name: String, quantity: String, note: String, category_id: Option, ) -> DomainResult { let ingredients = Arc::clone(&self.ingredients); self.db .run(move |txn| { Box::pin(async move { ingredients .add_ingredient(txn, meal_id, name, quantity, note, category_id) .await }) }) .await } pub async fn update_ingredient( &self, meal_id: i64, ingredient_id: i64, name: String, quantity: String, note: String, category_id: Option, ) -> DomainResult<()> { let ingredients = Arc::clone(&self.ingredients); self.db .run(move |txn| { Box::pin(async move { ingredients .update_ingredient( txn, meal_id, ingredient_id, name, quantity, note, category_id, ) .await }) }) .await } pub async fn delete_ingredient(&self, meal_id: i64, ingredient_id: i64) -> DomainResult<()> { let ingredients = Arc::clone(&self.ingredients); self.db .run(move |txn| { Box::pin(async move { ingredients .delete_ingredient(txn, meal_id, ingredient_id) .await }) }) .await } /// Expands a meal's ingredients into items on a list in one unit of work, /// recording the meal on the list and bumping the list revision exactly once. pub async fn add_meal_to_list(&self, meal_id: i64, list_id: i64) -> DomainResult { let meals = Arc::clone(&self.meals); let lists = Arc::clone(&self.lists); let items = Arc::clone(&self.items); let list_meals = Arc::clone(&self.list_meals); let revision = self .db .run(move |txn| { Box::pin(async move { let meal = meals .get_meal(txn, meal_id) .await? .ok_or(DomainError::NotFound)?; if lists.get_list(txn, list_id).await?.is_none() { return Err(DomainError::NotFound); } let list_meal_id = list_meals .add_meal(txn, list_id, meal.id, meal.name.clone()) .await?; let new_items = meal .ingredients .into_iter() .map(|ingredient| NewItem { name: ingredient.name, quantity: ingredient.quantity, note: ingredient.note, category_id: ingredient.category_id, list_meal_id: Some(list_meal_id), }) .collect(); items.add_items_bulk(txn, list_id, new_items).await }) }) .await?; self.realtime.publish_list_changed(list_id, revision).await; Ok(revision) } /// Lists the meals that have been added to a list, most recent first. pub async fn list_meals_on_list(&self, list_id: i64) -> DomainResult> { let list_meals = Arc::clone(&self.list_meals); self.db .run(move |txn| Box::pin(async move { list_meals.list_meals(txn, list_id).await })) .await } /// Removes a meal instance from a list, deleting the items that came from it /// and bumping the list revision exactly once. pub async fn remove_meal_from_list( &self, list_id: i64, list_meal_id: i64, ) -> DomainResult { let list_meals = Arc::clone(&self.list_meals); let revision = self .db .run(move |txn| { Box::pin(async move { list_meals.remove_meal(txn, list_id, list_meal_id).await }) }) .await?; self.realtime.publish_list_changed(list_id, revision).await; Ok(revision) } } pub struct InvitationService { db: SqliteDatabase, invitations: Arc, tokens: Arc, } impl InvitationService { pub fn new( db: SqliteDatabase, invitations: Arc, tokens: Arc, ) -> Self { Self { db, invitations, tokens, } } pub async fn create_invitation(&self, created_by: i64) -> DomainResult { 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 { 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 } } pub struct RewardsCardService { db: SqliteDatabase, cards: Arc, } impl RewardsCardService { pub fn new(db: SqliteDatabase, cards: Arc) -> Self { Self { db, cards } } pub async fn list_cards(&self, user_id: i64) -> DomainResult> { let cards = Arc::clone(&self.cards); self.db .run(move |txn| Box::pin(async move { cards.list_cards(txn, user_id).await })) .await } pub async fn get_card(&self, user_id: i64, card_id: i64) -> DomainResult> { let cards = Arc::clone(&self.cards); self.db .run(move |txn| Box::pin(async move { cards.get_card(txn, user_id, card_id).await })) .await } pub async fn create_card( &self, user_id: i64, store_name: String, number: String, symbology: String, ) -> DomainResult { let cards = Arc::clone(&self.cards); self.db .run(move |txn| { Box::pin(async move { cards .create_card(txn, user_id, store_name, number, symbology) .await }) }) .await } pub async fn delete_card(&self, user_id: i64, card_id: i64) -> DomainResult<()> { let cards = Arc::clone(&self.cards); self.db .run(move |txn| Box::pin(async move { cards.delete_card(txn, user_id, card_id).await })) .await } }