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, Deserialize)]
19pub struct JwtConfig {
20 pub secret: String,
21 #[serde(default = "default_access_ttl")]
22 pub access_ttl: u64,
23 #[serde(default = "default_refresh_ttl")]
24 pub refresh_ttl: u64,
25 #[serde(default)]
26 pub issuer: Option<String>,
27}
28
29fn default_access_ttl() -> u64 {
30 900
31}
32
33fn default_refresh_ttl() -> u64 {
34 604_800
35}
36
37impl JwtConfig {
38 pub fn validate(&self) -> Result<(), AuthError> {
39 if self.secret.trim().is_empty() {
40 return Err(AuthError::Config(
41 "auth.jwt.secret must not be empty".into(),
42 ));
43 }
44 Ok(())
45 }
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
50#[serde(rename_all = "snake_case")]
51pub enum OAuthProviderType {
52 Oauth1,
53 Oauth2,
54}
55
56#[derive(Debug, Clone, Deserialize)]
58pub struct OAuthProviderConfig {
59 #[serde(rename = "type")]
60 pub provider_type: OAuthProviderType,
61 #[serde(default)]
62 pub client_id: Option<String>,
63 #[serde(default)]
64 pub client_secret: Option<String>,
65 #[serde(default)]
66 pub consumer_key: Option<String>,
67 #[serde(default)]
68 pub consumer_secret: Option<String>,
69 #[serde(default)]
70 pub redirect_uri: Option<String>,
71 #[serde(default)]
72 pub scopes: Vec<String>,
73 #[serde(default)]
74 pub authorize_url: Option<String>,
75 #[serde(default)]
76 pub token_url: Option<String>,
77}
78
79#[derive(Debug, Clone, Default, Deserialize)]
81pub struct TwoFactorConfig {
82 #[serde(default)]
83 pub enabled: bool,
84 #[serde(default)]
85 pub issuer: Option<String>,
86}
87
88#[derive(Debug, Clone, Deserialize)]
90pub struct AuthRoutesConfig {
91 #[serde(default = "default_prefix")]
92 pub prefix: String,
93 #[serde(default = "default_sign_in")]
94 pub sign_in: String,
95 #[serde(default = "default_sign_out")]
96 pub sign_out: String,
97 #[serde(default = "default_sign_up")]
98 pub sign_up: String,
99 #[serde(default = "default_password_reset")]
100 pub password_reset: String,
101}
102
103fn default_prefix() -> String {
104 "/users".into()
105}
106
107fn default_sign_in() -> String {
108 "sign_in".into()
109}
110
111fn default_sign_out() -> String {
112 "sign_out".into()
113}
114
115fn default_sign_up() -> String {
116 "sign_up".into()
117}
118
119fn default_password_reset() -> String {
120 "password".into()
121}
122
123impl Default for AuthRoutesConfig {
124 fn default() -> Self {
125 Self {
126 prefix: default_prefix(),
127 sign_in: default_sign_in(),
128 sign_out: default_sign_out(),
129 sign_up: default_sign_up(),
130 password_reset: default_password_reset(),
131 }
132 }
133}
134
135impl AuthRoutesConfig {
136 pub fn sign_in_path(&self) -> String {
137 format!("{}/{}", self.prefix.trim_end_matches('/'), self.sign_in)
138 }
139
140 pub fn sign_out_path(&self) -> String {
141 format!("{}/{}", self.prefix.trim_end_matches('/'), self.sign_out)
142 }
143
144 pub fn sign_up_path(&self) -> String {
145 format!("{}/{}", self.prefix.trim_end_matches('/'), self.sign_up)
146 }
147
148 pub fn password_path(&self) -> String {
149 format!(
150 "{}/{}",
151 self.prefix.trim_end_matches('/'),
152 self.password_reset
153 )
154 }
155}
156
157#[derive(Debug, Clone, Deserialize)]
159pub struct AuthConfig {
160 #[serde(default)]
161 pub user_model: Option<String>,
162 #[serde(default = "default_strategies")]
163 pub strategies: Vec<String>,
164 #[serde(default)]
165 pub jwt: Option<JwtConfig>,
166 #[serde(default)]
167 pub oauth: HashMap<String, OAuthProviderConfig>,
168 #[serde(default)]
169 pub two_factor: TwoFactorConfig,
170 #[serde(default)]
171 pub routes: AuthRoutesConfig,
172}
173
174fn default_strategies() -> Vec<String> {
175 vec!["cookie".into()]
176}
177
178impl Default for AuthConfig {
179 fn default() -> Self {
180 Self {
181 user_model: None,
182 strategies: default_strategies(),
183 jwt: None,
184 oauth: HashMap::new(),
185 two_factor: TwoFactorConfig::default(),
186 routes: AuthRoutesConfig::default(),
187 }
188 }
189}
190
191impl AuthConfig {
192 pub fn from_yaml(yaml: &str) -> Result<Self, std::io::Error> {
194 YamlConfig::from_yaml(yaml).map(|c| c.auth)
195 }
196
197 pub fn validate(&self) -> Result<(), AuthError> {
199 for name in &self.strategies {
200 match name.as_str() {
201 "cookie" => {}
202 "jwt" => {
203 let jwt = self.jwt.as_ref().ok_or_else(|| {
204 AuthError::Config(
205 "auth.jwt section required when jwt strategy is enabled".into(),
206 )
207 })?;
208 jwt.validate()?;
209 }
210 other => {
211 if !crate::registry::has_strategy(other) {
212 return Err(AuthError::UnknownStrategy(other.to_string()));
213 }
214 }
215 }
216 }
217 Ok(())
218 }
219
220 pub fn strategy_kinds(&self) -> Vec<StrategyKind> {
221 self.strategies
222 .iter()
223 .filter_map(|s| match s.as_str() {
224 "cookie" => Some(StrategyKind::Cookie),
225 "jwt" => Some(StrategyKind::Jwt),
226 _ => None,
227 })
228 .collect()
229 }
230}
231
232#[derive(Debug, Clone, Default, Deserialize)]
234pub struct YamlConfig {
235 #[serde(default)]
236 pub auth: AuthConfig,
237}
238
239impl YamlConfig {
240 pub fn load() -> std::io::Result<Self> {
241 Self::load_env(Environment::get_env())
242 }
243
244 pub fn load_env(env: Environment) -> std::io::Result<Self> {
245 let path = format!("config/{}.yml", env.as_str());
246 let contents = std::fs::read_to_string(&path)?;
247 Self::from_yaml(&contents)
248 }
249
250 pub fn from_yaml(yaml: &str) -> std::io::Result<Self> {
251 serde_norway::from_str(yaml)
252 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
253 }
254}
255
256pub fn load() -> AuthConfig {
258 YamlConfig::load().map(|c| c.auth).unwrap_or_default()
259}