From 94578475ea85f8aadd30d085cf7f38aa75b7b0b1 Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Tue, 28 Oct 2025 02:52:41 +0100 Subject: [PATCH] appconfig --- backend/Cargo.lock | 42 ++++++++++ backend/Cargo.toml | 2 + backend/src/config.rs | 173 +++++++++++++++++++++++------------------- 3 files changed, 140 insertions(+), 77 deletions(-) diff --git a/backend/Cargo.lock b/backend/Cargo.lock index ab266a7..a213662 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -611,6 +611,7 @@ dependencies = [ "diesel", "diesel_migrations", "dotenv", + "envy", "futures-util", "hex", "http-body-util", @@ -625,6 +626,7 @@ dependencies = [ "rand 0.8.5", "reqwest", "serde", + "serde-aux", "serde_json", "sha2", "tempfile", @@ -1175,6 +1177,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "envy" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f47e0157f2cb54f5ae1bd371b30a2ae4311e1c028f575cd4e81de7353215965" +dependencies = [ + "serde", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -2160,6 +2171,15 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + [[package]] name = "outref" version = "0.5.2" @@ -2904,6 +2924,28 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-aux" +version = "4.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "207f67b28fe90fb596503a9bf0bf1ea5e831e21307658e177c5dfcdfc3ab8a0a" +dependencies = [ + "chrono", + "serde", + "serde-value", + "serde_json", +] + +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + [[package]] name = "serde_core" version = "1.0.228" diff --git a/backend/Cargo.toml b/backend/Cargo.toml index aab422b..2bc86bb 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -25,6 +25,8 @@ aws-credential-types = "1.2" # Serialization serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +envy = "0.4" +serde-aux = "4.4" # Utilities tracing = "0.1" diff --git a/backend/src/config.rs b/backend/src/config.rs index 37c73f7..48ff6da 100644 --- a/backend/src/config.rs +++ b/backend/src/config.rs @@ -1,35 +1,60 @@ -use std::env; - use anyhow::{Context, Result}; use url::Url; +use serde::Deserialize; +use serde_aux::field_attributes::deserialize_bool_from_anything; + use crate::db::DEFAULT_MAX_POOL_SIZE; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Deserialize)] pub struct AppConfig { pub database_url: String, + #[serde(default = "default_database_max_pool_size")] pub database_max_pool_size: u32, + #[serde(default = "default_server_host")] pub server_host: String, + #[serde(default = "default_server_port")] pub server_port: u16, + #[serde(default = "default_webdav_host")] pub webdav_host: String, + #[serde(default = "default_webdav_port")] pub webdav_port: u16, pub jwt_secret: String, + #[serde(default = "default_jwt_issuer")] pub jwt_issuer: String, + #[serde(default = "default_jwt_audience")] pub jwt_audience: String, + #[serde(default = "default_jwt_expiry_minutes")] pub jwt_expiry_minutes: i64, + #[serde(default = "default_download_token_audience")] pub download_token_audience: String, + #[serde(default = "default_download_token_expiry_minutes")] pub download_token_expiry_minutes: i64, + #[serde(default = "default_refresh_token_expiry_days")] pub refresh_token_expiry_days: i64, + #[serde( + default = "default_refresh_cookie_secure", + deserialize_with = "deserialize_bool_from_anything" + )] pub refresh_cookie_secure: bool, + #[serde(default)] pub refresh_cookie_domain: Option, + #[serde(default)] pub cors_allowed_origin: Option, + #[serde(default)] pub aws_endpoint_url: Option, + #[serde(default)] pub aws_access_key_id: Option, + #[serde(default)] pub aws_secret_access_key: Option, + #[serde(default = "default_aws_region")] pub aws_region: String, pub s3_bucket: String, + #[serde(default)] pub quickwit_endpoint: Option, + #[serde(default)] pub quickwit_index: Option, + #[serde(default = "default_tenant_slug")] pub default_tenant_slug: String, } @@ -49,80 +74,9 @@ impl AppConfig { } pub fn from_env() -> Result { - let database_url = env::var("DATABASE_URL").context("DATABASE_URL must be set")?; - let database_max_pool_size = env::var("DATABASE_MAX_POOL_SIZE") - .ok() - .and_then(|value| value.parse().ok()) - .unwrap_or(DEFAULT_MAX_POOL_SIZE); - let server_host = env::var("SERVER_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()); - let server_port = env::var("SERVER_PORT") - .unwrap_or_else(|_| "3000".to_string()) - .parse() - .context("SERVER_PORT must be a valid u16")?; - let webdav_host = env::var("WEBDAV_HOST").unwrap_or_else(|_| server_host.clone()); - let webdav_port = env::var("WEBDAV_PORT") - .unwrap_or_else(|_| "3001".to_string()) - .parse() - .context("WEBDAV_PORT must be a valid u16")?; - let jwt_secret = env::var("JWT_SECRET").context("JWT_SECRET must be set")?; - let jwt_issuer = env::var("JWT_ISSUER").unwrap_or_else(|_| "papercrate".to_string()); - let jwt_audience = - env::var("JWT_AUDIENCE").unwrap_or_else(|_| "papercrate-clients".to_string()); - let jwt_expiry_minutes = env::var("JWT_EXPIRY_MINUTES") - .unwrap_or_else(|_| "60".to_string()) - .parse() - .context("JWT_EXPIRY_MINUTES must be an integer")?; - let download_token_audience = env::var("DOWNLOAD_TOKEN_AUDIENCE") - .unwrap_or_else(|_| "papercrate-download".to_string()); - let download_token_expiry_minutes = env::var("DOWNLOAD_TOKEN_EXPIRY_MINUTES") - .unwrap_or_else(|_| "60".to_string()) - .parse() - .context("DOWNLOAD_TOKEN_EXPIRY_MINUTES must be an integer")?; - let refresh_token_expiry_days = env::var("REFRESH_TOKEN_EXPIRY_DAYS") - .unwrap_or_else(|_| "30".to_string()) - .parse() - .context("REFRESH_TOKEN_EXPIRY_DAYS must be an integer")?; - let refresh_cookie_secure = env::var("REFRESH_COOKIE_SECURE") - .map(|v| v == "1" || v.eq_ignore_ascii_case("true")) - .unwrap_or(false); - let refresh_cookie_domain = env::var("REFRESH_COOKIE_DOMAIN").ok(); - let cors_allowed_origin = env::var("CORS_ALLOWED_ORIGIN").ok(); - let aws_endpoint_url = env::var("AWS_ENDPOINT_URL").ok(); - let aws_access_key_id = env::var("AWS_ACCESS_KEY_ID").ok(); - let aws_secret_access_key = env::var("AWS_SECRET_ACCESS_KEY").ok(); - let aws_region = env::var("AWS_REGION").unwrap_or_else(|_| "us-east-1".to_string()); - let s3_bucket = env::var("S3_BUCKET").context("S3_BUCKET must be set")?; - let quickwit_endpoint = env::var("QUICKWIT_ENDPOINT").ok(); - let quickwit_index = env::var("QUICKWIT_INDEX").ok(); - let default_tenant_slug = - env::var("DEFAULT_TENANT_SLUG").unwrap_or_else(|_| "admin".to_string()); - - Ok(Self { - database_url, - database_max_pool_size, - server_host, - server_port, - webdav_host, - webdav_port, - jwt_secret, - jwt_issuer, - jwt_audience, - jwt_expiry_minutes, - download_token_audience, - download_token_expiry_minutes, - refresh_token_expiry_days, - refresh_cookie_secure, - refresh_cookie_domain, - cors_allowed_origin, - aws_endpoint_url, - aws_access_key_id, - aws_secret_access_key, - aws_region, - s3_bucket, - quickwit_endpoint, - quickwit_index, - default_tenant_slug, - }) + let config: AppConfig = envy::from_env() + .context("failed to parse application configuration from environment")?; + Ok(config.normalize()) } pub fn redacted_database_url(&self) -> String { @@ -130,6 +84,71 @@ impl AppConfig { } } +impl AppConfig { + fn normalize(mut self) -> Self { + if self.webdav_host.is_empty() { + self.webdav_host = self.server_host.clone(); + } + self + } +} + +fn default_database_max_pool_size() -> u32 { + DEFAULT_MAX_POOL_SIZE +} + +fn default_server_host() -> String { + "127.0.0.1".to_string() +} + +fn default_server_port() -> u16 { + 3000 +} + +fn default_webdav_host() -> String { + String::new() +} + +fn default_webdav_port() -> u16 { + 3001 +} + +fn default_jwt_issuer() -> String { + "papercrate".to_string() +} + +fn default_jwt_audience() -> String { + "papercrate-clients".to_string() +} + +fn default_jwt_expiry_minutes() -> i64 { + 60 +} + +fn default_download_token_audience() -> String { + "papercrate-download".to_string() +} + +fn default_download_token_expiry_minutes() -> i64 { + 60 +} + +fn default_refresh_token_expiry_days() -> i64 { + 30 +} + +fn default_refresh_cookie_secure() -> bool { + false +} + +fn default_aws_region() -> String { + "us-east-1".to_string() +} + +fn default_tenant_slug() -> String { + "admin".to_string() +} + fn redact_database_url(raw: &str) -> String { match Url::parse(raw) { Ok(mut parsed) => {