b453d5d
use base64::Engine;
use serde::Deserialize;
#[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 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 {
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 {
pub fn new() -> Self {
Self::default()
}
pub fn with_password_policy(mut self, policy: PasswordPolicy) -> Self {
self.password_policy = policy;
self
}
pub fn with_verify_code_ttl_secs(mut self, secs: u64) -> Self {
self.verify_code_ttl_secs = secs;
self
}
pub fn with_verify_code_len(mut self, len: usize) -> Self {
self.verify_code_len = len;
self
}
pub fn with_reset_token_ttl_secs(mut self, secs: u64) -> Self {
self.reset_token_ttl_secs = secs;
self
}
pub fn with_reset_token_bytes(mut self, bytes: usize) -> Self {
self.reset_token_bytes = bytes;
self
}
pub fn with_email_code_rate_limit_secs(mut self, secs: u64) -> Self {
self.email_code_rate_limit_secs = secs;
self
}
pub fn with_max_verify_attempts(mut self, attempts: u64) -> Self {
self.max_verify_attempts = attempts;
self
}
pub fn with_min_age(mut self, min_age: u8) -> Self {
self.min_age = min_age;
self
}
pub fn with_totp_encryption_key(mut self, key: impl Into<String>) -> Self {
self.totp_encryption_key = Some(key.into());
self
}
pub fn with_agreements(mut self, agreements: Vec<AgreementDocConfig>) -> Self {
self.agreements = agreements;
self
}
pub fn validate(&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());
}
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 < 32 {
return Err(
"auth.totp_encryption_key must be base64 of at least 32 bytes".to_string(),
);
}
}
Ok(())
}
}