diff --git a/src/application/mod.rs b/src/application/mod.rs index c3ddc22..c84a49f 100644 --- a/src/application/mod.rs +++ b/src/application/mod.rs @@ -3,6 +3,8 @@ pub mod errors; pub mod routes; pub mod server; pub mod services; +pub mod state; pub use routes::app_router; pub use server::{ServerConfig, serve}; +pub use state::{AppState, AppStateConfig}; diff --git a/src/application/server.rs b/src/application/server.rs index 617e425..00b38cc 100644 --- a/src/application/server.rs +++ b/src/application/server.rs @@ -2,7 +2,6 @@ use std::net::SocketAddr; use std::sync::Arc; use anyhow::Context; -use axum::Router; use chrono::{Duration, Utc}; use tokio::net::TcpListener; use tokio::signal; @@ -10,33 +9,11 @@ use tracing::info; use webauthn_rs::prelude::*; use crate::application::routes::app_router; -use crate::application::services::{ - BagService, BrewService, CafeService, CupService, GearService, RoastService, RoasterService, -}; +use crate::application::state::{AppState, AppStateConfig}; use crate::domain::registration_tokens::NewRegistrationToken; -use crate::domain::repositories::{ - AiUsageRepository, BagRepository, BrewRepository, CafeRepository, CupRepository, - GearRepository, PasskeyCredentialRepository, RegistrationTokenRepository, RoastRepository, - RoasterRepository, SessionRepository, TimelineEventRepository, TokenRepository, UserRepository, -}; +use crate::domain::repositories::{RegistrationTokenRepository, UserRepository}; use crate::infrastructure::auth::{generate_session_token, hash_token}; -use crate::infrastructure::backup::BackupService; use crate::infrastructure::database::Database; -use crate::infrastructure::repositories::ai_usage::SqlAiUsageRepository; -use crate::infrastructure::repositories::bags::SqlBagRepository; -use crate::infrastructure::repositories::brews::SqlBrewRepository; -use crate::infrastructure::repositories::cafes::SqlCafeRepository; -use crate::infrastructure::repositories::cups::SqlCupRepository; -use crate::infrastructure::repositories::gear::SqlGearRepository; -use crate::infrastructure::repositories::passkey_credentials::SqlPasskeyCredentialRepository; -use crate::infrastructure::repositories::registration_tokens::SqlRegistrationTokenRepository; -use crate::infrastructure::repositories::roasters::SqlRoasterRepository; -use crate::infrastructure::repositories::roasts::SqlRoastRepository; -use crate::infrastructure::repositories::sessions::SqlSessionRepository; -use crate::infrastructure::repositories::timeline_events::SqlTimelineEventRepository; -use crate::infrastructure::repositories::tokens::SqlTokenRepository; -use crate::infrastructure::repositories::users::SqlUserRepository; -use crate::infrastructure::webauthn::ChallengeStore; pub struct ServerConfig { pub bind_address: SocketAddr, @@ -48,41 +25,6 @@ pub struct ServerConfig { pub foursquare_api_key: String, } -#[derive(Clone)] -pub struct AppState { - pub roaster_repo: Arc, - pub roast_repo: Arc, - pub bag_repo: Arc, - pub gear_repo: Arc, - pub brew_repo: Arc, - pub cafe_repo: Arc, - pub cup_repo: Arc, - pub timeline_repo: Arc, - pub user_repo: Arc, - pub token_repo: Arc, - pub session_repo: Arc, - pub passkey_repo: Arc, - pub registration_token_repo: Arc, - pub ai_usage_repo: Arc, - pub webauthn: Arc, - pub challenge_store: Arc, - pub http_client: reqwest::Client, - pub foursquare_url: String, - pub foursquare_api_key: String, - pub openrouter_url: String, - pub openrouter_api_key: String, - pub openrouter_model: String, - pub backup_service: Arc, - pub roaster_service: RoasterService, - pub roast_service: RoastService, - pub bag_service: BagService, - pub brew_service: BrewService, - pub gear_service: GearService, - pub cafe_service: CafeService, - pub cup_service: CupService, -} - -#[allow(clippy::too_many_lines)] pub async fn serve(config: ServerConfig) -> anyhow::Result<()> { let database = Database::connect(&config.database_url) .await @@ -97,95 +39,31 @@ pub async fn serve(config: ServerConfig) -> anyhow::Result<()> { .context("failed to build WebAuthn instance")?, ); - let roaster_repo: Arc = - Arc::new(SqlRoasterRepository::new(database.clone_pool())); - let roast_repo: Arc = - Arc::new(SqlRoastRepository::new(database.clone_pool())); - let bag_repo: Arc = Arc::new(SqlBagRepository::new(database.clone_pool())); - let gear_repo: Arc = - Arc::new(SqlGearRepository::new(database.clone_pool())); - let brew_repo: Arc = - Arc::new(SqlBrewRepository::new(database.clone_pool())); - let cafe_repo: Arc = - Arc::new(SqlCafeRepository::new(database.clone_pool())); - let cup_repo: Arc = Arc::new(SqlCupRepository::new(database.clone_pool())); - let timeline_repo: Arc = - Arc::new(SqlTimelineEventRepository::new(database.clone_pool())); - let user_repo: Arc = - Arc::new(SqlUserRepository::new(database.clone_pool())); - let token_repo: Arc = - Arc::new(SqlTokenRepository::new(database.clone_pool())); - let session_repo: Arc = - Arc::new(SqlSessionRepository::new(database.clone_pool())); - let passkey_repo: Arc = - Arc::new(SqlPasskeyCredentialRepository::new(database.clone_pool())); - let registration_token_repo: Arc = - Arc::new(SqlRegistrationTokenRepository::new(database.clone_pool())); - let ai_usage_repo: Arc = - Arc::new(SqlAiUsageRepository::new(database.clone_pool())); - - let backup_service = Arc::new(BackupService::new(database.clone_pool())); - let challenge_store = Arc::new(ChallengeStore::new()); - - let roaster_service = - RoasterService::new(Arc::clone(&roaster_repo), Arc::clone(&timeline_repo)); - let roast_service = RoastService::new( - Arc::clone(&roast_repo), - Arc::clone(&roaster_repo), - Arc::clone(&timeline_repo), + let state = AppState::from_database( + &database, + AppStateConfig { + webauthn, + foursquare_url: crate::infrastructure::foursquare::FOURSQUARE_SEARCH_URL.to_string(), + foursquare_api_key: config.foursquare_api_key, + openrouter_url: crate::infrastructure::ai::OPENROUTER_URL.to_string(), + openrouter_api_key: config.openrouter_api_key, + openrouter_model: config.openrouter_model, + }, ); - let bag_service = BagService::new( - Arc::clone(&bag_repo), - Arc::clone(&roast_repo), - Arc::clone(&roaster_repo), - Arc::clone(&timeline_repo), - ); - let brew_service = BrewService::new(Arc::clone(&brew_repo), Arc::clone(&timeline_repo)); - let gear_service = GearService::new(Arc::clone(&gear_repo), Arc::clone(&timeline_repo)); - let cafe_service = CafeService::new(Arc::clone(&cafe_repo), Arc::clone(&timeline_repo)); - let cup_service = CupService::new(Arc::clone(&cup_repo), Arc::clone(&timeline_repo)); // Bootstrap: if no users exist, generate a one-time registration token - bootstrap_registration(®istration_token_repo, &user_repo, &config.rp_origin).await?; - - let state = AppState { - roaster_repo, - roast_repo, - bag_repo, - gear_repo, - brew_repo, - cafe_repo, - cup_repo, - timeline_repo, - user_repo, - token_repo, - session_repo, - passkey_repo, - registration_token_repo, - ai_usage_repo, - webauthn, - challenge_store, - http_client: reqwest::Client::new(), - foursquare_url: crate::infrastructure::foursquare::FOURSQUARE_SEARCH_URL.to_string(), - foursquare_api_key: config.foursquare_api_key, - openrouter_url: crate::infrastructure::ai::OPENROUTER_URL.to_string(), - openrouter_api_key: config.openrouter_api_key, - openrouter_model: config.openrouter_model, - backup_service, - roaster_service, - roast_service, - bag_service, - brew_service, - gear_service, - cafe_service, - cup_service, - }; + bootstrap_registration( + &state.registration_token_repo, + &state.user_repo, + &config.rp_origin, + ) + .await?; let listener = TcpListener::bind(config.bind_address) .await .with_context(|| format!("failed to bind to {}", config.bind_address))?; - let app: Router = app_router(state); + let app = app_router(state); info!( address = %config.bind_address, diff --git a/src/application/state.rs b/src/application/state.rs new file mode 100644 index 0000000..db4864c --- /dev/null +++ b/src/application/state.rs @@ -0,0 +1,157 @@ +use std::sync::Arc; + +use webauthn_rs::prelude::*; + +use crate::application::services::{ + BagService, BrewService, CafeService, CupService, GearService, RoastService, RoasterService, +}; +use crate::domain::repositories::{ + AiUsageRepository, BagRepository, BrewRepository, CafeRepository, CupRepository, + GearRepository, PasskeyCredentialRepository, RegistrationTokenRepository, RoastRepository, + RoasterRepository, SessionRepository, TimelineEventRepository, TokenRepository, UserRepository, +}; +use crate::infrastructure::backup::BackupService; +use crate::infrastructure::database::Database; +use crate::infrastructure::repositories::ai_usage::SqlAiUsageRepository; +use crate::infrastructure::repositories::bags::SqlBagRepository; +use crate::infrastructure::repositories::brews::SqlBrewRepository; +use crate::infrastructure::repositories::cafes::SqlCafeRepository; +use crate::infrastructure::repositories::cups::SqlCupRepository; +use crate::infrastructure::repositories::gear::SqlGearRepository; +use crate::infrastructure::repositories::passkey_credentials::SqlPasskeyCredentialRepository; +use crate::infrastructure::repositories::registration_tokens::SqlRegistrationTokenRepository; +use crate::infrastructure::repositories::roasters::SqlRoasterRepository; +use crate::infrastructure::repositories::roasts::SqlRoastRepository; +use crate::infrastructure::repositories::sessions::SqlSessionRepository; +use crate::infrastructure::repositories::timeline_events::SqlTimelineEventRepository; +use crate::infrastructure::repositories::tokens::SqlTokenRepository; +use crate::infrastructure::repositories::users::SqlUserRepository; +use crate::infrastructure::webauthn::ChallengeStore; + +/// Configuration for external services and auth — everything that varies +/// between production and test environments. Repos and services are created +/// automatically from the database pool. +pub struct AppStateConfig { + pub webauthn: Arc, + pub foursquare_url: String, + pub foursquare_api_key: String, + pub openrouter_url: String, + pub openrouter_api_key: String, + pub openrouter_model: String, +} + +#[derive(Clone)] +pub struct AppState { + pub roaster_repo: Arc, + pub roast_repo: Arc, + pub bag_repo: Arc, + pub gear_repo: Arc, + pub brew_repo: Arc, + pub cafe_repo: Arc, + pub cup_repo: Arc, + pub timeline_repo: Arc, + pub user_repo: Arc, + pub token_repo: Arc, + pub session_repo: Arc, + pub passkey_repo: Arc, + pub registration_token_repo: Arc, + pub ai_usage_repo: Arc, + pub webauthn: Arc, + pub challenge_store: Arc, + pub http_client: reqwest::Client, + pub foursquare_url: String, + pub foursquare_api_key: String, + pub openrouter_url: String, + pub openrouter_api_key: String, + pub openrouter_model: String, + pub backup_service: Arc, + pub roaster_service: RoasterService, + pub roast_service: RoastService, + pub bag_service: BagService, + pub brew_service: BrewService, + pub gear_service: GearService, + pub cafe_service: CafeService, + pub cup_service: CupService, +} + +impl AppState { + /// Build the full application state from a database connection and config. + /// Creates all repositories and services internally. + pub fn from_database(database: &Database, config: AppStateConfig) -> Self { + let pool = database.clone_pool(); + + let roaster_repo: Arc = + Arc::new(SqlRoasterRepository::new(pool.clone())); + let roast_repo: Arc = Arc::new(SqlRoastRepository::new(pool.clone())); + let bag_repo: Arc = Arc::new(SqlBagRepository::new(pool.clone())); + let gear_repo: Arc = Arc::new(SqlGearRepository::new(pool.clone())); + let brew_repo: Arc = Arc::new(SqlBrewRepository::new(pool.clone())); + let cafe_repo: Arc = Arc::new(SqlCafeRepository::new(pool.clone())); + let cup_repo: Arc = Arc::new(SqlCupRepository::new(pool.clone())); + let timeline_repo: Arc = + Arc::new(SqlTimelineEventRepository::new(pool.clone())); + let user_repo: Arc = Arc::new(SqlUserRepository::new(pool.clone())); + let token_repo: Arc = Arc::new(SqlTokenRepository::new(pool.clone())); + let session_repo: Arc = + Arc::new(SqlSessionRepository::new(pool.clone())); + let passkey_repo: Arc = + Arc::new(SqlPasskeyCredentialRepository::new(pool.clone())); + let registration_token_repo: Arc = + Arc::new(SqlRegistrationTokenRepository::new(pool.clone())); + let ai_usage_repo: Arc = + Arc::new(SqlAiUsageRepository::new(pool.clone())); + + let backup_service = Arc::new(BackupService::new(pool)); + + let roaster_service = + RoasterService::new(Arc::clone(&roaster_repo), Arc::clone(&timeline_repo)); + let roast_service = RoastService::new( + Arc::clone(&roast_repo), + Arc::clone(&roaster_repo), + Arc::clone(&timeline_repo), + ); + let bag_service = BagService::new( + Arc::clone(&bag_repo), + Arc::clone(&roast_repo), + Arc::clone(&roaster_repo), + Arc::clone(&timeline_repo), + ); + let brew_service = BrewService::new(Arc::clone(&brew_repo), Arc::clone(&timeline_repo)); + let gear_service = GearService::new(Arc::clone(&gear_repo), Arc::clone(&timeline_repo)); + let cafe_service = CafeService::new(Arc::clone(&cafe_repo), Arc::clone(&timeline_repo)); + let cup_service = CupService::new(Arc::clone(&cup_repo), Arc::clone(&timeline_repo)); + + Self { + roaster_repo, + roast_repo, + bag_repo, + gear_repo, + brew_repo, + cafe_repo, + cup_repo, + timeline_repo, + user_repo, + token_repo, + session_repo, + passkey_repo, + registration_token_repo, + ai_usage_repo, + webauthn: config.webauthn, + challenge_store: Arc::new(ChallengeStore::new()), + http_client: reqwest::Client::new(), + foursquare_url: config.foursquare_url, + foursquare_api_key: config.foursquare_api_key, + openrouter_url: config.openrouter_url, + openrouter_api_key: config.openrouter_api_key, + openrouter_model: config.openrouter_model, + backup_service, + roaster_service, + roast_service, + bag_service, + brew_service, + gear_service, + cafe_service, + cup_service, + } + } +}