use std::env;
use crate::error::AuthError;
#[derive(Debug, Clone)]
pub struct PostgresStoreConfig {
pub url: String,
pub max_connections: Option<u32>,
}
impl PostgresStoreConfig {
#[must_use]
pub fn new(url: impl Into<String>) -> Self {
Self {
url: url.into(),
max_connections: None,
}
}
#[must_use]
pub const fn with_max_connections(mut self, max: u32) -> Self {
self.max_connections = Some(max);
self
}
}
#[derive(Debug, Clone)]
pub struct AuthConfig {
pub secret: String,
pub database: Option<PostgresStoreConfig>,
}
impl AuthConfig {
pub fn from_env() -> Result<Self, AuthError> {
let secret = env::var("AUTH_SECRET")
.or_else(|_| env::var("JWT_SECRET"))
.map_err(|_| AuthError::BadRequest("AUTH_SECRET is required".to_string()))?;
let database_url = env::var("DATABASE_URL").ok();
let max_connections = env::var("DATABASE_MAX_CONNECTIONS")
.ok()
.and_then(|value| value.parse::<u32>().ok());
let database = database_url.map(|url| PostgresStoreConfig {
url,
max_connections,
});
Ok(Self { secret, database })
}
}
#[derive(Debug, Clone)]
pub struct MfaConfig {
pub issuer: String,
pub backup_code_count: usize,
pub partial_session_expiry_minutes: i64,
}
impl Default for MfaConfig {
fn default() -> Self {
Self {
issuer: "ThreatFlux".to_string(),
backup_code_count: 8,
partial_session_expiry_minutes: 5,
}
}
}
impl MfaConfig {
pub fn from_env() -> Self {
Self {
issuer: env::var("MFA_ISSUER").unwrap_or_else(|_| "ThreatFlux".to_string()),
backup_code_count: env::var("MFA_BACKUP_CODE_COUNT")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(8),
partial_session_expiry_minutes: env::var("MFA_PARTIAL_SESSION_EXPIRY_MINUTES")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(5),
}
}
}
#[derive(Debug, Clone)]
pub struct TokenConfig {
pub magic_link_expiry_minutes: i64,
pub password_reset_expiry_minutes: i64,
pub rate_limit_per_hour: i32,
}
impl Default for TokenConfig {
fn default() -> Self {
Self {
magic_link_expiry_minutes: 15,
password_reset_expiry_minutes: 60,
rate_limit_per_hour: 3,
}
}
}
impl TokenConfig {
pub fn from_env() -> Self {
Self {
magic_link_expiry_minutes: env::var("MAGIC_LINK_EXPIRY_MINUTES")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(15),
password_reset_expiry_minutes: env::var("PASSWORD_RESET_EXPIRY_MINUTES")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(60),
rate_limit_per_hour: env::var("TOKEN_RATE_LIMIT_PER_HOUR")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(3),
}
}
}
#[derive(Debug, Clone)]
pub struct DeviceConfig {
pub trust_duration_days: i64,
}
impl Default for DeviceConfig {
fn default() -> Self {
Self {
trust_duration_days: 90,
}
}
}
impl DeviceConfig {
pub fn from_env() -> Self {
Self {
trust_duration_days: env::var("DEVICE_TRUST_DURATION_DAYS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(90),
}
}
}
#[derive(Debug, Clone)]
pub struct WebAuthnConfig {
pub rp_id: String,
pub rp_name: String,
pub rp_origin: String,
pub challenge_ttl_minutes: i64,
}
impl Default for WebAuthnConfig {
fn default() -> Self {
Self {
rp_id: "localhost".to_string(),
rp_name: "ThreatFlux".to_string(),
rp_origin: "http://localhost:8080".to_string(),
challenge_ttl_minutes: 5,
}
}
}
impl WebAuthnConfig {
pub fn from_env() -> Self {
Self {
rp_id: env::var("WEBAUTHN_RP_ID").unwrap_or_else(|_| "localhost".to_string()),
rp_name: env::var("WEBAUTHN_RP_NAME").unwrap_or_else(|_| "ThreatFlux".to_string()),
rp_origin: env::var("WEBAUTHN_RP_ORIGIN")
.unwrap_or_else(|_| "http://localhost:8080".to_string()),
challenge_ttl_minutes: env::var("WEBAUTHN_CHALLENGE_TTL_MINUTES")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(5),
}
}
}
#[derive(Debug, Clone)]
pub struct EncryptionConfig {
pub key: String,
}
impl EncryptionConfig {
pub fn from_env() -> Result<Self, AuthError> {
let key = env::var("ENCRYPTION_KEY")
.or_else(|_| env::var("PLAID_ENCRYPTION_KEY"))
.map_err(|_| AuthError::BadRequest("ENCRYPTION_KEY is required".to_string()))?;
if key.len() != 64 {
return Err(AuthError::BadRequest(
"ENCRYPTION_KEY must be 64 hex characters (32 bytes)".to_string(),
));
}
Ok(Self { key })
}
}