Skip to main content

shared/config/configuration/
mod.rs

1pub mod validate;
2
3use serde::{Deserialize, Serialize};
4use utoipa::ToSchema;
5
6use crate::error::{CoreError, InternalError};
7
8use super::boot::AppConfig;
9use super::boot::cache::CacheDriver;
10use super::boot::database::DatabaseDriver;
11use super::validate::Validate;
12
13#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
14pub struct AnzarConfiguration {
15    pub app: App,           // Required
16    pub database: Database, // Required
17    #[serde(default)]
18    pub server: Server, // [Optional] Uses Default
19    #[serde(default)]
20    pub auth: Authentication, // [Optional] Uses Default
21    pub security: Security, // Required
22}
23
24impl AnzarConfiguration {
25    pub fn validate(&self) -> Result<(), CoreError> {
26        let mut errors = vec![];
27
28        if let Err(e) = self.auth.validate() {
29            errors.extend(e);
30        }
31        if let Err(e) = self.security.validate() {
32            errors.extend(e);
33        }
34
35        if errors.is_empty() {
36            Ok(())
37        } else {
38            Err(CoreError::Internal(InternalError::InvalidConfig(errors)))
39        }
40    }
41}
42
43impl AnzarConfiguration {
44    pub fn new(app_config: AppConfig) -> Self {
45        Self {
46            app: App {
47                environment: "dev".into(),
48                url: "localhost:3000".to_string(),
49            },
50            database: Database {
51                driver: app_config.database.driver,
52                connection_string: app_config.database.connection_string(),
53                cache: Cache {
54                    driver: app_config.cache.driver,
55                    url: app_config.cache.url,
56                },
57            },
58            server: Server::default(),
59            auth: Authentication {
60                strategy: app_config.auth,
61                ..Default::default()
62            },
63            security: Security {
64                secret_key: String::default(),
65                rate_limit: RateLimit {
66                    enabled: true,
67                    ip: RateLimitConfig::ip(),
68                    strict: RateLimitConfig {
69                        duration_minutes: 60,
70                        capacity: 7,
71                    },
72                    default: RateLimitConfig::defaults(),
73                },
74                headers: vec![],
75                auth: AuthSecurity {
76                    max_failed_attempts: 5,
77                    lockout_duration: 1800,
78                },
79            },
80        }
81    }
82    pub fn with_appurl(mut self, url: &str) -> Self {
83        self.app.url = url.to_string();
84        self
85    }
86    pub fn with_secret(mut self, key: &str) -> Self {
87        self.security.secret_key = key.to_string();
88        self
89    }
90}
91
92// =============================================================================
93// API Configuration - REQUIRED
94// =============================================================================
95#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
96pub struct App {
97    pub environment: String,
98    pub url: String,
99}
100
101// =============================================================================
102// Database Configuration - REQUIRED
103// =============================================================================
104#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
105pub struct Database {
106    pub driver: DatabaseDriver,
107    pub connection_string: String,
108    pub cache: Cache,
109}
110impl Database {
111    pub fn name(&self) -> Option<&str> {
112        self.connection_string
113            .rsplit('/')
114            .next()
115            .and_then(|s| s.split('?').next())
116    }
117}
118// Cache
119// ------------------------------------------------------------
120#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
121pub struct Cache {
122    pub driver: CacheDriver,
123    pub url: String,
124}
125
126// =============================================================================
127// Server Configuration - Optional
128// =============================================================================
129#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
130#[serde(default)]
131pub struct Server {
132    pub https: HttpsConfig,
133    pub cors: CorsConfig,
134}
135// HttpsConfig
136// ------------------------------------------------------------
137#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
138#[serde(default)]
139pub struct HttpsConfig {
140    pub enabled: bool,
141    pub port: u16,
142    pub cert_path: Option<String>,
143    pub key_path: Option<String>,
144}
145impl Default for HttpsConfig {
146    fn default() -> Self {
147        Self {
148            enabled: false,
149            port: 3000,
150            cert_path: None,
151            key_path: None,
152        }
153    }
154}
155// CorsConfig
156// ------------------------------------------------------------
157#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
158#[serde(default)]
159pub struct CorsConfig {
160    pub enabled: bool,
161    pub allowed_origins: Vec<String>,
162    pub allowed_methods: Vec<String>,
163    pub allowed_headers: Vec<String>,
164    pub allow_credentials: bool,
165    pub max_age: u64,
166}
167impl Default for CorsConfig {
168    fn default() -> Self {
169        Self {
170            enabled: true,
171            allowed_origins: vec!["localhost:3000".into()],
172            allowed_methods: vec![
173                "GET".into(),
174                "POST".into(),
175                "PUT".into(),
176                "DELETE".into(),
177                "OPTIONS".into(),
178            ],
179            allowed_headers: vec![
180                "authorization".into(),
181                "content-type".into(),
182                "accept".into(),
183                "accept-language".into(),
184                "Content-Language".into(),
185            ],
186            allow_credentials: true,
187            max_age: 3600,
188        }
189    }
190}
191
192// =============================================================================
193// Authentication Configuration - Optional
194// =============================================================================
195#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
196#[serde(default)]
197pub struct Authentication {
198    pub strategy: AuthStrategy,
199    pub email: EmailConfig,
200    pub password: PasswordConfig,
201    pub rbac: RbacConfig,
202}
203impl Authentication {
204    pub fn jwt(&self) -> Result<&JwtConfig, CoreError> {
205        match &self.strategy {
206            AuthStrategy::Jwt(config) => Ok(config),
207            _ => Err(CoreError::Internal(InternalError::MissingConfiguration(
208                "JWT strategy is required, but auth.strategy was not configured correctly".into(),
209            ))),
210        }
211    }
212    pub fn session(&self) -> Result<&SessionConfig, CoreError> {
213        match &self.strategy {
214            AuthStrategy::Session(config) => Ok(config),
215            _ => Err(CoreError::Internal(InternalError::MissingConfiguration(
216                "Session strategy is required, but auth.strategy was not configured correctly"
217                    .into(),
218            ))),
219        }
220    }
221}
222// AuthStrategy
223// ------------------------------------------------------------
224#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
225#[serde(tag = "type")]
226pub enum AuthStrategy {
227    Session(SessionConfig),
228    Jwt(JwtConfig),
229}
230impl Default for AuthStrategy {
231    fn default() -> Self {
232        Self::Session(SessionConfig::default())
233    }
234}
235
236impl std::fmt::Display for AuthStrategy {
237    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
238        match self {
239            AuthStrategy::Session(_) => write!(f, "Session"),
240            AuthStrategy::Jwt(_) => write!(f, "Jwt"),
241        }
242    }
243}
244// JwtConfig
245// ------------------------------------------------------------
246#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
247#[serde(default)]
248pub struct JwtConfig {
249    pub algorithm: AlgorithmConfig,
250    pub access_token_expires_in: i64,
251    pub refresh_token_expires_in: i64,
252    pub issuer: String,
253    pub audience: String,
254}
255impl Default for JwtConfig {
256    fn default() -> Self {
257        Self {
258            algorithm: AlgorithmConfig::default(),
259            access_token_expires_in: 900,
260            refresh_token_expires_in: 604800,
261            issuer: String::new(),
262            audience: String::new(),
263        }
264    }
265}
266//
267#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
268pub enum AlgorithmConfig {
269    ES256,
270    ES384,
271    #[default]
272    RS256,
273    RS384,
274    RS512,
275    PS256,
276    PS384,
277    PS512,
278    EdDSA,
279}
280impl AlgorithmConfig {
281    pub fn as_str(&self) -> &'static str {
282        match self {
283            AlgorithmConfig::ES256 => "ES256",
284            AlgorithmConfig::ES384 => "ES384",
285            AlgorithmConfig::RS256 => "RS256",
286            AlgorithmConfig::RS384 => "RS384",
287            AlgorithmConfig::RS512 => "RS512",
288            AlgorithmConfig::PS256 => "PS256",
289            AlgorithmConfig::PS384 => "PS384",
290            AlgorithmConfig::PS512 => "PS512",
291            AlgorithmConfig::EdDSA => "EdDSA",
292        }
293    }
294}
295impl From<AlgorithmConfig> for jsonwebtoken::Algorithm {
296    fn from(value: AlgorithmConfig) -> Self {
297        match value {
298            AlgorithmConfig::ES256 => jsonwebtoken::Algorithm::ES256,
299            AlgorithmConfig::ES384 => jsonwebtoken::Algorithm::ES384,
300            AlgorithmConfig::RS256 => jsonwebtoken::Algorithm::RS256,
301            AlgorithmConfig::RS384 => jsonwebtoken::Algorithm::RS384,
302            AlgorithmConfig::PS256 => jsonwebtoken::Algorithm::PS256,
303            AlgorithmConfig::PS384 => jsonwebtoken::Algorithm::PS384,
304            AlgorithmConfig::PS512 => jsonwebtoken::Algorithm::PS512,
305            AlgorithmConfig::RS512 => jsonwebtoken::Algorithm::RS512,
306            AlgorithmConfig::EdDSA => jsonwebtoken::Algorithm::EdDSA,
307        }
308    }
309}
310// SessionConfig
311// ------------------------------------------------------------
312#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
313#[serde(default)]
314pub struct SessionConfig {
315    pub name: String,
316    pub max_age: u64,
317    pub secure: bool,
318    pub http_only: bool,
319    pub same_site: SameSiteConfig,
320}
321#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
322pub enum SameSiteConfig {
323    #[default]
324    Strict,
325    Lax,
326    None,
327}
328impl Default for SessionConfig {
329    fn default() -> Self {
330        Self {
331            name: "id".into(),
332            max_age: 3600,
333            secure: true,
334            http_only: true,
335            same_site: SameSiteConfig::default(),
336        }
337    }
338}
339
340// EmailConfig
341// ------------------------------------------------------------
342#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
343#[serde(default)]
344pub struct EmailConfig {
345    pub verification: EmailVerification,
346}
347// ************************************************************
348#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
349#[serde(default)]
350pub struct EmailVerification {
351    pub required: bool,
352    pub token_expires_in: i64, // maybe option
353    pub success_redirect: Option<String>,
354    pub error_redirect: Option<String>,
355}
356impl Default for EmailVerification {
357    fn default() -> Self {
358        Self {
359            required: false,
360            token_expires_in: 1800,
361            success_redirect: None,
362            error_redirect: None,
363        }
364    }
365}
366
367// PasswordConfig
368// ------------------------------------------------------------
369#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
370#[serde(default)]
371pub struct PasswordConfig {
372    pub algorithm: HashingAlgorithm,
373    pub requirements: PasswordRequirements,
374    pub reset: PasswordReset,
375}
376// ************************************************************
377#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
378#[serde(tag = "type")]
379pub enum HashingAlgorithm {
380    Argon2 {
381        memory_kib: u32,
382        iterations: u32,
383        parallelism: u32,
384    },
385    Bcrypt {
386        // const MIN_COST: u32 = 4;
387        // const MAX_COST: u32 = 31;
388        // pub const DEFAULT_COST: u32 = 12;
389        cost: u32,
390    },
391}
392impl Default for HashingAlgorithm {
393    fn default() -> Self {
394        pub const DEFAULT_M_COST: u32 = 19 * 1024; // ~19 MiB
395        pub const DEFAULT_T_COST: u32 = 2;
396        pub const DEFAULT_P_COST: u32 = 1;
397
398        Self::Argon2 {
399            memory_kib: DEFAULT_M_COST,
400            iterations: DEFAULT_T_COST,
401            parallelism: DEFAULT_P_COST,
402        }
403    }
404}
405
406// ************************************************************
407#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
408#[serde(default)]
409pub struct PasswordRequirements {
410    pub min_length: u16,
411    pub max_length: u16,
412    pub require_uppercase: bool,
413    pub require_number: bool,
414    pub require_special_char: bool,
415}
416impl Default for PasswordRequirements {
417    fn default() -> Self {
418        Self {
419            min_length: 8,
420            max_length: 128,
421            require_uppercase: false,
422            require_number: false,
423            require_special_char: false,
424        }
425    }
426}
427// ************************************************************
428#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
429#[serde(default)]
430pub struct PasswordReset {
431    pub token_expires_in: i64, // maybe option
432    // TODO: remove option and use redirect to root
433    pub success_redirect: Option<String>,
434    pub error_redirect: Option<String>,
435}
436impl Default for PasswordReset {
437    fn default() -> Self {
438        Self {
439            token_expires_in: 1800,
440            success_redirect: None,
441            error_redirect: None,
442        }
443    }
444}
445
446// RbacConfig
447// ------------------------------------------------------------
448#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
449#[serde(default)]
450pub struct RbacConfig {
451    pub enabled: bool,
452    pub default_role: String,
453    pub roles: Vec<RoleConfig>,
454}
455
456#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
457pub struct RoleConfig {
458    pub name: String,
459    #[serde(default)]
460    pub inherits: Vec<String>,
461    pub permissions: Vec<String>,
462}
463
464impl Default for RbacConfig {
465    fn default() -> Self {
466        Self {
467            enabled: false,
468            default_role: "user".into(),
469            roles: vec![RoleConfig {
470                name: "user".into(),
471                inherits: vec![],
472                permissions: vec!["*:read".into()],
473            }],
474        }
475    }
476}
477
478// =============================================================================
479// Security Configuration - REQUIRED
480// =============================================================================
481#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
482pub struct Security {
483    #[serde(skip_serializing)]
484    pub secret_key: String,
485
486    #[serde(default)]
487    pub auth: AuthSecurity,
488
489    #[serde(default)]
490    pub rate_limit: RateLimit,
491
492    #[serde(default = "default_headers")]
493    pub headers: Vec<(String, String)>,
494}
495
496// ************************************************************
497#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
498#[serde(default)]
499pub struct AuthSecurity {
500    pub max_failed_attempts: u8,
501    pub lockout_duration: i64,
502}
503impl Default for AuthSecurity {
504    fn default() -> Self {
505        Self {
506            max_failed_attempts: 5,
507            lockout_duration: 1800,
508        }
509    }
510}
511
512// RateLimit
513// ------------------------------------------------------------
514#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
515pub struct RateLimit {
516    #[serde(default)]
517    pub enabled: bool,
518
519    #[serde(default = "RateLimitConfig::ip")]
520    pub ip: RateLimitConfig,
521
522    #[serde(default = "RateLimitConfig::strict")]
523    pub strict: RateLimitConfig,
524
525    #[serde(default = "RateLimitConfig::defaults")]
526    pub default: RateLimitConfig,
527}
528
529impl Default for RateLimit {
530    fn default() -> Self {
531        Self {
532            enabled: false,
533            ip: RateLimitConfig::ip(),
534            strict: RateLimitConfig::strict(),
535            default: RateLimitConfig::defaults(),
536        }
537    }
538}
539
540// RateLimitConfig
541// ------------------------------------------------------------
542#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
543pub struct RateLimitConfig {
544    pub capacity: u32,
545    pub duration_minutes: u32,
546}
547impl RateLimitConfig {
548    fn ip() -> RateLimitConfig {
549        RateLimitConfig {
550            duration_minutes: 1,
551            capacity: 100,
552        }
553    }
554    fn strict() -> RateLimitConfig {
555        RateLimitConfig {
556            duration_minutes: 60,
557            capacity: 5,
558        }
559    }
560    fn defaults() -> RateLimitConfig {
561        RateLimitConfig {
562            duration_minutes: 15,
563            capacity: 20,
564        }
565    }
566}
567
568// Headers
569// ------------------------------------------------------------
570fn default_headers() -> Vec<(String, String)> {
571    vec![
572        ("X-Content-Type-Options".into(), "nosniff".into()),
573        ("X-Frame-Options".into(), "DENY".into()),
574        ("X-XSS-Protection".into(), "0".into()),
575        ("Cache-Control".into(), "no-store".into()),
576        ("Pragma".into(), "no-cache".into()),
577        (
578            "Content-Security-Policy".into(),
579            "default-src 'self'".into(),
580        ),
581        ("Content-Type".into(), "application/json".into()),
582        (
583            "Strict-Transport-Security".into(),
584            "max-age=31536000".into(),
585        ),
586    ]
587}
588
589// humantime-serde is great for this — lets you write "15m" in config files.
590// server:
591//   request:
592//     timeout_ms: 30000
593//     max_body_size: "2mb"
594
595// =============================================================================
596// Logging Configuration
597// =============================================================================
598// logging:
599//   level: "${LOG_LEVEL:info}"    # debug | info | warn | error
600//   format: "json"                # json | text
601//   redact: ["password", "token", "secret", "authorization"]