1use crate::error::AuthError;
4use doido_core::Environment;
5use serde::Deserialize;
6use std::collections::HashMap;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Default)]
10#[serde(rename_all = "snake_case")]
11pub enum StrategyKind {
12 #[default]
13 Cookie,
14 Jwt,
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
24#[serde(rename_all = "snake_case")]
25pub enum AuthModule {
26 DatabaseAuthenticatable,
28 Registerable,
30 Recoverable,
32 Rememberable,
34 Trackable,
36 Timeoutable,
38 Validatable,
40 Confirmable,
42 Lockable,
44 Omniauthable,
46 TwoFactorAuthenticatable,
48}
49
50impl AuthModule {
51 pub const ALL: [AuthModule; 11] = [
53 AuthModule::DatabaseAuthenticatable,
54 AuthModule::Registerable,
55 AuthModule::Recoverable,
56 AuthModule::Rememberable,
57 AuthModule::Trackable,
58 AuthModule::Timeoutable,
59 AuthModule::Validatable,
60 AuthModule::Confirmable,
61 AuthModule::Lockable,
62 AuthModule::Omniauthable,
63 AuthModule::TwoFactorAuthenticatable,
64 ];
65
66 pub fn as_str(self) -> &'static str {
68 match self {
69 AuthModule::DatabaseAuthenticatable => "database_authenticatable",
70 AuthModule::Registerable => "registerable",
71 AuthModule::Recoverable => "recoverable",
72 AuthModule::Rememberable => "rememberable",
73 AuthModule::Trackable => "trackable",
74 AuthModule::Timeoutable => "timeoutable",
75 AuthModule::Validatable => "validatable",
76 AuthModule::Confirmable => "confirmable",
77 AuthModule::Lockable => "lockable",
78 AuthModule::Omniauthable => "omniauthable",
79 AuthModule::TwoFactorAuthenticatable => "two_factor_authenticatable",
80 }
81 }
82
83 pub fn from_name(s: &str) -> Option<AuthModule> {
85 AuthModule::ALL.into_iter().find(|m| m.as_str() == s)
86 }
87
88 pub fn route_group(self) -> Option<&'static str> {
92 match self {
93 AuthModule::Registerable => Some("registrations"),
94 AuthModule::Recoverable => Some("passwords"),
95 AuthModule::Confirmable => Some("confirmation"),
96 AuthModule::Lockable => Some("unlock"),
97 AuthModule::Omniauthable => Some("oauth"),
98 AuthModule::TwoFactorAuthenticatable => Some("two_factor"),
99 _ => None,
100 }
101 }
102}
103
104#[derive(Debug, Clone, Deserialize)]
106pub struct JwtConfig {
107 pub secret: String,
108 #[serde(default = "default_access_ttl")]
109 pub access_ttl: u64,
110 #[serde(default = "default_refresh_ttl")]
111 pub refresh_ttl: u64,
112 #[serde(default)]
113 pub issuer: Option<String>,
114}
115
116fn default_access_ttl() -> u64 {
117 900
118}
119
120fn default_refresh_ttl() -> u64 {
121 604_800
122}
123
124impl JwtConfig {
125 pub fn validate(&self) -> Result<(), AuthError> {
126 if self.secret.trim().is_empty() {
127 return Err(AuthError::Config(
128 "auth.jwt.secret must not be empty".into(),
129 ));
130 }
131 Ok(())
132 }
133}
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
137#[serde(rename_all = "snake_case")]
138pub enum OAuthProviderType {
139 Oauth1,
140 Oauth2,
141}
142
143#[derive(Debug, Clone, Deserialize)]
145pub struct OAuthProviderConfig {
146 #[serde(rename = "type")]
147 pub provider_type: OAuthProviderType,
148 #[serde(default)]
149 pub client_id: Option<String>,
150 #[serde(default)]
151 pub client_secret: Option<String>,
152 #[serde(default)]
153 pub consumer_key: Option<String>,
154 #[serde(default)]
155 pub consumer_secret: Option<String>,
156 #[serde(default)]
157 pub redirect_uri: Option<String>,
158 #[serde(default)]
159 pub scopes: Vec<String>,
160 #[serde(default)]
161 pub authorize_url: Option<String>,
162 #[serde(default)]
163 pub token_url: Option<String>,
164}
165
166#[derive(Debug, Clone, Default, Deserialize)]
168pub struct TwoFactorConfig {
169 #[serde(default)]
170 pub enabled: bool,
171 #[serde(default)]
172 pub issuer: Option<String>,
173}
174
175#[derive(Debug, Clone, Deserialize)]
177pub struct AuthRoutesConfig {
178 #[serde(default = "default_prefix")]
179 pub prefix: String,
180 #[serde(default = "default_sign_in")]
181 pub sign_in: String,
182 #[serde(default = "default_sign_out")]
183 pub sign_out: String,
184 #[serde(default = "default_sign_up")]
185 pub sign_up: String,
186 #[serde(default = "default_password_reset")]
187 pub password_reset: String,
188}
189
190fn default_prefix() -> String {
191 "/users".into()
192}
193
194fn default_sign_in() -> String {
195 "sign_in".into()
196}
197
198fn default_sign_out() -> String {
199 "sign_out".into()
200}
201
202fn default_sign_up() -> String {
203 "sign_up".into()
204}
205
206fn default_password_reset() -> String {
207 "password".into()
208}
209
210impl Default for AuthRoutesConfig {
211 fn default() -> Self {
212 Self {
213 prefix: default_prefix(),
214 sign_in: default_sign_in(),
215 sign_out: default_sign_out(),
216 sign_up: default_sign_up(),
217 password_reset: default_password_reset(),
218 }
219 }
220}
221
222impl AuthRoutesConfig {
223 pub fn sign_in_path(&self) -> String {
224 format!("{}/{}", self.prefix.trim_end_matches('/'), self.sign_in)
225 }
226
227 pub fn sign_out_path(&self) -> String {
228 format!("{}/{}", self.prefix.trim_end_matches('/'), self.sign_out)
229 }
230
231 pub fn sign_up_path(&self) -> String {
232 format!("{}/{}", self.prefix.trim_end_matches('/'), self.sign_up)
233 }
234
235 pub fn password_path(&self) -> String {
236 format!(
237 "{}/{}",
238 self.prefix.trim_end_matches('/'),
239 self.password_reset
240 )
241 }
242}
243
244#[derive(Debug, Clone, Deserialize)]
246pub struct AuthConfig {
247 #[serde(default)]
248 pub user_model: Option<String>,
249 #[serde(default = "default_modules")]
250 pub modules: Vec<AuthModule>,
251 #[serde(default = "default_strategies")]
252 pub strategies: Vec<String>,
253 #[serde(default)]
254 pub jwt: Option<JwtConfig>,
255 #[serde(default)]
256 pub oauth: HashMap<String, OAuthProviderConfig>,
257 #[serde(default)]
258 pub two_factor: TwoFactorConfig,
259 #[serde(default = "default_timeout")]
261 pub timeout: u64,
262 #[serde(default = "default_password_length")]
264 pub password_length: usize,
265 #[serde(default = "default_maximum_attempts")]
267 pub maximum_attempts: u32,
268 #[serde(default = "default_unlock_in")]
270 pub unlock_in: i64,
271 #[serde(default = "default_reset_within")]
273 pub reset_password_within: i64,
274 #[serde(default = "default_remember_for")]
276 pub remember_for: i64,
277 #[serde(default)]
278 pub routes: AuthRoutesConfig,
279}
280
281fn default_strategies() -> Vec<String> {
282 vec!["cookie".into()]
283}
284
285fn default_modules() -> Vec<AuthModule> {
287 vec![
288 AuthModule::DatabaseAuthenticatable,
289 AuthModule::Registerable,
290 AuthModule::Recoverable,
291 AuthModule::Rememberable,
292 AuthModule::Validatable,
293 ]
294}
295
296fn default_timeout() -> u64 {
297 1_800
298}
299
300fn default_password_length() -> usize {
301 6
302}
303
304fn default_maximum_attempts() -> u32 {
305 20
306}
307
308fn default_unlock_in() -> i64 {
309 3_600
310}
311
312fn default_reset_within() -> i64 {
313 21_600
314}
315
316fn default_remember_for() -> i64 {
317 1_209_600
318}
319
320impl Default for AuthConfig {
321 fn default() -> Self {
322 Self {
323 user_model: None,
324 modules: default_modules(),
325 strategies: default_strategies(),
326 jwt: None,
327 oauth: HashMap::new(),
328 two_factor: TwoFactorConfig::default(),
329 timeout: default_timeout(),
330 password_length: default_password_length(),
331 maximum_attempts: default_maximum_attempts(),
332 unlock_in: default_unlock_in(),
333 reset_password_within: default_reset_within(),
334 remember_for: default_remember_for(),
335 routes: AuthRoutesConfig::default(),
336 }
337 }
338}
339
340impl AuthConfig {
341 pub fn from_yaml(yaml: &str) -> Result<Self, std::io::Error> {
343 YamlConfig::from_yaml(yaml).map(|c| c.auth)
344 }
345
346 pub fn has_module(&self, module: AuthModule) -> bool {
348 self.modules.contains(&module)
349 }
350
351 pub fn enabled_route_groups(&self) -> Vec<&'static str> {
355 let mut groups = vec!["sessions"];
356 for module in AuthModule::ALL {
357 if self.has_module(module) {
358 if let Some(group) = module.route_group() {
359 groups.push(group);
360 }
361 }
362 }
363 groups
364 }
365
366 pub fn validate(&self) -> Result<(), AuthError> {
368 for name in &self.strategies {
369 match name.as_str() {
370 "cookie" => {}
371 "jwt" => {
372 let jwt = self.jwt.as_ref().ok_or_else(|| {
373 AuthError::Config(
374 "auth.jwt section required when jwt strategy is enabled".into(),
375 )
376 })?;
377 jwt.validate()?;
378 }
379 other => {
380 if !crate::registry::has_strategy(other) {
381 return Err(AuthError::UnknownStrategy(other.to_string()));
382 }
383 }
384 }
385 }
386
387 if !self.has_module(AuthModule::DatabaseAuthenticatable) {
388 return Err(AuthError::Config(
389 "auth.modules must include database_authenticatable".into(),
390 ));
391 }
392 if self.has_module(AuthModule::TwoFactorAuthenticatable) && !cfg!(feature = "auth-2fa") {
393 return Err(AuthError::Config(
394 "auth.modules includes two_factor_authenticatable but the `auth-2fa` feature is not enabled".into(),
395 ));
396 }
397
398 Ok(())
399 }
400
401 pub fn strategy_kinds(&self) -> Vec<StrategyKind> {
402 self.strategies
403 .iter()
404 .filter_map(|s| match s.as_str() {
405 "cookie" => Some(StrategyKind::Cookie),
406 "jwt" => Some(StrategyKind::Jwt),
407 _ => None,
408 })
409 .collect()
410 }
411}
412
413#[derive(Debug, Clone, Default, Deserialize)]
415pub struct YamlConfig {
416 #[serde(default)]
417 pub auth: AuthConfig,
418}
419
420impl YamlConfig {
421 pub fn load() -> std::io::Result<Self> {
422 Self::load_env(Environment::get_env())
423 }
424
425 pub fn load_env(env: Environment) -> std::io::Result<Self> {
426 let path = format!("config/{}.yml", env.as_str());
427 let contents = std::fs::read_to_string(&path)?;
428 Self::from_yaml(&contents)
429 }
430
431 pub fn from_yaml(yaml: &str) -> std::io::Result<Self> {
432 serde_norway::from_str(yaml)
433 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
434 }
435}
436
437pub fn load() -> AuthConfig {
439 YamlConfig::load().map(|c| c.auth).unwrap_or_default()
440}