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")?; // A weak/placeholder signing key lets anyone forge any token (including // admin). Detect the known dev sentinel, the `.env.example` placeholder, // and anything that smells like an unreplaced template value. let lower = jwt_secret.to_ascii_lowercase(); let looks_placeholder = jwt_secret == DEV_JWT_SECRET_SENTINEL || lower.contains("change") || lower.contains("example") || lower.contains("placeholder") || lower.contains("replace"); if is_prod { // Production must use a real, strong secret — no placeholders, ≥64 // chars (an `openssl rand -hex 64` is 128 hex chars). if looks_placeholder || jwt_secret.len() < 64 { return Err(anyhow!( "Refusing to start in production: JWT_SECRET is a placeholder or too short. \ Generate a real one (openssl rand -hex 64) and set it in the prod environment." )); } } else if looks_placeholder || jwt_secret.len() < 32 { tracing::warn!( "JWT_SECRET looks like a dev/placeholder value — fine for local development, \ NEVER ship this to production." ); } 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")?, }) } }