use base64::Engine; use serde::Deserialize; pub const TOTP_KEY_MIN_BYTES: usize = 32; #[derive(Debug, Clone, Deserialize)] #[serde(default)] pub struct PasswordPolicy { pub min_length: usize, pub require_upper: bool, pub require_lower: bool, pub require_digit: bool, pub require_symbol: bool, } impl Default for PasswordPolicy { fn default() -> Self { Self { min_length: 8, require_upper: true, require_lower: true, require_digit: true, require_symbol: false, } } } #[derive(Debug, Clone, Default, Deserialize)] #[serde(default)] pub struct AgreementDocConfig { pub key: String, pub version: String, pub title: String, pub content_md: String, } #[derive(Debug, Clone, Deserialize)] #[serde(default)] pub struct AuthConfig { pub rpc_key: String, pub password_policy: PasswordPolicy, pub verify_code_ttl_secs: u64, pub verify_code_len: usize, pub reset_token_ttl_secs: u64, pub reset_token_bytes: usize, pub email_code_rate_limit_secs: u64, pub max_verify_attempts: u64, pub min_age: u8, pub totp_encryption_key: Option, pub agreements: Vec, } impl Default for AuthConfig { fn default() -> Self { Self { rpc_key: String::new(), password_policy: PasswordPolicy::default(), verify_code_ttl_secs: 600, verify_code_len: 6, reset_token_ttl_secs: 1800, reset_token_bytes: 32, email_code_rate_limit_secs: 60, max_verify_attempts: 5, min_age: 13, totp_encryption_key: None, agreements: vec![ AgreementDocConfig { key: "terms".to_string(), version: "0.1.0".to_string(), title: "Terms of Service".to_string(), content_md: "Placeholder terms of service.".to_string(), }, AgreementDocConfig { key: "privacy".to_string(), version: "0.1.0".to_string(), title: "Privacy Policy".to_string(), content_md: "Placeholder privacy policy.".to_string(), }, ], } } } impl AuthConfig { fn validate_common(&self) -> Result<(), String> { if !(4..=10).contains(&self.verify_code_len) { return Err("auth.verify_code_len must be between 4 and 10".to_string()); } if self.verify_code_ttl_secs == 0 { return Err("auth.verify_code_ttl_secs must be greater than 0".to_string()); } if self.reset_token_ttl_secs == 0 { return Err("auth.reset_token_ttl_secs must be greater than 0".to_string()); } if self.reset_token_bytes < 16 { return Err("auth.reset_token_bytes must be at least 16".to_string()); } if self.email_code_rate_limit_secs == 0 { return Err("auth.email_code_rate_limit_secs must be greater than 0".to_string()); } if self.max_verify_attempts == 0 { return Err("auth.max_verify_attempts must be greater than 0".to_string()); } Ok(()) } pub fn validate(&self) -> Result<(), String> { self.validate_common()?; if let Some(key) = &self.totp_encryption_key { let length = base64::engine::general_purpose::STANDARD .decode(key) .map(|bytes| bytes.len()) .unwrap_or(0); if length < TOTP_KEY_MIN_BYTES { return Err( "auth.totp_encryption_key must be base64 of at least 32 bytes".to_string(), ); } } Ok(()) } } impl crate::loader::Section for AuthConfig { const KEY: &'static str = "auth"; }