use std::{env, net::SocketAddr}; // 集中管理 api-server 的启动配置,避免入口层直接散落环境变量解析逻辑。 #[derive(Clone, Debug)] pub struct AppConfig { pub bind_host: String, pub bind_port: u16, pub log_filter: String, pub jwt_issuer: String, pub jwt_secret: String, pub jwt_access_token_ttl_seconds: u64, pub refresh_cookie_name: String, pub refresh_cookie_path: String, pub refresh_cookie_secure: bool, pub refresh_cookie_same_site: String, pub refresh_session_ttl_days: u32, pub sms_auth_enabled: bool, pub wechat_auth_enabled: bool, pub oss_bucket: Option, pub oss_endpoint: Option, pub oss_access_key_id: Option, pub oss_access_key_secret: Option, pub oss_public_base_url: Option, pub oss_post_expire_seconds: u64, pub oss_post_max_size_bytes: u64, pub oss_success_action_status: u16, } impl Default for AppConfig { fn default() -> Self { Self { bind_host: "127.0.0.1".to_string(), bind_port: 3000, log_filter: "info,tower_http=info".to_string(), jwt_issuer: "https://auth.genarrative.local".to_string(), jwt_secret: "genarrative-dev-secret".to_string(), jwt_access_token_ttl_seconds: 2 * 60 * 60, refresh_cookie_name: "genarrative_refresh_session".to_string(), refresh_cookie_path: "/api/auth".to_string(), refresh_cookie_secure: false, refresh_cookie_same_site: "Lax".to_string(), refresh_session_ttl_days: 30, sms_auth_enabled: false, wechat_auth_enabled: false, oss_bucket: None, oss_endpoint: None, oss_access_key_id: None, oss_access_key_secret: None, oss_public_base_url: None, oss_post_expire_seconds: 10 * 60, oss_post_max_size_bytes: 20 * 1024 * 1024, oss_success_action_status: 200, } } } impl AppConfig { pub fn from_env() -> Self { let mut config = Self::default(); if let Ok(bind_host) = env::var("GENARRATIVE_API_HOST") && !bind_host.trim().is_empty() { config.bind_host = bind_host; } if let Ok(bind_port) = env::var("GENARRATIVE_API_PORT") && let Ok(parsed_port) = bind_port.parse::() { config.bind_port = parsed_port; } if let Ok(log_filter) = env::var("GENARRATIVE_API_LOG") && !log_filter.trim().is_empty() { config.log_filter = log_filter; } if let Some(jwt_issuer) = read_first_non_empty_env(&["GENARRATIVE_JWT_ISSUER", "JWT_ISSUER"]) { config.jwt_issuer = jwt_issuer; } if let Some(jwt_secret) = read_first_non_empty_env(&["GENARRATIVE_JWT_SECRET", "JWT_SECRET"]) { config.jwt_secret = jwt_secret; } if let Some(ttl_seconds) = read_first_duration_seconds_env(&[ "GENARRATIVE_JWT_ACCESS_TOKEN_TTL_SECONDS", "JWT_EXPIRES_IN", ]) { config.jwt_access_token_ttl_seconds = ttl_seconds; } if let Some(refresh_cookie_name) = read_first_non_empty_env(&["AUTH_REFRESH_COOKIE_NAME"]) { config.refresh_cookie_name = refresh_cookie_name; } if let Some(refresh_cookie_path) = read_first_non_empty_env(&["AUTH_REFRESH_COOKIE_PATH"]) { config.refresh_cookie_path = refresh_cookie_path; } if let Some(refresh_cookie_same_site) = read_first_non_empty_env(&["AUTH_REFRESH_COOKIE_SAME_SITE"]) { config.refresh_cookie_same_site = refresh_cookie_same_site; } if let Some(refresh_cookie_secure) = read_first_bool_env(&["AUTH_REFRESH_COOKIE_SECURE"]) { config.refresh_cookie_secure = refresh_cookie_secure; } if let Some(refresh_session_ttl_days) = read_first_positive_u32_env(&["AUTH_REFRESH_SESSION_TTL_DAYS"]) { config.refresh_session_ttl_days = refresh_session_ttl_days; } if let Some(sms_auth_enabled) = read_first_bool_env(&["SMS_AUTH_ENABLED"]) { config.sms_auth_enabled = sms_auth_enabled; } if let Some(wechat_auth_enabled) = read_first_bool_env(&["WECHAT_AUTH_ENABLED"]) { config.wechat_auth_enabled = wechat_auth_enabled; } config.oss_bucket = read_first_non_empty_env(&["ALIYUN_OSS_BUCKET"]); config.oss_endpoint = read_first_non_empty_env(&["ALIYUN_OSS_ENDPOINT"]); config.oss_access_key_id = read_first_non_empty_env(&["ALIYUN_OSS_ACCESS_KEY_ID"]); config.oss_access_key_secret = read_first_non_empty_env(&["ALIYUN_OSS_ACCESS_KEY_SECRET"]); config.oss_public_base_url = read_first_non_empty_env(&["ALIYUN_OSS_PUBLIC_BASE_URL"]); if let Some(oss_post_expire_seconds) = read_first_duration_seconds_env(&["ALIYUN_OSS_POST_EXPIRE_SECONDS"]) { config.oss_post_expire_seconds = oss_post_expire_seconds; } if let Some(oss_post_max_size_bytes) = read_first_positive_u64_env(&["ALIYUN_OSS_POST_MAX_SIZE_BYTES"]) { config.oss_post_max_size_bytes = oss_post_max_size_bytes; } if let Some(oss_success_action_status) = read_first_positive_u16_env(&["ALIYUN_OSS_SUCCESS_ACTION_STATUS"]) { config.oss_success_action_status = oss_success_action_status; } config } pub fn bind_socket_addr(&self) -> SocketAddr { let address = format!("{}:{}", self.bind_host, self.bind_port); address .parse() .unwrap_or_else(|_| SocketAddr::from(([127, 0, 0, 1], 3000))) } } fn read_first_non_empty_env(keys: &[&str]) -> Option { keys.iter().find_map(|key| { env::var(key).ok().and_then(|value| { let value = value.trim().to_string(); if value.is_empty() { return None; } Some(value) }) }) } fn read_first_duration_seconds_env(keys: &[&str]) -> Option { keys.iter().find_map(|key| { env::var(key) .ok() .and_then(|value| parse_duration_seconds(&value)) }) } fn read_first_bool_env(keys: &[&str]) -> Option { keys.iter() .find_map(|key| env::var(key).ok().and_then(|value| parse_bool(&value))) } fn read_first_positive_u32_env(keys: &[&str]) -> Option { keys.iter().find_map(|key| { env::var(key) .ok() .and_then(|value| parse_positive_u32(&value)) }) } fn read_first_positive_u64_env(keys: &[&str]) -> Option { keys.iter().find_map(|key| { env::var(key) .ok() .and_then(|value| parse_positive_u64(&value)) }) } fn read_first_positive_u16_env(keys: &[&str]) -> Option { keys.iter().find_map(|key| { env::var(key) .ok() .and_then(|value| parse_positive_u16(&value)) }) } fn parse_duration_seconds(raw: &str) -> Option { let raw = raw.trim(); if raw.is_empty() { return None; } if let Ok(seconds) = raw.parse::() { return Some(seconds); } let (number, unit) = raw.split_at(raw.len().checked_sub(1)?); let unit = unit.to_ascii_lowercase(); let number = number.trim().parse::().ok()?; let multiplier = match unit.as_str() { "s" => 1, "m" => 60, "h" => 60 * 60, "d" => 24 * 60 * 60, _ => return None, }; number.checked_mul(multiplier) } fn parse_bool(raw: &str) -> Option { match raw.trim().to_ascii_lowercase().as_str() { "1" | "true" | "yes" | "on" => Some(true), "0" | "false" | "no" | "off" => Some(false), _ => None, } } fn parse_positive_u32(raw: &str) -> Option { let value = raw.trim().parse::().ok()?; if value == 0 { return None; } Some(value) } fn parse_positive_u64(raw: &str) -> Option { let value = raw.trim().parse::().ok()?; if value == 0 { return None; } Some(value) } fn parse_positive_u16(raw: &str) -> Option { let value = raw.trim().parse::().ok()?; if value == 0 { return None; } Some(value) }