02d8172
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<String>,
pub agreements: Vec<AgreementDocConfig>,
}
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";
}