use crate::error::{AppError, ErrorCode}; static TRUST_PROXY_HEADERS: std::sync::OnceLock = std::sync::OnceLock::new(); #[derive(Debug, Clone)] pub struct Config { pub role: String, pub host: String, pub port: u16, pub public_base_url: String, pub database_url: String, pub database_max_connections: u32, pub redis_url: String, pub worker_concurrency: u32, pub image_processing_concurrency: u32, pub jwt_secret: String, pub jwt_expiry_hours: i64, pub api_key_pepper: String, pub stripe_secret_key: Option, pub stripe_webhook_secret: Option, pub storage_path: String, pub allow_anonymous_upload: bool, pub anon_max_file_size_mb: u64, pub anon_max_files_per_batch: u32, pub anon_daily_units: u32, pub anon_retention_hours: u64, pub max_image_pixels: u64, pub idempotency_ttl_hours: u64, pub mail_enabled: bool, pub mail_log_links_when_disabled: bool, pub mail_provider: String, pub mail_from: String, pub mail_password: String, pub mail_from_name: String, pub mail_smtp_host: Option, pub mail_smtp_port: Option, pub mail_smtp_encryption: Option, } impl Config { pub fn from_env() -> Result { let role = env_string("IMAGEFORGE_ROLE").unwrap_or_else(|| "api".to_string()); let host = env_string("HOST").unwrap_or_else(|| "0.0.0.0".to_string()); let port = env_u16("PORT").unwrap_or(8080); let public_base_url = env_string("PUBLIC_BASE_URL").unwrap_or_else(|| "http://localhost:8080".to_string()); let database_url = env_string("DATABASE_URL") .ok_or_else(|| AppError::new(ErrorCode::InvalidRequest, "缺少环境变量 DATABASE_URL"))?; let database_max_connections = env_u32("DATABASE_MAX_CONNECTIONS").unwrap_or(10); let redis_url = env_string("REDIS_URL") .ok_or_else(|| AppError::new(ErrorCode::InvalidRequest, "缺少环境变量 REDIS_URL"))?; let worker_concurrency = env_u32("WORKER_CONCURRENCY").unwrap_or_else(|| { std::thread::available_parallelism() .map(|v| v.get() as u32) .unwrap_or(4) }); let image_processing_concurrency = env_u32("IMAGE_PROCESSING_CONCURRENCY") .filter(|value| *value > 0) .unwrap_or_else(|| { std::thread::available_parallelism() .map(|v| v.get() as u32) .unwrap_or(4) }); let jwt_secret = env_string("JWT_SECRET") .ok_or_else(|| AppError::new(ErrorCode::InvalidRequest, "缺少环境变量 JWT_SECRET"))?; let jwt_expiry_hours = env_i64("JWT_EXPIRY_HOURS").unwrap_or(168); let api_key_pepper = env_string("API_KEY_PEPPER").ok_or_else(|| { AppError::new(ErrorCode::InvalidRequest, "缺少环境变量 API_KEY_PEPPER") })?; let billing_provider = env_string("BILLING_PROVIDER").unwrap_or_else(|| "stripe".to_string()); if !billing_provider.eq_ignore_ascii_case("stripe") { return Err(AppError::new( ErrorCode::InvalidRequest, "BILLING_PROVIDER 目前仅支持 stripe", )); } let stripe_secret_key = env_string("STRIPE_SECRET_KEY"); let stripe_webhook_secret = env_string("STRIPE_WEBHOOK_SECRET"); let storage_path = env_string("STORAGE_PATH").unwrap_or_else(|| "./uploads".to_string()); let allow_anonymous_upload = env_bool("ALLOW_ANONYMOUS_UPLOAD").unwrap_or(true); let anon_max_file_size_mb = env_u64("ANON_MAX_FILE_SIZE_MB").unwrap_or(5); let anon_max_files_per_batch = env_u32("ANON_MAX_FILES_PER_BATCH").unwrap_or(5); let anon_daily_units = env_u32("ANON_DAILY_UNITS").unwrap_or(10); let anon_retention_hours = env_u64("ANON_RETENTION_HOURS").unwrap_or(24); let max_image_pixels = env_u64("MAX_IMAGE_PIXELS").unwrap_or(40_000_000); let idempotency_ttl_hours = env_u64("IDEMPOTENCY_TTL_HOURS").unwrap_or(24); let mail_enabled = env_bool("MAIL_ENABLED").unwrap_or(false); let mail_log_links_when_disabled = env_bool("MAIL_LOG_LINKS_WHEN_DISABLED").unwrap_or(false); let mail_provider = env_string("MAIL_PROVIDER").unwrap_or_else(|| "qq".to_string()); let mail_from = env_string("MAIL_FROM").unwrap_or_else(|| "noreply@example.com".to_string()); let mail_password = env_string("MAIL_PASSWORD").unwrap_or_default(); let mail_from_name = env_string("MAIL_FROM_NAME").unwrap_or_else(|| "ImageForge".to_string()); let mail_smtp_host = env_string("MAIL_SMTP_HOST"); let mail_smtp_port = env_u16("MAIL_SMTP_PORT"); let mail_smtp_encryption = env_string("MAIL_SMTP_ENCRYPTION"); Ok(Self { role, host, port, public_base_url, database_url, database_max_connections, redis_url, worker_concurrency, image_processing_concurrency, jwt_secret, jwt_expiry_hours, api_key_pepper, stripe_secret_key, stripe_webhook_secret, storage_path, allow_anonymous_upload, anon_max_file_size_mb, anon_max_files_per_batch, anon_daily_units, anon_retention_hours, max_image_pixels, idempotency_ttl_hours, mail_enabled, mail_log_links_when_disabled, mail_provider, mail_from, mail_password, mail_from_name, mail_smtp_host, mail_smtp_port, mail_smtp_encryption, }) } } fn env_string(key: &str) -> Option { std::env::var(key) .ok() .filter(|value| !value.trim().is_empty()) } fn env_u16(key: &str) -> Option { env_string(key).and_then(|v| v.parse::().ok()) } fn env_u32(key: &str) -> Option { env_string(key).and_then(|v| v.parse::().ok()) } fn env_i64(key: &str) -> Option { env_string(key).and_then(|v| v.parse::().ok()) } fn env_u64(key: &str) -> Option { env_string(key).and_then(|v| v.parse::().ok()) } fn env_bool(key: &str) -> Option { env_string(key).and_then(|v| match v.trim().to_ascii_lowercase().as_str() { "1" | "true" | "yes" | "y" | "on" => Some(true), "0" | "false" | "no" | "n" | "off" => Some(false), _ => None, }) } pub fn trust_proxy_headers() -> bool { *TRUST_PROXY_HEADERS.get_or_init(|| env_bool("TRUST_PROXY_HEADERS").unwrap_or(false)) }