use std::path::PathBuf; use anyhow::{anyhow, Context, Result}; /// Well-known dev JWT secret shipped in `.env.example`. If APP_ENV=production /// we refuse to start with this value; otherwise we warn loudly. const DEV_JWT_SECRET_SENTINEL: &str = "dev_secret_do_not_use_in_production_32byteslong_aaaa"; #[derive(Clone, Debug)] pub struct AppConfig { pub database_url: String, pub jwt_secret: String, pub session_expiry_days: i64, pub admin_password_hash: String, pub event_name: String, pub event_slug: String, pub media_path: PathBuf, pub app_port: u16, } impl AppConfig { pub fn from_env() -> Result { let app_env = std::env::var("APP_ENV").unwrap_or_else(|_| "development".to_string()); let is_prod = app_env.eq_ignore_ascii_case("production"); let jwt_secret = std::env::var("JWT_SECRET").context("JWT_SECRET must be set")?; if jwt_secret == DEV_JWT_SECRET_SENTINEL { if is_prod { return Err(anyhow!( "Refusing to start in production with the well-known dev JWT_SECRET — \ rotate it (openssl rand -hex 64)." )); } tracing::warn!( "JWT_SECRET is the dev sentinel — fine for local development, NEVER ship this." ); } else if jwt_secret.len() < 32 { return Err(anyhow!("JWT_SECRET must be at least 32 characters.")); } Ok(Self { database_url: std::env::var("DATABASE_URL") .context("DATABASE_URL must be set")?, jwt_secret, session_expiry_days: std::env::var("SESSION_EXPIRY_DAYS") .unwrap_or_else(|_| "30".to_string()) .parse() .context("SESSION_EXPIRY_DAYS must be a number")?, admin_password_hash: std::env::var("ADMIN_PASSWORD_HASH") .unwrap_or_default(), event_name: std::env::var("EVENT_NAME") .unwrap_or_else(|_| "EventSnap".to_string()), event_slug: std::env::var("EVENT_SLUG") .context("EVENT_SLUG must be set")?, media_path: PathBuf::from( std::env::var("MEDIA_PATH").unwrap_or_else(|_| "/media".to_string()), ), app_port: std::env::var("APP_PORT") .unwrap_or_else(|_| "3000".to_string()) .parse() .context("APP_PORT must be a number")?, }) } }