auth_framework/config/
app_config.rs

1/// Configuration management with environment variable support.
2///
3/// This module provides easy configuration loading from environment
4/// variables, config files, and other sources.
5use super::SecurityConfig;
6use serde::{Deserialize, Serialize};
7use std::{env, time::Duration};
8
9impl Default for ConfigBuilder {
10    fn default() -> Self {
11        Self::new()
12    }
13}
14
15/// Complete application configuration
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct AppConfig {
18    /// Database configuration
19    pub database: DatabaseConfig,
20    /// Redis configuration
21    pub redis: Option<RedisConfig>,
22    /// JWT configuration
23    pub jwt: JwtConfig,
24    /// OAuth providers
25    pub oauth: OAuthConfig,
26    /// Security settings
27    pub security: SecuritySettings,
28    /// Logging configuration
29    pub logging: LoggingConfig,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct DatabaseConfig {
34    pub url: String,
35    pub max_connections: u32,
36    pub min_connections: u32,
37    pub connect_timeout_seconds: u64,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct RedisConfig {
42    pub url: String,
43    pub pool_size: u32,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct JwtConfig {
48    pub secret_key: String,
49    pub issuer: String,
50    pub audience: String,
51    pub access_token_ttl_seconds: u64,
52    pub refresh_token_ttl_seconds: u64,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct OAuthConfig {
57    pub google: Option<OAuthProviderConfig>,
58    pub github: Option<OAuthProviderConfig>,
59    pub microsoft: Option<OAuthProviderConfig>,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct OAuthProviderConfig {
64    pub client_id: String,
65    pub client_secret: String,
66    pub redirect_uri: String,
67    pub scopes: Vec<String>,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct SecuritySettings {
72    pub password_min_length: usize,
73    pub password_require_special: bool,
74    pub rate_limit_requests_per_minute: u32,
75    pub session_timeout_hours: u64,
76    pub max_concurrent_sessions: u32,
77    pub require_mfa: bool,
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct LoggingConfig {
82    pub level: String,
83    pub audit_enabled: bool,
84    pub audit_storage: String, // "database", "file", "syslog"
85}
86
87impl AppConfig {
88    /// Load configuration from environment variables
89    pub fn from_env() -> Result<Self, ConfigError> {
90        Ok(Self {
91            database: DatabaseConfig {
92                url: env::var("DATABASE_URL")
93                    .map_err(|_| ConfigError::MissingEnvVar("DATABASE_URL"))?,
94                max_connections: env::var("DB_MAX_CONNECTIONS")
95                    .unwrap_or_else(|_| "10".to_string())
96                    .parse()
97                    .map_err(|_| ConfigError::InvalidValue("DB_MAX_CONNECTIONS"))?,
98                min_connections: 1,
99                connect_timeout_seconds: 30,
100            },
101            redis: if let Ok(redis_url) = env::var("REDIS_URL") {
102                Some(RedisConfig {
103                    url: redis_url,
104                    pool_size: 10,
105                })
106            } else {
107                None
108            },
109            jwt: JwtConfig {
110                secret_key: env::var("JWT_SECRET")
111                    .map_err(|_| ConfigError::MissingEnvVar("JWT_SECRET"))?,
112                issuer: env::var("JWT_ISSUER").unwrap_or_else(|_| "auth-framework".to_string()),
113                audience: env::var("JWT_AUDIENCE").unwrap_or_else(|_| "api".to_string()),
114                access_token_ttl_seconds: 3600,
115                refresh_token_ttl_seconds: 86400 * 7,
116            },
117            oauth: OAuthConfig {
118                google: Self::load_oauth_provider("GOOGLE"),
119                github: Self::load_oauth_provider("GITHUB"),
120                microsoft: Self::load_oauth_provider("MICROSOFT"),
121            },
122            security: SecuritySettings {
123                password_min_length: 8,
124                password_require_special: true,
125                rate_limit_requests_per_minute: 60,
126                session_timeout_hours: 24,
127                max_concurrent_sessions: 5,
128                require_mfa: env::var("REQUIRE_MFA").unwrap_or_default() == "true",
129            },
130            logging: LoggingConfig {
131                level: env::var("LOG_LEVEL").unwrap_or_else(|_| "info".to_string()),
132                audit_enabled: true,
133                audit_storage: env::var("AUDIT_STORAGE").unwrap_or_else(|_| "database".to_string()),
134            },
135        })
136    }
137
138    fn load_oauth_provider(provider: &str) -> Option<OAuthProviderConfig> {
139        let client_id = env::var(format!("{}_CLIENT_ID", provider)).ok()?;
140        let client_secret = env::var(format!("{}_CLIENT_SECRET", provider)).ok()?;
141
142        Some(OAuthProviderConfig {
143            client_id,
144            client_secret,
145            redirect_uri: env::var(format!("{}_REDIRECT_URI", provider))
146                .unwrap_or_else(|_| format!("/auth/{}/callback", provider.to_lowercase())),
147            scopes: env::var(format!("{}_SCOPES", provider))
148                .unwrap_or_default()
149                .split(',')
150                .map(|s| s.trim().to_string())
151                .filter(|s| !s.is_empty())
152                .collect(),
153        })
154    }
155
156    /// Convert to AuthConfig
157    pub fn to_auth_config(&self) -> super::AuthConfig {
158        super::AuthConfig::new()
159            .token_lifetime(Duration::from_secs(self.jwt.access_token_ttl_seconds))
160            .refresh_token_lifetime(Duration::from_secs(self.jwt.refresh_token_ttl_seconds))
161            .issuer(&self.jwt.issuer)
162            .audience(&self.jwt.audience)
163            .secret(&self.jwt.secret_key)
164    }
165
166    /// Convert to SecurityConfig
167    pub fn to_security_config(&self) -> SecurityConfig {
168        SecurityConfig::default() // Would customize based on security settings
169    }
170}
171
172#[derive(Debug, thiserror::Error)]
173pub enum ConfigError {
174    #[error("Missing environment variable: {0}")]
175    MissingEnvVar(&'static str),
176    #[error("Invalid value for: {0}")]
177    InvalidValue(&'static str),
178    #[error("Configuration validation error: {0}")]
179    Validation(String),
180}
181
182/// Configuration builder for easy setup
183pub struct ConfigBuilder {
184    config: AppConfig,
185}
186
187impl ConfigBuilder {
188    pub fn new() -> Self {
189        Self {
190            config: AppConfig::from_env().unwrap_or_else(|_| AppConfig::default()),
191        }
192    }
193
194    pub fn with_database_url(mut self, url: impl Into<String>) -> Self {
195        self.config.database.url = url.into();
196        self
197    }
198
199    pub fn with_jwt_secret(mut self, secret: impl Into<String>) -> Self {
200        self.config.jwt.secret_key = secret.into();
201        self
202    }
203
204    pub fn with_redis_url(mut self, url: impl Into<String>) -> Self {
205        self.config.redis = Some(RedisConfig {
206            url: url.into(),
207            pool_size: 10,
208        });
209        self
210    }
211
212    pub fn build(self) -> AppConfig {
213        self.config
214    }
215}
216
217impl Default for AppConfig {
218    fn default() -> Self {
219        Self {
220            database: DatabaseConfig {
221                url: "postgresql://localhost/auth_framework".to_string(),
222                max_connections: 10,
223                min_connections: 1,
224                connect_timeout_seconds: 30,
225            },
226            redis: None,
227            jwt: JwtConfig {
228                secret_key: "development-only-secret-change-in-production".to_string(),
229                issuer: "auth-framework".to_string(),
230                audience: "api".to_string(),
231                access_token_ttl_seconds: 3600,
232                refresh_token_ttl_seconds: 86400 * 7,
233            },
234            oauth: OAuthConfig {
235                google: None,
236                github: None,
237                microsoft: None,
238            },
239            security: SecuritySettings {
240                password_min_length: 8,
241                password_require_special: true,
242                rate_limit_requests_per_minute: 60,
243                session_timeout_hours: 24,
244                max_concurrent_sessions: 5,
245                require_mfa: false,
246            },
247            logging: LoggingConfig {
248                level: "info".to_string(),
249                audit_enabled: true,
250                audit_storage: "database".to_string(),
251            },
252        }
253    }
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259
260    #[test]
261    fn test_config_builder() {
262        let config = ConfigBuilder::new()
263            .with_database_url("postgresql://test")
264            .with_jwt_secret("test-secret")
265            .build();
266
267        assert_eq!(config.database.url, "postgresql://test");
268        assert_eq!(config.jwt.secret_key, "test-secret");
269    }
270}
271
272