From 04f9c136258b83bc59365c4596b6403ad4169347 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Sun, 25 Oct 2026 21:07:19 +0100 Subject: [PATCH] refactor: adapters configured from config crate --- Cargo.lock | 14 ++++++ crates/adapters/activitypub/Cargo.toml | 1 + crates/adapters/activitypub/src/lib.rs | 6 +-- crates/adapters/auth/Cargo.toml | 1 + crates/adapters/auth/src/lib.rs | 20 ++++----- crates/adapters/event-publisher/Cargo.toml | 1 + crates/adapters/event-publisher/src/lib.rs | 10 ++--- crates/adapters/image-converter/Cargo.toml | 1 + crates/adapters/image-converter/src/config.rs | 7 +-- crates/adapters/image-converter/src/lib.rs | 3 +- .../image-converter/src/tests/config.rs | 12 ++++++ crates/adapters/metadata/Cargo.toml | 1 + crates/adapters/metadata/src/lib.rs | 10 +++-- crates/adapters/nats/Cargo.toml | 1 + crates/adapters/nats/src/config.rs | 12 +++--- crates/adapters/nats/src/tests/config.rs | 17 ++++++++ crates/adapters/object-storage/Cargo.toml | 1 + crates/adapters/object-storage/src/config.rs | 28 ++++++------ crates/adapters/object-storage/src/lib.rs | 4 +- .../object-storage/src/tests/config.rs | 19 ++++++++ crates/adapters/poster-fetcher/Cargo.toml | 1 + crates/adapters/poster-fetcher/src/config.rs | 10 ++--- crates/adapters/poster-fetcher/src/lib.rs | 12 ++++-- crates/adapters/sqlite/Cargo.toml | 1 + crates/adapters/sqlite/src/event_queue/mod.rs | 27 +++++------- crates/adapters/tmdb-enrichment/Cargo.toml | 1 + crates/adapters/tmdb-enrichment/src/client.rs | 8 ++-- .../src/diary/tests/get_activity_feed.rs | 2 + crates/application/src/test_helpers.rs | 2 + crates/composition/Cargo.toml | 1 + crates/composition/src/factory.rs | 24 +++++++---- crates/composition/src/tests/build.rs | 2 + crates/infra-wiring/Cargo.toml | 1 + crates/infra-wiring/src/config.rs | 43 +++++++------------ crates/infra-wiring/src/lib.rs | 7 +-- crates/presentation/src/csrf.rs | 22 ++++++---- crates/presentation/src/handlers/auth.rs | 18 +++++--- crates/presentation/src/routes.rs | 20 +++++---- crates/presentation/src/tests/extractors.rs | 2 + crates/server/Cargo.toml | 1 + crates/server/src/main.rs | 41 +++++++++--------- crates/server/tests/api_test.rs | 2 + crates/worker/Cargo.toml | 1 + crates/worker/src/event_bus.rs | 13 +++--- crates/worker/src/main.rs | 23 +++++----- 45 files changed, 277 insertions(+), 177 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 10331fbf..d4de1912 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,7 @@ dependencies = [ "async-trait", "axum", "chrono", + "config", "domain", "k-ap", "serde", @@ -663,6 +664,7 @@ dependencies = [ "argon2", "async-trait", "chrono", + "config", "domain", "jsonwebtoken", "rand_core 0.6.4", @@ -1128,6 +1130,7 @@ dependencies = [ "async-trait", "auth", "chrono", + "config", "domain", "infra-wiring", "jellyfin", @@ -1887,6 +1890,7 @@ name = "event-publisher" version = "0.1.0" dependencies = [ "async-trait", + "config", "domain", "futures", "tokio", @@ -2693,6 +2697,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", + "config", "domain", "image", "object-storage", @@ -2771,6 +2776,7 @@ name = "infra-wiring" version = "0.1.0" dependencies = [ "anyhow", + "config", "sqlx", ] @@ -3246,6 +3252,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", + "config", "domain", "reqwest 0.13.3", "serde", @@ -3350,6 +3357,7 @@ dependencies = [ "async-nats", "async-trait", "chrono", + "config", "domain", "event-payload", "futures", @@ -3571,6 +3579,7 @@ dependencies = [ "anyhow", "async-trait", "bytes", + "config", "domain", "futures", "infer", @@ -3949,6 +3958,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", + "config", "domain", "reqwest 0.13.3", ] @@ -4946,6 +4956,7 @@ dependencies = [ "axum", "bytes", "composition", + "config", "domain", "dotenvy", "export", @@ -5208,6 +5219,7 @@ dependencies = [ "async-trait", "bytes", "chrono", + "config", "domain", "event-payload", "futures", @@ -5828,6 +5840,7 @@ dependencies = [ "anyhow", "async-trait", "chrono", + "config", "domain", "reqwest 0.13.3", "serde", @@ -7175,6 +7188,7 @@ dependencies = [ "async-trait", "auth", "composition", + "config", "domain", "dotenvy", "export", diff --git a/crates/adapters/activitypub/Cargo.toml b/crates/adapters/activitypub/Cargo.toml index 261eb766..c00dcf72 100644 --- a/crates/adapters/activitypub/Cargo.toml +++ b/crates/adapters/activitypub/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition.workspace = true [dependencies] +config = { workspace = true } k-ap = { version = "0.5.0", registry = "gitea" } domain = { workspace = true } axum = { workspace = true } diff --git a/crates/adapters/activitypub/src/lib.rs b/crates/adapters/activitypub/src/lib.rs index ee8e3531..3fa93716 100644 --- a/crates/adapters/activitypub/src/lib.rs +++ b/crates/adapters/activitypub/src/lib.rs @@ -68,6 +68,7 @@ pub struct ActivityPubDeps { pub follow_query: std::sync::Arc, pub instance: domain::value_objects::InstanceIdentity, pub allow_registration: bool, + pub federation: config::FederationConfig, pub event_publisher: std::sync::Arc, } @@ -92,6 +93,7 @@ pub async fn wire(deps: ActivityPubDeps) -> anyhow::Result { follow_query: _, instance, allow_registration, + federation, event_publisher, } = deps; let review_handler = std::sync::Arc::new(ReviewObjectHandler { @@ -118,9 +120,7 @@ pub async fn wire(deps: ActivityPubDeps) -> anyhow::Result { goal: goal_handler, }); - let federation_debug = std::env::var("FEDERATION_DEBUG") - .map(|v| v == "true" || v == "1") - .unwrap_or(false); + let federation_debug = federation.debug; if federation_debug { tracing::warn!( diff --git a/crates/adapters/auth/Cargo.toml b/crates/adapters/auth/Cargo.toml index 89a9f7b3..a55b773d 100644 --- a/crates/adapters/auth/Cargo.toml +++ b/crates/adapters/auth/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition.workspace = true [dependencies] +config = { workspace = true } async-trait = { workspace = true } domain = { workspace = true } anyhow = { workspace = true } diff --git a/crates/adapters/auth/src/lib.rs b/crates/adapters/auth/src/lib.rs index 6271e4b0..7cf767bf 100644 --- a/crates/adapters/auth/src/lib.rs +++ b/crates/adapters/auth/src/lib.rs @@ -22,19 +22,17 @@ pub struct AuthConfig { } impl AuthConfig { - pub fn from_env() -> anyhow::Result { - let secret = std::env::var("JWT_SECRET") - .map_err(|_| anyhow::anyhow!("JWT_SECRET env var is required"))?; + pub fn from_config(config: &config::AuthConfig) -> anyhow::Result { + let secret = config + .jwt_secret + .clone() + .ok_or_else(|| anyhow::anyhow!("JWT_SECRET env var is required"))?; if secret.is_empty() { anyhow::bail!("JWT_SECRET must not be empty"); } - let ttl_seconds = std::env::var("JWT_TTL_SECONDS") - .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(900u64); Ok(Self { secret, - ttl_seconds, + ttl_seconds: config.jwt_ttl_seconds, }) } } @@ -107,11 +105,13 @@ impl PasswordHasher for Argon2PasswordHasher { } } -pub fn create() -> anyhow::Result<( +pub fn create( + config: &config::AuthConfig, +) -> anyhow::Result<( std::sync::Arc, std::sync::Arc, )> { - let config = AuthConfig::from_env()?; + let config = AuthConfig::from_config(config)?; Ok(( std::sync::Arc::new(JwtAuthService::new(config)), std::sync::Arc::new(Argon2PasswordHasher), diff --git a/crates/adapters/event-publisher/Cargo.toml b/crates/adapters/event-publisher/Cargo.toml index bb925b3a..09aaee99 100644 --- a/crates/adapters/event-publisher/Cargo.toml +++ b/crates/adapters/event-publisher/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition.workspace = true [dependencies] +config = { workspace = true } domain = { workspace = true } async-trait = { workspace = true } tokio = { workspace = true } diff --git a/crates/adapters/event-publisher/src/lib.rs b/crates/adapters/event-publisher/src/lib.rs index 302ed8f5..aec30ee4 100644 --- a/crates/adapters/event-publisher/src/lib.rs +++ b/crates/adapters/event-publisher/src/lib.rs @@ -16,12 +16,10 @@ pub struct EventPublisherConfig { } impl EventPublisherConfig { - pub fn from_env() -> Self { - let channel_buffer = std::env::var("EVENT_CHANNEL_BUFFER") - .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(128); - Self { channel_buffer } + pub fn from_config(config: &config::EventsConfig) -> Self { + Self { + channel_buffer: config.channel_buffer, + } } } diff --git a/crates/adapters/image-converter/Cargo.toml b/crates/adapters/image-converter/Cargo.toml index 3e39230c..27053717 100644 --- a/crates/adapters/image-converter/Cargo.toml +++ b/crates/adapters/image-converter/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] +config = { workspace = true } domain = { workspace = true } async-trait = { workspace = true } anyhow = { workspace = true } diff --git a/crates/adapters/image-converter/src/config.rs b/crates/adapters/image-converter/src/config.rs index 04bf3c69..4745e970 100644 --- a/crates/adapters/image-converter/src/config.rs +++ b/crates/adapters/image-converter/src/config.rs @@ -18,11 +18,8 @@ pub struct ConversionConfig { } impl ConversionConfig { - pub fn from_env() -> anyhow::Result> { - Self::from_vars( - std::env::var("IMAGE_CONVERSION_ENABLED").ok().as_deref(), - std::env::var("IMAGE_CONVERSION_FORMAT").ok().as_deref(), - ) + pub fn from_config(config: &::config::ImageConversionConfig) -> anyhow::Result> { + Self::from_vars(config.enabled.then_some("true"), config.format.as_deref()) } fn from_vars(enabled: Option<&str>, format: Option<&str>) -> anyhow::Result> { diff --git a/crates/adapters/image-converter/src/lib.rs b/crates/adapters/image-converter/src/lib.rs index d34822e6..a9dba8f2 100644 --- a/crates/adapters/image-converter/src/lib.rs +++ b/crates/adapters/image-converter/src/lib.rs @@ -14,12 +14,13 @@ use std::sync::Arc; type ConversionPair = (Arc, Arc); pub fn build( + config: &::config::ImageConversionConfig, object_storage: Arc, image_ref_command: Arc, image_ref_query: Arc, event_publisher: Arc, ) -> anyhow::Result> { - let config = match ConversionConfig::from_env()? { + let config = match ConversionConfig::from_config(config)? { Some(c) => c, None => return Ok(None), }; diff --git a/crates/adapters/image-converter/src/tests/config.rs b/crates/adapters/image-converter/src/tests/config.rs index 65941a71..9747b481 100644 --- a/crates/adapters/image-converter/src/tests/config.rs +++ b/crates/adapters/image-converter/src/tests/config.rs @@ -43,3 +43,15 @@ fn avif_extension() { fn webp_extension() { assert_eq!(Format::Webp.extension(), ".webp"); } + +#[test] +fn from_config_follows_section() { + let disabled = ::config::ImageConversionConfig::default(); + assert!(ConversionConfig::from_config(&disabled).unwrap().is_none()); + let enabled = ::config::ImageConversionConfig { + enabled: true, + format: Some("webp".into()), + }; + let cfg = ConversionConfig::from_config(&enabled).unwrap().unwrap(); + assert_eq!(cfg.format, Format::Webp); +} diff --git a/crates/adapters/metadata/Cargo.toml b/crates/adapters/metadata/Cargo.toml index efbb2a6e..2cecca90 100644 --- a/crates/adapters/metadata/Cargo.toml +++ b/crates/adapters/metadata/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition.workspace = true [dependencies] +config = { workspace = true } anyhow = { workspace = true } async-trait = { workspace = true } reqwest = { workspace = true } diff --git a/crates/adapters/metadata/src/lib.rs b/crates/adapters/metadata/src/lib.rs index 6bd8d029..078ec401 100644 --- a/crates/adapters/metadata/src/lib.rs +++ b/crates/adapters/metadata/src/lib.rs @@ -68,12 +68,16 @@ impl MetadataClient for MetadataClientImpl { } } -pub fn create() -> anyhow::Result> { +pub fn create( + config: &config::MetadataConfig, +) -> anyhow::Result> { use anyhow::Context; - if let Ok(key) = std::env::var("TMDB_API_KEY") { + if let Some(key) = config.tmdb_api_key.clone() { Ok(std::sync::Arc::new(MetadataClientImpl::new_tmdb(key))) } else { - let key = std::env::var("OMDB_API_KEY") + let key = config + .omdb_api_key + .clone() .context("either TMDB_API_KEY or OMDB_API_KEY must be set")?; Ok(std::sync::Arc::new(MetadataClientImpl::new_omdb(key))) } diff --git a/crates/adapters/nats/Cargo.toml b/crates/adapters/nats/Cargo.toml index c2318dc2..ecaacd80 100644 --- a/crates/adapters/nats/Cargo.toml +++ b/crates/adapters/nats/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition.workspace = true [dependencies] +config = { workspace = true } async-nats = "0.48.0" domain = { workspace = true } diff --git a/crates/adapters/nats/src/config.rs b/crates/adapters/nats/src/config.rs index 6395af52..31d5b393 100644 --- a/crates/adapters/nats/src/config.rs +++ b/crates/adapters/nats/src/config.rs @@ -14,13 +14,13 @@ pub struct NatsConfig { } impl NatsConfig { - pub fn from_env() -> anyhow::Result { + pub fn from_config(config: &::config::NatsConfig) -> anyhow::Result { Self::from_vars( - std::env::var("NATS_URL").ok().as_deref(), - std::env::var("NATS_MODE").ok().as_deref(), - std::env::var("NATS_SUBJECT_PREFIX").ok().as_deref(), - std::env::var("NATS_STREAM_NAME").ok().as_deref(), - std::env::var("NATS_CONSUMER_NAME").ok().as_deref(), + config.url.as_deref(), + Some(&config.mode), + Some(&config.subject_prefix), + Some(&config.stream_name), + Some(&config.consumer_name), ) } diff --git a/crates/adapters/nats/src/tests/config.rs b/crates/adapters/nats/src/tests/config.rs index af9ed729..d81453e8 100644 --- a/crates/adapters/nats/src/tests/config.rs +++ b/crates/adapters/nats/src/tests/config.rs @@ -28,3 +28,20 @@ fn invalid_mode_errors() { NatsConfig::from_vars(Some("nats://test:4222"), Some("kafka"), None, None, None).is_err() ); } + +#[test] +fn from_config_requires_url() { + assert!(NatsConfig::from_config(&::config::NatsConfig::default()).is_err()); +} + +#[test] +fn from_config_uses_section_values() { + let section = ::config::NatsConfig { + url: Some("nats://test:4222".into()), + mode: "core".into(), + ..Default::default() + }; + let cfg = NatsConfig::from_config(§ion).unwrap(); + assert_eq!(cfg.url, "nats://test:4222"); + assert_eq!(cfg.mode, NatsMode::Core); +} diff --git a/crates/adapters/object-storage/Cargo.toml b/crates/adapters/object-storage/Cargo.toml index fcf61cfc..ec36d0c8 100644 --- a/crates/adapters/object-storage/Cargo.toml +++ b/crates/adapters/object-storage/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition.workspace = true [dependencies] +config = { workspace = true } domain = { workspace = true } anyhow = { workspace = true } async-trait = { workspace = true } diff --git a/crates/adapters/object-storage/src/config.rs b/crates/adapters/object-storage/src/config.rs index 73dc5467..a6f66855 100644 --- a/crates/adapters/object-storage/src/config.rs +++ b/crates/adapters/object-storage/src/config.rs @@ -5,21 +5,25 @@ use std::sync::Arc; pub struct StorageConfig(Arc); impl StorageConfig { - pub fn from_env() -> anyhow::Result { - let backend = std::env::var("IMAGE_STORAGE_BACKEND").unwrap_or_else(|_| "local".into()); - - let store: Arc = match backend.as_str() { + pub fn from_config(config: &::config::StorageConfig) -> anyhow::Result { + let store: Arc = match config.backend.as_str() { "s3" => build_s3_store( - &std::env::var("MINIO_ENDPOINT").context("MINIO_ENDPOINT required")?, - &std::env::var("MINIO_ACCESS_KEY_ID").context("MINIO_ACCESS_KEY_ID required")?, - &std::env::var("MINIO_SECRET_ACCESS_KEY") + config + .endpoint + .as_deref() + .context("MINIO_ENDPOINT required")?, + config + .access_key_id + .as_deref() + .context("MINIO_ACCESS_KEY_ID required")?, + config + .secret_access_key + .as_deref() .context("MINIO_SECRET_ACCESS_KEY required")?, - &std::env::var("MINIO_BUCKET").context("MINIO_BUCKET required")?, - &std::env::var("MINIO_REGION").unwrap_or_else(|_| "minio".to_string()), - )?, - "local" => build_local_store( - &std::env::var("IMAGE_STORAGE_PATH").unwrap_or_else(|_| "./images".into()), + config.bucket.as_deref().context("MINIO_BUCKET required")?, + &config.region, )?, + "local" => build_local_store(&config.path)?, other => { anyhow::bail!("Unknown IMAGE_STORAGE_BACKEND: {other:?}. Valid values: s3, local") } diff --git a/crates/adapters/object-storage/src/lib.rs b/crates/adapters/object-storage/src/lib.rs index c183ba10..f66c339a 100644 --- a/crates/adapters/object-storage/src/lib.rs +++ b/crates/adapters/object-storage/src/lib.rs @@ -127,9 +127,9 @@ impl EventHandler for ImageCleanupHandler { } } -pub fn create() -> anyhow::Result> { +pub fn create(config: &::config::StorageConfig) -> anyhow::Result> { Ok(Arc::new(ObjectStorageAdapter::from_config( - StorageConfig::from_env()?, + StorageConfig::from_config(config)?, ))) } diff --git a/crates/adapters/object-storage/src/tests/config.rs b/crates/adapters/object-storage/src/tests/config.rs index f0ee8cc1..26fcd862 100644 --- a/crates/adapters/object-storage/src/tests/config.rs +++ b/crates/adapters/object-storage/src/tests/config.rs @@ -15,3 +15,22 @@ fn local_store_succeeds_if_dir_already_exists() { let result = build_local_store(dir.to_str().unwrap()); assert!(result.is_ok()); } + +#[test] +fn s3_backend_requires_endpoint() { + let section = ::config::StorageConfig { + backend: "s3".into(), + ..Default::default() + }; + let err = StorageConfig::from_config(§ion).err().unwrap(); + assert!(err.to_string().contains("MINIO_ENDPOINT required")); +} + +#[test] +fn unknown_backend_is_error() { + let section = ::config::StorageConfig { + backend: "ftp".into(), + ..Default::default() + }; + assert!(StorageConfig::from_config(§ion).is_err()); +} diff --git a/crates/adapters/poster-fetcher/Cargo.toml b/crates/adapters/poster-fetcher/Cargo.toml index 4d8ab67b..dad29290 100644 --- a/crates/adapters/poster-fetcher/Cargo.toml +++ b/crates/adapters/poster-fetcher/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] +config = { workspace = true } domain = { workspace = true } async-trait = { workspace = true } reqwest = { workspace = true } diff --git a/crates/adapters/poster-fetcher/src/config.rs b/crates/adapters/poster-fetcher/src/config.rs index def31db3..52d82d53 100644 --- a/crates/adapters/poster-fetcher/src/config.rs +++ b/crates/adapters/poster-fetcher/src/config.rs @@ -3,11 +3,9 @@ pub struct PosterFetcherConfig { } impl PosterFetcherConfig { - pub fn from_env() -> Self { - let timeout_seconds = std::env::var("POSTER_FETCH_TIMEOUT_SECONDS") - .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(30); - Self { timeout_seconds } + pub fn from_config(config: &::config::MetadataConfig) -> Self { + Self { + timeout_seconds: config.poster_fetch_timeout_seconds, + } } } diff --git a/crates/adapters/poster-fetcher/src/lib.rs b/crates/adapters/poster-fetcher/src/lib.rs index 187e4d76..504eae70 100644 --- a/crates/adapters/poster-fetcher/src/lib.rs +++ b/crates/adapters/poster-fetcher/src/lib.rs @@ -59,14 +59,18 @@ impl ImageFetcher for ReqwestPosterFetcher { } } -pub fn create() -> anyhow::Result> { +pub fn create( + config: &::config::MetadataConfig, +) -> anyhow::Result> { Ok(std::sync::Arc::new(ReqwestPosterFetcher::new( - PosterFetcherConfig::from_env(), + PosterFetcherConfig::from_config(config), )?)) } -pub fn create_image_fetcher() -> anyhow::Result> { +pub fn create_image_fetcher( + config: &::config::MetadataConfig, +) -> anyhow::Result> { Ok(std::sync::Arc::new(ReqwestPosterFetcher::new( - PosterFetcherConfig::from_env(), + PosterFetcherConfig::from_config(config), )?)) } diff --git a/crates/adapters/sqlite/Cargo.toml b/crates/adapters/sqlite/Cargo.toml index 4f207f4d..373715e7 100644 --- a/crates/adapters/sqlite/Cargo.toml +++ b/crates/adapters/sqlite/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition.workspace = true [dependencies] +config = { workspace = true } sqlx = { workspace = true, features = ["macros", "chrono"] } domain = { workspace = true } diff --git a/crates/adapters/sqlite/src/event_queue/mod.rs b/crates/adapters/sqlite/src/event_queue/mod.rs index 7798d060..8479ac8a 100644 --- a/crates/adapters/sqlite/src/event_queue/mod.rs +++ b/crates/adapters/sqlite/src/event_queue/mod.rs @@ -22,20 +22,11 @@ pub struct DbEventQueueConfig { } impl DbEventQueueConfig { - pub fn from_env() -> Self { + pub fn from_config(config: &config::EventsConfig) -> Self { Self { - poll_interval_ms: std::env::var("EVENT_QUEUE_POLL_INTERVAL_MS") - .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(500), - batch_size: std::env::var("EVENT_QUEUE_BATCH_SIZE") - .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(10), - max_attempts: std::env::var("EVENT_QUEUE_MAX_ATTEMPTS") - .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(5), + poll_interval_ms: config.queue_poll_interval_ms, + batch_size: config.queue_batch_size, + max_attempts: config.queue_max_attempts, } } } @@ -54,15 +45,19 @@ impl SqliteEventQueue { }) } - pub async fn create_publisher(pool: SqlitePool) -> anyhow::Result> { - let q = Self::create(pool, DbEventQueueConfig::from_env()).await?; + pub async fn create_publisher( + pool: SqlitePool, + config: &config::EventsConfig, + ) -> anyhow::Result> { + let q = Self::create(pool, DbEventQueueConfig::from_config(config)).await?; Ok(Arc::new(q)) } pub async fn create_channel( pool: SqlitePool, + config: &config::EventsConfig, ) -> anyhow::Result<(Arc, Arc)> { - let q = Self::create(pool, DbEventQueueConfig::from_env()).await?; + let q = Self::create(pool, DbEventQueueConfig::from_config(config)).await?; Ok((Arc::new(q.clone()), Arc::new(q))) } } diff --git a/crates/adapters/tmdb-enrichment/Cargo.toml b/crates/adapters/tmdb-enrichment/Cargo.toml index 11fa35c3..7e640900 100644 --- a/crates/adapters/tmdb-enrichment/Cargo.toml +++ b/crates/adapters/tmdb-enrichment/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition.workspace = true [dependencies] +config = { workspace = true } domain = { workspace = true } reqwest = { workspace = true } serde = { workspace = true } diff --git a/crates/adapters/tmdb-enrichment/src/client.rs b/crates/adapters/tmdb-enrichment/src/client.rs index 3fc468ef..09c111c5 100644 --- a/crates/adapters/tmdb-enrichment/src/client.rs +++ b/crates/adapters/tmdb-enrichment/src/client.rs @@ -7,9 +7,11 @@ pub struct TmdbEnrichmentClient { } impl TmdbEnrichmentClient { - pub fn from_env() -> Result { - let api_key = std::env::var("TMDB_API_KEY") - .map_err(|_| DomainError::InfrastructureError("TMDB_API_KEY is not set".into()))?; + pub fn from_config(config: &config::MetadataConfig) -> Result { + let api_key = config + .tmdb_api_key + .clone() + .ok_or_else(|| DomainError::InfrastructureError("TMDB_API_KEY is not set".into()))?; Ok(Self { api_key, http: reqwest::Client::new(), diff --git a/crates/application/src/diary/tests/get_activity_feed.rs b/crates/application/src/diary/tests/get_activity_feed.rs index 89471eea..0780aee8 100644 --- a/crates/application/src/diary/tests/get_activity_feed.rs +++ b/crates/application/src/diary/tests/get_activity_feed.rs @@ -129,6 +129,8 @@ async fn following_filter_separates_local_and_remote() { base_url: "http://localhost:3000".into(), rate_limit: 20, refresh_ttl_seconds: 2_592_000, + secure_cookies: false, + cors_origins: String::new(), wrapup: crate::config::WrapUpConfig { font_path: None, logo_path: None, diff --git a/crates/application/src/test_helpers.rs b/crates/application/src/test_helpers.rs index aa18b5ea..51872a51 100644 --- a/crates/application/src/test_helpers.rs +++ b/crates/application/src/test_helpers.rs @@ -136,6 +136,8 @@ impl TestContextBuilder { base_url: "http://localhost:3000".into(), rate_limit: 20, refresh_ttl_seconds: 2_592_000, + secure_cookies: false, + cors_origins: String::new(), wrapup: crate::config::WrapUpConfig { font_path: None, logo_path: None, diff --git a/crates/composition/Cargo.toml b/crates/composition/Cargo.toml index c0c4bdcd..292ae32d 100644 --- a/crates/composition/Cargo.toml +++ b/crates/composition/Cargo.toml @@ -9,6 +9,7 @@ nats = ["dep:nats", "infra-wiring/nats"] federation = ["application/federation", "dep:sqlite-federation", "dep:activitypub"] [dependencies] +config = { workspace = true } domain = { workspace = true } application = { workspace = true } infra-wiring = { workspace = true } diff --git a/crates/composition/src/factory.rs b/crates/composition/src/factory.rs index f1009836..1a15c430 100644 --- a/crates/composition/src/factory.rs +++ b/crates/composition/src/factory.rs @@ -103,20 +103,28 @@ pub async fn build_database_adapters( }) } -pub fn build_auth_adapters() -> anyhow::Result<(Arc, Arc)> { - auth::create() +pub fn build_auth_adapters( + config: &config::AuthConfig, +) -> anyhow::Result<(Arc, Arc)> { + auth::create(config) } -pub fn build_metadata_client() -> anyhow::Result> { - metadata::create() +pub fn build_metadata_client( + config: &config::MetadataConfig, +) -> anyhow::Result> { + metadata::create(config) } -pub fn build_poster_fetcher() -> anyhow::Result> { - poster_fetcher::create() +pub fn build_poster_fetcher( + config: &config::MetadataConfig, +) -> anyhow::Result> { + poster_fetcher::create(config) } -pub fn build_object_storage() -> anyhow::Result> { - object_storage::create() +pub fn build_object_storage( + config: &config::StorageConfig, +) -> anyhow::Result> { + object_storage::create(config) } pub fn build_profile_fields_repo( diff --git a/crates/composition/src/tests/build.rs b/crates/composition/src/tests/build.rs index e0746cf4..4cfc62d3 100644 --- a/crates/composition/src/tests/build.rs +++ b/crates/composition/src/tests/build.rs @@ -346,6 +346,8 @@ fn test_config() -> AppConfig { base_url: "http://localhost:3000".into(), rate_limit: 20, refresh_ttl_seconds: 2_592_000, + secure_cookies: false, + cors_origins: String::new(), wrapup: infra_wiring::WrapUpConfig { font_path: None, logo_path: None, diff --git a/crates/infra-wiring/Cargo.toml b/crates/infra-wiring/Cargo.toml index 228f8686..8a96e9b5 100644 --- a/crates/infra-wiring/Cargo.toml +++ b/crates/infra-wiring/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] +config = { workspace = true } sqlx = { workspace = true } anyhow = { workspace = true } diff --git a/crates/infra-wiring/src/config.rs b/crates/infra-wiring/src/config.rs index 75d1eb96..101cad4b 100644 --- a/crates/infra-wiring/src/config.rs +++ b/crates/infra-wiring/src/config.rs @@ -4,6 +4,8 @@ pub struct AppConfig { pub base_url: String, pub rate_limit: u64, pub refresh_ttl_seconds: u64, + pub secure_cookies: bool, + pub cors_origins: String, pub wrapup: WrapUpConfig, } @@ -14,39 +16,26 @@ pub struct WrapUpConfig { pub bg_dir: Option, } -impl AppConfig { - pub fn from_env() -> Self { - let allow_registration = std::env::var("ALLOW_REGISTRATION") - .map(|v| v == "true" || v == "1") - .unwrap_or(false); - let base_url = std::env::var("BASE_URL") - .unwrap_or_else(|_| "http://localhost:3000".to_string()) - .trim_end_matches('/') - .to_string(); - let rate_limit = std::env::var("RATE_LIMIT") - .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(60); - let refresh_ttl_seconds = std::env::var("REFRESH_TTL_SECONDS") - .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(2_592_000u64); +impl From<&::config::AppConfig> for AppConfig { + fn from(config: &::config::AppConfig) -> Self { Self { - allow_registration, - base_url, - rate_limit, - refresh_ttl_seconds, - wrapup: WrapUpConfig::from_env(), + allow_registration: config.server.allow_registration, + base_url: config.server.base_url.clone(), + rate_limit: config.server.rate_limit, + refresh_ttl_seconds: config.auth.refresh_ttl_seconds, + secure_cookies: config.server.secure_cookies, + cors_origins: config.server.cors_origins.clone(), + wrapup: WrapUpConfig::from(&config.wrapup), } } } -impl WrapUpConfig { - pub fn from_env() -> Self { +impl From<&::config::WrapUpConfig> for WrapUpConfig { + fn from(config: &::config::WrapUpConfig) -> Self { Self { - font_path: std::env::var("WRAPUP_FONT_PATH").ok(), - logo_path: std::env::var("WRAPUP_LOGO_PATH").ok(), - bg_dir: std::env::var("WRAPUP_BG_DIR").ok(), + font_path: config.font_path.clone(), + logo_path: config.logo_path.clone(), + bg_dir: config.bg_dir.clone(), } } } diff --git a/crates/infra-wiring/src/lib.rs b/crates/infra-wiring/src/lib.rs index b4320c09..a0205078 100644 --- a/crates/infra-wiring/src/lib.rs +++ b/crates/infra-wiring/src/lib.rs @@ -14,11 +14,8 @@ pub enum EventBusBackend { } impl EventBusBackend { - pub fn from_env() -> anyhow::Result { - match std::env::var("EVENT_BUS_BACKEND") - .unwrap_or_else(|_| "db".to_string()) - .as_str() - { + pub fn from_config(config: &::config::EventsConfig) -> anyhow::Result { + match config.bus_backend.as_str() { "db" => Ok(Self::Db), #[cfg(feature = "nats")] "nats" => Ok(Self::Nats), diff --git a/crates/presentation/src/csrf.rs b/crates/presentation/src/csrf.rs index 70464fd4..aa607835 100644 --- a/crates/presentation/src/csrf.rs +++ b/crates/presentation/src/csrf.rs @@ -1,5 +1,5 @@ use axum::{ - extract::Request, + extract::{Request, State}, http::{HeaderValue, header}, middleware::Next, response::Response, @@ -19,15 +19,19 @@ pub fn extract_from_cookie(headers: &axum::http::HeaderMap) -> Option { }) } -pub(crate) fn secure_flag() -> &'static str { - if std::env::var("SECURE_COOKIES").as_deref() == Ok("true") { - "; Secure" - } else { - "" - } +/// Router state for [`csrf_middleware`]: whether cookies carry the `Secure` attribute. +#[derive(Clone, Copy)] +pub struct SecureCookies(pub bool); + +pub(crate) fn secure_flag(secure_cookies: bool) -> &'static str { + if secure_cookies { "; Secure" } else { "" } } -pub async fn csrf_middleware(mut req: Request, next: Next) -> Response { +pub async fn csrf_middleware( + State(SecureCookies(secure_cookies)): State, + mut req: Request, + next: Next, +) -> Response { let existing = extract_from_cookie(req.headers()); let (token, needs_set) = match existing { Some(t) => (t, false), @@ -42,7 +46,7 @@ pub async fn csrf_middleware(mut req: Request, next: Next) -> Response { let cookie = format!( "csrf={}; HttpOnly; Path=/; SameSite=Strict{}", token, - secure_flag() + secure_flag(secure_cookies) ); if let Ok(val) = HeaderValue::from_str(&cookie) { response.headers_mut().append(header::SET_COOKIE, val); diff --git a/crates/presentation/src/handlers/auth.rs b/crates/presentation/src/handlers/auth.rs index 89127aaa..0cc2a4e4 100644 --- a/crates/presentation/src/handlers/auth.rs +++ b/crates/presentation/src/handlers/auth.rs @@ -25,12 +25,16 @@ use template_askama::{LoginTemplate, RegisterTemplate}; // ── HTML helpers ───────────────────────────────────────────────────────────── -fn set_cookie_header(token: &str, max_age: i64) -> (axum::http::HeaderName, HeaderValue) { +fn set_cookie_header( + token: &str, + max_age: i64, + secure_cookies: bool, +) -> (axum::http::HeaderName, HeaderValue) { let val = format!( "token={}; HttpOnly; Path=/; SameSite=Strict; Max-Age={}{}", token, max_age, - secure_flag() + secure_flag(secure_cookies) ); ( SET_COOKIE, @@ -177,17 +181,18 @@ pub async fn post_login( { Ok(result) => { let max_age = (result.expires_at - Utc::now()).num_seconds().max(0); - let cookie = set_cookie_header(&result.token, max_age); + let cookie = + set_cookie_header(&result.token, max_age, state.app_ctx.config.secure_cookies); ([cookie], Redirect::to("/")).into_response() } Err(_) => Redirect::to("/login?error=Invalid+credentials").into_response(), } } -pub async fn get_logout() -> impl IntoResponse { +pub async fn get_logout(State(state): State) -> impl IntoResponse { let val = format!( "token=; HttpOnly; Path=/; SameSite=Strict; Max-Age=0{}", - secure_flag() + secure_flag(state.app_ctx.config.secure_cookies) ); let cookie = ( SET_COOKIE, @@ -246,7 +251,8 @@ pub async fn post_register( { Ok(result) => { let max_age = (result.expires_at - Utc::now()).num_seconds().max(0); - let cookie = set_cookie_header(&result.token, max_age); + let cookie = + set_cookie_header(&result.token, max_age, state.app_ctx.config.secure_cookies); ([cookie], Redirect::to("/")).into_response() } Err(_) => { diff --git a/crates/presentation/src/routes.rs b/crates/presentation/src/routes.rs index 53d7295f..77e08d53 100644 --- a/crates/presentation/src/routes.rs +++ b/crates/presentation/src/routes.rs @@ -12,6 +12,8 @@ use crate::{handlers, state::AppState}; pub fn build_router(state: AppState, ap_router: Router) -> Router { let rate_limit = state.app_ctx.config.rate_limit; + let secure_cookies = state.app_ctx.config.secure_cookies; + let cors_origins = state.app_ctx.config.cors_origins.clone(); let ap_cfg = GovernorConfigBuilder::default() .with_extractor(PeerIp::default()) @@ -23,8 +25,8 @@ pub fn build_router(state: AppState, ap_router: Router) -> Router { Router::new() .route("/health", routing::get(health_handler)) - .merge(html_routes(rate_limit)) - .merge(api_routes(rate_limit)) + .merge(html_routes(rate_limit, secure_cookies)) + .merge(api_routes(rate_limit, &cors_origins)) .nest_service("/static", ServeDir::new("static")) .nest_service( "/app", @@ -44,7 +46,7 @@ fn per_minute(n: u64) -> Quota { Quota::requests_per_minute(n) } -fn html_routes(rate_limit: u64) -> Router { +fn html_routes(rate_limit: u64, secure_cookies: bool) -> Router { let auth = Router::new() .route( "/login", @@ -188,7 +190,10 @@ fn html_routes(rate_limit: u64) -> Router { #[cfg(feature = "federation")] let base = base.merge(federation_html_routes()); - base.layer(axum::middleware::from_fn(crate::csrf::csrf_middleware)) + base.layer(axum::middleware::from_fn_with_state( + crate::csrf::SecureCookies(secure_cookies), + crate::csrf::csrf_middleware, + )) } fn social_html_routes() -> Router { @@ -278,11 +283,10 @@ fn federation_html_routes() -> Router { ) } -fn cors_layer() -> CorsLayer { +fn cors_layer(origins: &str) -> CorsLayer { use axum::http::{HeaderName, Method}; use tower_http::cors::AllowOrigin; - let origins = std::env::var("CORS_ORIGINS").unwrap_or_default(); let layer = CorsLayer::new() .allow_methods([ Method::GET, @@ -319,7 +323,7 @@ fn cors_layer() -> CorsLayer { } } -fn api_routes(rate_limit: u64) -> Router { +fn api_routes(rate_limit: u64, cors_origins: &str) -> Router { let cfg = GovernorConfigBuilder::default() .with_extractor(PeerIp::default()) .expect_connect_info() @@ -506,7 +510,7 @@ fn api_routes(rate_limit: u64) -> Router { Router::new() .nest("/api/v1", base.layer(GovernorLayer::new(cfg))) .nest("/api/v1", webhook_routes) - .layer(cors_layer()) + .layer(cors_layer(cors_origins)) } fn social_api_routes() -> Router { diff --git a/crates/presentation/src/tests/extractors.rs b/crates/presentation/src/tests/extractors.rs index d6fc267b..152c72f3 100644 --- a/crates/presentation/src/tests/extractors.rs +++ b/crates/presentation/src/tests/extractors.rs @@ -843,6 +843,8 @@ pub(crate) fn build_test_state_from( base_url: "http://localhost:3000".to_string(), rate_limit: 20, refresh_ttl_seconds: 2_592_000, + secure_cookies: false, + cors_origins: String::new(), wrapup: application::config::WrapUpConfig { font_path: None, logo_path: None, diff --git a/crates/server/Cargo.toml b/crates/server/Cargo.toml index 89010c28..3004e56b 100644 --- a/crates/server/Cargo.toml +++ b/crates/server/Cargo.toml @@ -20,6 +20,7 @@ sqlite = [] sqlite-federation = ["federation"] [dependencies] +config = { workspace = true } axum = { workspace = true } tokio = { workspace = true } anyhow = { workspace = true } diff --git a/crates/server/src/main.rs b/crates/server/src/main.rs index 4d7138d0..fc39b4bc 100644 --- a/crates/server/src/main.rs +++ b/crates/server/src/main.rs @@ -5,7 +5,6 @@ use anyhow::Context; use tokio::net::TcpListener; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; -use application::config::AppConfig; use composition::Repositories; use export::ExportAdapter; use importer::ImporterDocumentParser; @@ -21,15 +20,15 @@ async fn main() -> anyhow::Result<()> { dotenvy::dotenv().ok(); init_tracing(); - let (state, ap_router) = wire_dependencies() + let config = config::load().context("Failed to load configuration")?; + + let (state, ap_router) = wire_dependencies(&config) .await .context("Failed to wire dependencies")?; let app = openapi::serve(routes::build_router(state, ap_router)); - let host = std::env::var("HOST").unwrap_or_else(|_| "0.0.0.0".to_string()); - let port = std::env::var("PORT").unwrap_or_else(|_| "3000".to_string()); - let addr = format!("{}:{}", host, port); + let addr = format!("{}:{}", config.server.host, config.server.port); let listener = TcpListener::bind(&addr).await?; tracing::info!("Listening on {}", addr); axum::serve( @@ -41,24 +40,24 @@ async fn main() -> anyhow::Result<()> { Ok(()) } -async fn wire_dependencies() -> anyhow::Result<(AppState, axum::Router)> { - let app_config = AppConfig::from_env(); +async fn wire_dependencies(config: &config::AppConfig) -> anyhow::Result<(AppState, axum::Router)> { + let app_config = application::config::AppConfig::from(config); let instance = domain::value_objects::InstanceIdentity::new(app_config.base_url.clone()); - let database_url = std::env::var("DATABASE_URL").context("DATABASE_URL must be set")?; + let database_url = config.database.url.as_str(); composition::factory::warn_if_database_backend_set(); - let (auth_service, password_hasher) = composition::factory::build_auth_adapters()?; - let metadata_client = composition::factory::build_metadata_client()?; - let poster_fetcher = composition::factory::build_poster_fetcher()?; - let object_storage = composition::factory::build_object_storage()?; + let (auth_service, password_hasher) = composition::factory::build_auth_adapters(&config.auth)?; + let metadata_client = composition::factory::build_metadata_client(&config.metadata)?; + let poster_fetcher = composition::factory::build_poster_fetcher(&config.metadata)?; + let object_storage = composition::factory::build_object_storage(&config.storage)?; - let db = composition::factory::build_database_adapters(&database_url, &instance).await?; + let db = composition::factory::build_database_adapters(database_url, &instance).await?; #[cfg(feature = "federation")] let ap_content_repo = db.ap_content; let db_pool = db.db_pool; // Wire up event channel, federation service, and ap_router - let event_bus = EventBusBackend::from_env()?; + let event_bus = EventBusBackend::from_config(&config.events)?; #[cfg(feature = "federation")] let ( @@ -78,7 +77,7 @@ async fn wire_dependencies() -> anyhow::Result<(AppState, axum::Router)> { } }; - let ep = create_event_publisher(event_bus, &db_pool).await?; + let ep = create_event_publisher(event_bus, &db_pool, config).await?; let ap = activitypub::wire(activitypub::ActivityPubDeps { activity_repo: fed_repos.activity, @@ -100,6 +99,7 @@ async fn wire_dependencies() -> anyhow::Result<(AppState, axum::Router)> { follow_query: Arc::clone(&fed_repos.follow_query), instance: instance.clone(), allow_registration: app_config.allow_registration, + federation: config.federation.clone(), event_publisher: Arc::clone(&ep), }) .await?; @@ -136,7 +136,7 @@ async fn wire_dependencies() -> anyhow::Result<(AppState, axum::Router)> { }; #[cfg(not(feature = "federation"))] - let event_publisher_arc = create_event_publisher(event_bus, &db_pool).await?; + let event_publisher_arc = create_event_publisher(event_bus, &db_pool, config).await?; #[cfg(not(feature = "federation"))] let ap_router = axum::Router::new(); #[cfg(not(feature = "federation"))] @@ -252,9 +252,7 @@ async fn wire_dependencies() -> anyhow::Result<(AppState, axum::Router)> { let state = AppState { app_ctx, - rss_renderer: Arc::new(RssAdapter::new( - std::env::var("BASE_URL").unwrap_or_else(|_| "http://localhost:3000".into()), - )), + rss_renderer: Arc::new(RssAdapter::new(config.server.base_url.clone())), }; Ok((state, ap_router)) } @@ -262,19 +260,20 @@ async fn wire_dependencies() -> anyhow::Result<(AppState, axum::Router)> { async fn create_event_publisher( event_bus: EventBusBackend, db_pool: &composition::DbPool, + config: &config::AppConfig, ) -> anyhow::Result> { match event_bus { EventBusBackend::Db => { tracing::info!("event bus: DB queue"); Ok(match db_pool { composition::DbPool::Sqlite(pool) => { - sqlite::SqliteEventQueue::create_publisher(pool.clone()).await? + sqlite::SqliteEventQueue::create_publisher(pool.clone(), &config.events).await? } }) } #[cfg(feature = "nats")] EventBusBackend::Nats => { - let cfg = nats::NatsConfig::from_env() + let cfg = nats::NatsConfig::from_config(&config.nats) .context("EVENT_BUS_BACKEND=nats requires NATS_URL to be set")?; tracing::info!("event bus: NATS ({})", cfg.url); Ok(nats::create_publisher(cfg).await?) diff --git a/crates/server/tests/api_test.rs b/crates/server/tests/api_test.rs index 0ff2a4f9..de982935 100644 --- a/crates/server/tests/api_test.rs +++ b/crates/server/tests/api_test.rs @@ -450,6 +450,8 @@ async fn test_app_with(auth: impl AuthService + 'static) -> (Router, SqlitePool) base_url: "http://localhost:3000".to_string(), rate_limit: 20, refresh_ttl_seconds: 2_592_000, + secure_cookies: false, + cors_origins: String::new(), wrapup: application::config::WrapUpConfig { font_path: None, logo_path: None, diff --git a/crates/worker/Cargo.toml b/crates/worker/Cargo.toml index 002647b6..6a1f7ab7 100644 --- a/crates/worker/Cargo.toml +++ b/crates/worker/Cargo.toml @@ -17,6 +17,7 @@ sqlite = [] sqlite-federation = ["federation"] [dependencies] +config = { workspace = true } domain = { workspace = true } application = { workspace = true } composition = { workspace = true } diff --git a/crates/worker/src/event_bus.rs b/crates/worker/src/event_bus.rs index 35814ee6..244c5cd3 100644 --- a/crates/worker/src/event_bus.rs +++ b/crates/worker/src/event_bus.rs @@ -8,19 +8,22 @@ use infra_wiring::{DbPool, EventBusBackend}; pub async fn create( db_pool: &DbPool, + config: &config::AppConfig, ) -> anyhow::Result<(Arc, Arc)> { - match EventBusBackend::from_env()? { + match EventBusBackend::from_config(&config.events)? { EventBusBackend::Db => { tracing::info!("event bus: DB queue"); match db_pool { - DbPool::Sqlite(pool) => { - Ok(sqlite::SqliteEventQueue::create_channel(pool.clone()).await?) - } + DbPool::Sqlite(pool) => Ok(sqlite::SqliteEventQueue::create_channel( + pool.clone(), + &config.events, + ) + .await?), } } #[cfg(feature = "nats")] EventBusBackend::Nats => { - let cfg = nats::NatsConfig::from_env() + let cfg = nats::NatsConfig::from_config(&config.nats) .context("EVENT_BUS_BACKEND=nats requires NATS_URL to be set")?; tracing::info!("event bus: NATS ({})", cfg.url); Ok(nats::create_channel(cfg).await?) diff --git a/crates/worker/src/main.rs b/crates/worker/src/main.rs index b20cea59..f1027431 100644 --- a/crates/worker/src/main.rs +++ b/crates/worker/src/main.rs @@ -6,8 +6,7 @@ use std::sync::Arc; use anyhow::Context; use application::{ - MovieDiscoveryIndexer, SearchCleanupHandler, SearchReindexHandler, config::AppConfig, - worker::WorkerService, + MovieDiscoveryIndexer, SearchCleanupHandler, SearchReindexHandler, worker::WorkerService, }; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; @@ -18,18 +17,18 @@ async fn main() -> anyhow::Result<()> { dotenvy::dotenv().ok(); init_tracing(); - let database_url = std::env::var("DATABASE_URL").context("DATABASE_URL must be set")?; + let config = config::load().context("Failed to load configuration")?; composition::factory::warn_if_database_backend_set(); - let app_config = AppConfig::from_env(); + let app_config = application::config::AppConfig::from(&config); let instance = domain::value_objects::InstanceIdentity::new(app_config.base_url.clone()); - let metadata_client = metadata::create()?; - let poster_fetcher = poster_fetcher::create()?; - let object_storage = object_storage::create()?; + let metadata_client = metadata::create(&config.metadata)?; + let poster_fetcher = poster_fetcher::create(&config.metadata)?; + let object_storage = object_storage::create(&config.storage)?; // TMDb client detection happens before `build_worker_deps` so the optional // person-enrichment port can be handed to `WorkerServices` up front. - let tmdb_client = match tmdb_enrichment::TmdbEnrichmentClient::from_env() { + let tmdb_client = match tmdb_enrichment::TmdbEnrichmentClient::from_config(&config.metadata) { Ok(client) => { tracing::info!("TMDb enrichment enabled"); Some(Arc::new(client)) @@ -43,8 +42,8 @@ async fn main() -> anyhow::Result<()> { .clone() .map(|c| c as Arc); - let db = composition::factory::build_database_adapters(&database_url, &instance).await?; - let (event_publisher_arc, consumer_arc) = event_bus::create(&db.db_pool).await?; + let db = composition::factory::build_database_adapters(&config.database.url, &instance).await?; + let (event_publisher_arc, consumer_arc) = event_bus::create(&db.db_pool, &config).await?; let worker_services = application::WorkerServices { object_storage: Arc::clone(&object_storage), @@ -113,7 +112,7 @@ async fn main() -> anyhow::Result<()> { let (enrichment_handler, person_enrichment_handler, enrichment_job): EnrichmentParts = match tmdb_client { Some(client) => { - let image_fetcher = poster_fetcher::create_image_fetcher()?; + let image_fetcher = poster_fetcher::create_image_fetcher(&config.metadata)?; let handler = Arc::new(application::movies::MovieEnrichmentHandler::new( Arc::clone(&client) as Arc, worker_deps.enrich_movie.movie_query.clone(), @@ -140,6 +139,7 @@ async fn main() -> anyhow::Result<()> { // ── Image conversion ────────────────────────────────────────────────────── let conversion = image_converter::build( + &config.image_conversion, Arc::clone(&object_storage), image_ref_command, image_ref_query, @@ -257,6 +257,7 @@ async fn main() -> anyhow::Result<()> { follow_query: fed_repos.follow_query, instance: instance.clone(), allow_registration, + federation: config.federation.clone(), event_publisher: Arc::clone(&event_publisher), federation_settings: std::sync::Arc::clone(&db.federation_settings), })