threatflux_core/
config.rs

1use std::env;
2
3use crate::error::AuthError;
4
5#[derive(Debug, Clone)]
6pub struct PostgresStoreConfig {
7    pub url: String,
8    pub max_connections: Option<u32>,
9}
10
11impl PostgresStoreConfig {
12    #[must_use]
13    pub fn new(url: impl Into<String>) -> Self {
14        Self {
15            url: url.into(),
16            max_connections: None,
17        }
18    }
19
20    #[must_use]
21    pub const fn with_max_connections(mut self, max: u32) -> Self {
22        self.max_connections = Some(max);
23        self
24    }
25}
26
27#[derive(Debug, Clone)]
28pub struct AuthConfig {
29    pub secret: String,
30    pub database: Option<PostgresStoreConfig>,
31}
32
33impl AuthConfig {
34    pub fn from_env() -> Result<Self, AuthError> {
35        let secret = env::var("AUTH_SECRET")
36            .or_else(|_| env::var("JWT_SECRET"))
37            .map_err(|_| AuthError::BadRequest("AUTH_SECRET is required".to_string()))?;
38
39        let database_url = env::var("DATABASE_URL").ok();
40        let max_connections = env::var("DATABASE_MAX_CONNECTIONS")
41            .ok()
42            .and_then(|value| value.parse::<u32>().ok());
43
44        let database = database_url.map(|url| PostgresStoreConfig {
45            url,
46            max_connections,
47        });
48
49        Ok(Self { secret, database })
50    }
51}
52
53// ============================================================================
54// MFA Configuration
55// ============================================================================
56
57/// Configuration for Multi-Factor Authentication
58#[derive(Debug, Clone)]
59pub struct MfaConfig {
60    /// Issuer name shown in authenticator apps (e.g., `MyApp`)
61    pub issuer: String,
62    /// Number of backup codes to generate (default: 8)
63    pub backup_code_count: usize,
64    /// Partial session expiry in minutes (default: 5)
65    pub partial_session_expiry_minutes: i64,
66}
67
68impl Default for MfaConfig {
69    fn default() -> Self {
70        Self {
71            issuer: "ThreatFlux".to_string(),
72            backup_code_count: 8,
73            partial_session_expiry_minutes: 5,
74        }
75    }
76}
77
78impl MfaConfig {
79    pub fn from_env() -> Self {
80        Self {
81            issuer: env::var("MFA_ISSUER").unwrap_or_else(|_| "ThreatFlux".to_string()),
82            backup_code_count: env::var("MFA_BACKUP_CODE_COUNT")
83                .ok()
84                .and_then(|v| v.parse().ok())
85                .unwrap_or(8),
86            partial_session_expiry_minutes: env::var("MFA_PARTIAL_SESSION_EXPIRY_MINUTES")
87                .ok()
88                .and_then(|v| v.parse().ok())
89                .unwrap_or(5),
90        }
91    }
92}
93
94// ============================================================================
95// Token Configuration (Magic Links, Password Reset)
96// ============================================================================
97
98/// Configuration for authentication tokens
99#[derive(Debug, Clone)]
100pub struct TokenConfig {
101    /// Magic link expiry in minutes (default: 15)
102    pub magic_link_expiry_minutes: i64,
103    /// Password reset token expiry in minutes (default: 60)
104    pub password_reset_expiry_minutes: i64,
105    /// Rate limit - max tokens per hour per user (default: 3)
106    pub rate_limit_per_hour: i32,
107}
108
109impl Default for TokenConfig {
110    fn default() -> Self {
111        Self {
112            magic_link_expiry_minutes: 15,
113            password_reset_expiry_minutes: 60,
114            rate_limit_per_hour: 3,
115        }
116    }
117}
118
119impl TokenConfig {
120    pub fn from_env() -> Self {
121        Self {
122            magic_link_expiry_minutes: env::var("MAGIC_LINK_EXPIRY_MINUTES")
123                .ok()
124                .and_then(|v| v.parse().ok())
125                .unwrap_or(15),
126            password_reset_expiry_minutes: env::var("PASSWORD_RESET_EXPIRY_MINUTES")
127                .ok()
128                .and_then(|v| v.parse().ok())
129                .unwrap_or(60),
130            rate_limit_per_hour: env::var("TOKEN_RATE_LIMIT_PER_HOUR")
131                .ok()
132                .and_then(|v| v.parse().ok())
133                .unwrap_or(3),
134        }
135    }
136}
137
138// ============================================================================
139// Device Configuration
140// ============================================================================
141
142/// Configuration for device tracking
143#[derive(Debug, Clone)]
144pub struct DeviceConfig {
145    /// How long a device stays trusted in days (default: 90)
146    pub trust_duration_days: i64,
147}
148
149impl Default for DeviceConfig {
150    fn default() -> Self {
151        Self {
152            trust_duration_days: 90,
153        }
154    }
155}
156
157impl DeviceConfig {
158    pub fn from_env() -> Self {
159        Self {
160            trust_duration_days: env::var("DEVICE_TRUST_DURATION_DAYS")
161                .ok()
162                .and_then(|v| v.parse().ok())
163                .unwrap_or(90),
164        }
165    }
166}
167
168// ============================================================================
169// WebAuthn Configuration
170// ============================================================================
171
172/// Configuration for WebAuthn/Passkeys
173#[derive(Debug, Clone)]
174pub struct WebAuthnConfig {
175    /// Relying Party ID (usually your domain, e.g., "example.com")
176    pub rp_id: String,
177    /// Relying Party name shown to users
178    pub rp_name: String,
179    /// Relying Party origin URL (e.g., `https://example.com`)
180    pub rp_origin: String,
181    /// Challenge TTL in minutes (default: 5)
182    pub challenge_ttl_minutes: i64,
183}
184
185impl Default for WebAuthnConfig {
186    fn default() -> Self {
187        Self {
188            rp_id: "localhost".to_string(),
189            rp_name: "ThreatFlux".to_string(),
190            rp_origin: "http://localhost:8080".to_string(),
191            challenge_ttl_minutes: 5,
192        }
193    }
194}
195
196impl WebAuthnConfig {
197    pub fn from_env() -> Self {
198        Self {
199            rp_id: env::var("WEBAUTHN_RP_ID").unwrap_or_else(|_| "localhost".to_string()),
200            rp_name: env::var("WEBAUTHN_RP_NAME").unwrap_or_else(|_| "ThreatFlux".to_string()),
201            rp_origin: env::var("WEBAUTHN_RP_ORIGIN")
202                .unwrap_or_else(|_| "http://localhost:8080".to_string()),
203            challenge_ttl_minutes: env::var("WEBAUTHN_CHALLENGE_TTL_MINUTES")
204                .ok()
205                .and_then(|v| v.parse().ok())
206                .unwrap_or(5),
207        }
208    }
209}
210
211// ============================================================================
212// Encryption Configuration
213// ============================================================================
214
215/// Configuration for encryption services
216#[derive(Debug, Clone)]
217pub struct EncryptionConfig {
218    /// Hex-encoded 32-byte encryption key (64 characters)
219    pub key: String,
220}
221
222impl EncryptionConfig {
223    pub fn from_env() -> Result<Self, AuthError> {
224        let key = env::var("ENCRYPTION_KEY")
225            .or_else(|_| env::var("PLAID_ENCRYPTION_KEY"))
226            .map_err(|_| AuthError::BadRequest("ENCRYPTION_KEY is required".to_string()))?;
227
228        if key.len() != 64 {
229            return Err(AuthError::BadRequest(
230                "ENCRYPTION_KEY must be 64 hex characters (32 bytes)".to_string(),
231            ));
232        }
233
234        Ok(Self { key })
235    }
236}