1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct AppConfig {
18 pub database: DatabaseConfig,
20 pub redis: Option<RedisConfig>,
22 pub jwt: JwtConfig,
24 pub oauth: OAuthConfig,
26 pub security: SecuritySettings,
28 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, }
86
87impl AppConfig {
88 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 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 pub fn to_security_config(&self) -> SecurityConfig {
168 SecurityConfig::default() }
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
182pub 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