b6f7f73
ZhenYi
refactor: reorganize configuration files and update module paths for improved structure
b6f7f73
use base64::Engine;
use serde::Deserialize;
pub const TOTP_KEY_MIN_BYTES: usize = 32;
#[derive(Clone, Deserialize)]
#[serde(default)]
pub struct OAuthConfig {
pub public_url: String,
pub clients: Vec<OAuthClient>,
pub socket_origins: Vec<String>,
#[serde(default = "default_access_token_ttl")]
pub access_token_ttl_secs: i64,
#[serde(default = "default_id_token_ttl")]
pub id_token_ttl_secs: i64,
#[serde(default = "default_refresh_token_ttl")]
pub refresh_token_ttl_secs: i64,
#[serde(default = "default_code_ttl")]
pub code_ttl_secs: i64,
#[serde(default = "default_true")]
pub require_consent: bool,
#[serde(default = "default_true")]
pub allow_wildcard_redirect: bool,
#[serde(default)]
pub rpc_key: String,
}
impl Default for OAuthConfig {
fn default() -> Self {
Self {
public_url: String::new(),
clients: Vec::new(),
socket_origins: Vec::new(),
access_token_ttl_secs: default_access_token_ttl(),
id_token_ttl_secs: default_id_token_ttl(),
refresh_token_ttl_secs: default_refresh_token_ttl(),
code_ttl_secs: default_code_ttl(),
require_consent: true,
allow_wildcard_redirect: true,
rpc_key: String::new(),
}
}
}
fn default_access_token_ttl() -> i64 {
600
}
fn default_id_token_ttl() -> i64 {
300
}
fn default_refresh_token_ttl() -> i64 {
2_592_000
}
fn default_code_ttl() -> i64 {
60
}
fn default_true() -> bool {
true
}
impl std::fmt::Debug for OAuthConfig {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("OAuthConfig")
.field("public_url", &self.public_url)
.field("clients", &self.clients)
.field("socket_origins", &self.socket_origins)
.field("access_token_ttl_secs", &self.access_token_ttl_secs)
.field("id_token_ttl_secs", &self.id_token_ttl_secs)
.field("refresh_token_ttl_secs", &self.refresh_token_ttl_secs)
.field("code_ttl_secs", &self.code_ttl_secs)
.field("require_consent", &self.require_consent)
.field("allow_wildcard_redirect", &self.allow_wildcard_redirect)
.field("rpc_key", &"<redacted>")
.finish()
}
}
#[derive(Clone, Deserialize)]
#[serde(default)]
pub struct OAuthClient {
pub client_id: String,
pub name: String,
pub kind: models::users::oauth_clients::ClientKind,
pub secret: Option<String>,
pub audience: Option<String>,
#[serde(default = "default_true")]
pub enabled: bool,
pub redirect_uris: Vec<String>,
pub post_logout_redirect_uris: Vec<String>,
pub backchannel_logout_uris: Vec<String>,
pub scopes: Vec<String>,
pub service_scopes: Vec<String>,
}
impl Default for OAuthClient {
fn default() -> Self {
Self {
client_id: String::new(),
name: String::new(),
kind: models::users::oauth_clients::ClientKind::Public,
secret: None,
audience: None,
enabled: true,
redirect_uris: Vec::new(),
post_logout_redirect_uris: Vec::new(),
backchannel_logout_uris: Vec::new(),
scopes: Vec::new(),
service_scopes: Vec::new(),
}
}
}
impl std::fmt::Debug for OAuthClient {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("OAuthClient")
.field("client_id", &self.client_id)
.field("name", &self.name)
.field("kind", &self.kind)
.field("secret", &self.secret.as_ref().map(|_| "<redacted>"))
.field("audience", &self.audience)
.field("enabled", &self.enabled)
.field("redirect_uris", &self.redirect_uris)
.field("post_logout_redirect_uris", &self.post_logout_redirect_uris)
.field("backchannel_logout_uris", &self.backchannel_logout_uris)
.field("scopes", &self.scopes)
.field("service_scopes", &self.service_scopes)
.finish()
}
}
#[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 oauth: OAuthConfig,
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 {
oauth: OAuthConfig::default(),
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(())
}
pub fn validate_auth(&self) -> Result<(), String> {
self.validate_common()?;
let Some(key) = &self.totp_encryption_key else {
return Err("auth.totp_encryption_key is required".to_string());
};
if key.trim().starts_with("REPLACE_WITH_") {
return Err(
"auth.totp_encryption_key is still set to the placeholder from \
config.yaml.example"
.to_string(),
);
}
let length = base64::engine::general_purpose::STANDARD
.decode(key.trim())
.map(|bytes| bytes.len())
.unwrap_or(0);
if length < TOTP_KEY_MIN_BYTES {
return Err(format!(
"auth.totp_encryption_key must be base64 of at least {TOTP_KEY_MIN_BYTES} bytes"
));
}
Ok(())
}
}
impl crate::loader::Section for AuthConfig {
const KEY: &'static str = "auth";
}