1use crate::AuthError;
2use crate::config::{DatabaseIdStrategy, HashMap};
3#[derive(Debug, Clone, PartialEq, Eq)]
4pub enum SameSite {
5 Strict,
6 Lax,
7 None,
8}
9
10impl std::fmt::Display for SameSite {
11 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
12 match self {
13 Self::Strict => f.write_str("Strict"),
14 Self::Lax => f.write_str("Lax"),
15 Self::None => f.write_str("None"),
16 }
17 }
18}
19
20#[derive(Debug, Clone, Default)]
22pub struct AdvancedConfig {
23 pub trust_forwarded_host: bool,
26
27 pub ip_address: IpAddressConfig,
29
30 pub disable_csrf_check: Option<bool>,
33
34 pub disable_origin_check: bool,
40
41 pub disable_origin_check_paths: Vec<String>,
44
45 pub skip_trailing_slashes: bool,
47
48 pub use_secure_cookies: Option<bool>,
52
53 pub cross_sub_domain_cookies: Option<CrossSubDomainConfig>,
55
56 pub cookies: HashMap<String, CookieOverride>,
61
62 pub default_cookie_attributes: CookieAttributes,
65
66 pub cookie_prefix: Option<String>,
69
70 pub database: AdvancedDatabaseConfig,
72}
73
74#[derive(Debug, Clone)]
76pub struct IpAddressConfig {
77 pub headers: Vec<String>,
80
81 pub trusted_proxies: Vec<String>,
86
87 pub ipv6_subnet: f64,
91
92 pub localhost_fallback: bool,
96
97 pub disable_ip_tracking: bool,
99}
100
101#[derive(Debug, Clone, Default)]
103pub struct CrossSubDomainConfig {
104 pub domain: String,
107}
108
109#[derive(Debug, Clone, Default)]
111pub struct CookieAttributes {
112 pub secure: Option<bool>,
114 pub http_only: Option<bool>,
116 pub same_site: Option<SameSite>,
118 pub path: Option<String>,
120 pub max_age: Option<f64>,
122 pub expires: Option<chrono::DateTime<chrono::Utc>>,
124 pub partitioned: Option<bool>,
126 pub domain: Option<String>,
128}
129
130#[derive(Debug, Clone, Default)]
132pub struct CookieOverride {
133 pub name: Option<String>,
135 pub attributes: CookieAttributes,
137}
138
139#[derive(Debug, Clone)]
141pub struct TwoFactorDatabaseConfig {
142 pub table_name: String,
143 pub columns: std::collections::HashMap<String, String>,
145}
146
147#[derive(Debug, Clone)]
149pub struct AdvancedDatabaseConfig {
150 pub default_find_many_limit: usize,
152
153 pub schema_name: Option<String>,
155
156 pub two_factor: Option<TwoFactorDatabaseConfig>,
158
159 pub sso_provider: Option<SsoProviderDatabaseConfig>,
161
162 pub sso_provider_fields: crate::field_policy::SessionFields,
164
165 pub use_number_id: bool,
168
169 pub generate_id: Option<DatabaseIdStrategy>,
171}
172
173impl Default for IpAddressConfig {
174 fn default() -> Self {
175 Self {
176 headers: vec!["x-forwarded-for".to_owned()],
177 trusted_proxies: Vec::new(),
178 ipv6_subnet: 64.0,
179 localhost_fallback: matches!(
180 std::env::var("NODE_ENV").as_deref(),
181 Ok("dev" | "development" | "test")
182 ) || std::env::var("TEST")
183 .is_ok_and(|value| !matches!(value.as_str(), "" | "false")),
184 disable_ip_tracking: false,
185 }
186 }
187}
188
189impl Default for AdvancedDatabaseConfig {
190 fn default() -> Self {
191 Self {
192 default_find_many_limit: 100,
193 schema_name: None,
194 two_factor: None,
195 sso_provider: None,
196 sso_provider_fields: crate::field_policy::SessionFields::default(),
197 use_number_id: false,
198 generate_id: None,
199 }
200 }
201}
202
203impl TwoFactorDatabaseConfig {
204 pub fn migration_statements(&self) -> crate::AuthResult<Vec<String>> {
206 const COLUMNS: &[&str] = &[
207 "id",
208 "secret",
209 "backup_codes",
210 "user_id",
211 "verified",
212 "failed_verification_count",
213 "locked_until",
214 ];
215 if self.table_name.is_empty()
216 || self
217 .columns
218 .iter()
219 .any(|(name, column)| !COLUMNS.contains(&name.as_str()) || column.is_empty())
220 {
221 return Err(AuthError::config("Invalid two-factor storage mapping"));
222 }
223 let quote = |name: &str| format!("\"{}\"", name.replace('"', "\"\""));
224 let table = quote(&self.table_name);
225 let mut sql = Vec::new();
226 if self.table_name != "twoFactor" {
227 sql.push(format!("ALTER TABLE \"twoFactor\" RENAME TO {table}"));
228 }
229 for name in COLUMNS {
230 if let Some(column) = self
231 .columns
232 .get(*name)
233 .filter(|column| column.as_str() != two_factor_column(name))
234 {
235 sql.push(format!(
236 "ALTER TABLE {table} RENAME COLUMN {} TO {}",
237 quote(two_factor_column(name)),
238 quote(column)
239 ));
240 }
241 }
242 Ok(sql)
243 }
244}
245
246pub(crate) fn two_factor_column(name: &str) -> &str {
248 match name {
249 "backup_codes" => "backupCodes",
250 "user_id" => "userId",
251 "failed_verification_count" => "failedVerificationCount",
252 "locked_until" => "lockedUntil",
253 name => name,
254 }
255}
256
257#[derive(Debug, Clone)]
259pub struct SsoProviderDatabaseConfig {
260 pub table_name: String,
261 pub columns: std::collections::HashMap<String, String>,
263}
264impl Default for SsoProviderDatabaseConfig {
265 fn default() -> Self {
266 Self {
267 table_name: "ssoProvider".into(),
268 columns: std::collections::HashMap::default(),
269 }
270 }
271}
272impl SsoProviderDatabaseConfig {
273 #[must_use]
276 pub fn predicate_field<'a>(&'a self, field: &'a str) -> &'a str {
277 let physical = self
278 .columns
279 .iter()
280 .find(|(name, _)| sso_provider_column(name) == field)
281 .map_or(field, |(_, physical)| physical.as_str());
282 if [
283 "id",
284 "issuer",
285 "domain",
286 "providerId",
287 "userId",
288 "organizationId",
289 "oidcConfig",
290 "samlConfig",
291 "domainVerified",
292 ]
293 .contains(&physical)
294 {
295 physical
296 } else {
297 field
298 }
299 }
300
301 pub fn migration_statements(&self) -> crate::AuthResult<Vec<String>> {
303 const COLUMNS: &[&str] = &[
304 "id",
305 "issuer",
306 "domain",
307 "provider_id",
308 "user_id",
309 "organization_id",
310 "oidc_config",
311 "saml_config",
312 "domain_verified",
313 ];
314 if self.table_name.is_empty()
315 || self
316 .columns
317 .iter()
318 .any(|(name, column)| !COLUMNS.contains(&name.as_str()) || column.is_empty())
319 {
320 return Err(AuthError::config("Invalid SSO provider storage mapping"));
321 }
322 let quote = |name: &str| format!("\"{}\"", name.replace('"', "\"\""));
323 let table = quote(&self.table_name);
324 let mut sql = Vec::new();
325 if self.table_name != "ssoProvider" {
326 sql.push(format!("ALTER TABLE \"ssoProvider\" RENAME TO {table}"));
327 }
328 let nonce = uuid::Uuid::new_v4();
329 let mut destinations = Vec::new();
330 for (index, name) in COLUMNS.iter().enumerate() {
331 if let Some(column) = self
332 .columns
333 .get(*name)
334 .filter(|column| column.as_str() != sso_provider_column(name))
335 {
336 let temporary = format!("__alibi_sso_{nonce}_{index}");
337 sql.push(format!(
338 "ALTER TABLE {table} RENAME COLUMN {} TO {}",
339 quote(sso_provider_column(name)),
340 quote(&temporary)
341 ));
342 destinations.push((temporary, column));
343 }
344 }
345 for (temporary, column) in destinations {
346 sql.push(format!(
347 "ALTER TABLE {table} RENAME COLUMN {} TO {}",
348 quote(&temporary),
349 quote(column)
350 ));
351 }
352 Ok(sql)
353 }
354}
355pub(crate) fn sso_provider_column(name: &str) -> &str {
356 match name {
357 "provider_id" => "providerId",
358 "user_id" => "userId",
359 "organization_id" => "organizationId",
360 "oidc_config" => "oidcConfig",
361 "saml_config" => "samlConfig",
362 "domain_verified" => "domainVerified",
363 name => name,
364 }
365}