use std::sync::Arc; use auth::features::AuthService; use auth::pow::{PowService, PowSettings}; use cache::redis::RedisCache; use db::conn::Db; use email::{EmailService, EmailServiceBuilder}; use issues::service::IssueService; use jwt::JwtState; use jwt::keys::SigningKeyCache; use project::service::ProjectService; use queue::{Queue, QueueBuilder}; use repo::service::RepoService; use space::features::SpaceService; use storage::StorageRepository; use storage::repo::from_config as storage_from_config; use thiserror::Error; use tokio::sync::RwLock; use user::features::UserService; use crate::config::AppConfig; /// Error raised while constructing the service stack. #[derive(Debug, Error)] pub enum WebapiError { #[error("database: {0}")] Db(#[from] db::conn::DbError), #[error("cache: {0}")] Cache(#[from] cache::redis::RedisError), #[error("email: {0}")] Email(#[from] email::EmailError), #[error("queue: {0}")] Queue(#[from] queue::QueueError), #[error("storage: {0}")] Storage(#[from] storage::StorageError), /// Auth routes require a working email transport (verification codes, /// password reset, email reclaim). Refuse to boot without one. #[error("email service is required (auth depends on it); enable email in config")] EmailRequired, } pub type WebapiResult = Result; /// All services built from an [`AppConfig`], ready to be handed to the router. pub struct AppServices { pub db: Db, pub kv: Arc, pub jwt: JwtState, pub email: EmailService, pub queue: Option>, pub storage: Arc, pub auth: AuthService, pub issues: IssueService, pub project: ProjectService, pub repo: RepoService, pub space: SpaceService, pub user: UserService, } impl AppServices { /// Build the full service stack from configuration. /// /// The queue is optional (its settings may disable it); when it is /// disabled the email service cannot be built either, and because the /// auth service requires a concrete email transport this fails startup /// with [`WebapiError::EmailRequired`]. pub async fn build(cfg: &AppConfig) -> WebapiResult { tracing::info!("building service stack"); let db = Db::from_config(&cfg.db).await?; tracing::info!("database connected"); let kv = Arc::new(RedisCache::new(&cfg.cache).await?); tracing::info!("cache connected"); let queue = QueueBuilder::from_config(cfg.queue.clone()) .build() .await? .map(Arc::new); tracing::info!("queue ready"); let email = match queue.clone() { Some(queue) => { let service = EmailServiceBuilder::new(cfg.email.clone(), queue) .build() .await?; match service { Some(service) => { tracing::info!("email service enabled"); service } None => return Err(WebapiError::EmailRequired), } } None => return Err(WebapiError::EmailRequired), }; let storage = storage_from_config(&cfg.storage)?; tracing::info!("storage ready"); let jwt = JwtState::new( db.writer(), cfg.jwt.clone(), Arc::new(RwLock::new(SigningKeyCache::new())), ); let pow = PowService::new(PowSettings::default()); let auth = AuthService::new( db.clone(), email.clone(), kv.clone(), pow, cfg.auth.clone(), jwt.clone(), ); let issues = IssueService::new(db.clone(), kv.clone(), jwt.clone(), storage.clone()) .with_config(Arc::new(cfg.issues.clone())); let project = ProjectService::new(db.clone(), kv.clone(), jwt.clone()) .with_config(Arc::new(cfg.project.clone())); let repo = RepoService::new(db.clone(), kv.clone(), jwt.clone()) .with_config(Arc::new(cfg.repo.clone())); let space = SpaceService::new(db.clone(), kv.clone(), jwt.clone()) .with_config(Arc::new(cfg.space.clone())); let user = UserService::new(db.clone(), kv.clone(), jwt.clone(), storage.clone()) .with_config(Arc::new(cfg.user.clone())); tracing::info!("service stack ready"); Ok(Self { db, kv, jwt, email, queue, storage, auth, issues, project, repo, space, user, }) } }