Skip to main content

ruest/security/
config.rs

1use std::env;
2
3use super::SecurityError;
4
5/// Configuration JWT / sécurité (variables d'environnement ou builder).
6#[derive(Debug, Clone)]
7pub struct SecurityConfig {
8    /// Secret HMAC (min. 32 caractères recommandé en production).
9    pub jwt_secret: String,
10    /// Durée de vie du token en secondes.
11    pub jwt_expires_in_secs: u64,
12    /// Émetteur optionnel (`iss` claim).
13    pub jwt_issuer: Option<String>,
14    /// Chemins HTTP publics (sans Bearer), ex. `/health`, `/auth/login`.
15    pub public_routes: Vec<String>,
16}
17
18impl SecurityConfig {
19    /// Valeurs de développement — **ne pas utiliser en production**.
20    pub fn dev() -> Self {
21        Self {
22            jwt_secret: "ruest-dev-secret-change-in-production!!".into(),
23            jwt_expires_in_secs: 3600,
24            jwt_issuer: Some("ruest".into()),
25            public_routes: vec![
26                "/health".into(),
27                "/auth/login".into(),
28                "/auth/register".into(),
29            ],
30        }
31    }
32
33    pub fn builder() -> SecurityConfigBuilder {
34        SecurityConfigBuilder::default()
35    }
36
37    /// Charge depuis l'environnement :
38    /// `RUEST_JWT_SECRET`, `RUEST_JWT_EXPIRES_IN_SECS`, `RUEST_JWT_ISSUER`,
39    /// `RUEST_PUBLIC_ROUTES` (séparés par des virgules).
40    pub fn from_env() -> Result<Self, SecurityError> {
41        let jwt_secret = env::var("RUEST_JWT_SECRET").map_err(|_| {
42            SecurityError::Config(
43                "RUEST_JWT_SECRET is not set (use SecurityConfig::dev() for local dev)".into(),
44            )
45        })?;
46
47        let jwt_expires_in_secs = env::var("RUEST_JWT_EXPIRES_IN_SECS")
48            .ok()
49            .and_then(|v| v.parse().ok())
50            .unwrap_or(3600);
51
52        let jwt_issuer = env::var("RUEST_JWT_ISSUER").ok();
53
54        let mut public_routes = vec![
55            "/health".into(),
56            "/auth/login".into(),
57            "/auth/register".into(),
58        ];
59        if let Ok(extra) = env::var("RUEST_PUBLIC_ROUTES") {
60            for path in extra.split(',').map(str::trim).filter(|s| !s.is_empty()) {
61                public_routes.push(path.to_string());
62            }
63        }
64
65        Ok(Self {
66            jwt_secret,
67            jwt_expires_in_secs,
68            jwt_issuer,
69            public_routes,
70        })
71    }
72
73    pub fn is_public_route(&self, path: &str) -> bool {
74        self.public_routes.iter().any(|p| path == p.as_str() || path.starts_with(&format!("{p}/")))
75    }
76}
77
78#[derive(Debug, Default)]
79pub struct SecurityConfigBuilder {
80    jwt_secret: Option<String>,
81    jwt_expires_in_secs: u64,
82    jwt_issuer: Option<String>,
83    public_routes: Vec<String>,
84}
85
86impl SecurityConfigBuilder {
87    pub fn jwt_secret(mut self, secret: impl Into<String>) -> Self {
88        self.jwt_secret = Some(secret.into());
89        self
90    }
91
92    pub fn expires_in_secs(mut self, secs: u64) -> Self {
93        self.jwt_expires_in_secs = secs;
94        self
95    }
96
97    pub fn issuer(mut self, issuer: impl Into<String>) -> Self {
98        self.jwt_issuer = Some(issuer.into());
99        self
100    }
101
102    pub fn public_route(mut self, path: impl Into<String>) -> Self {
103        self.public_routes.push(path.into());
104        self
105    }
106
107    pub fn build(self) -> Result<SecurityConfig, SecurityError> {
108        let jwt_secret = self.jwt_secret.ok_or_else(|| {
109            SecurityError::Config("jwt_secret is required (call .jwt_secret(...))".into())
110        })?;
111        Ok(SecurityConfig {
112            jwt_secret,
113            jwt_expires_in_secs: if self.jwt_expires_in_secs == 0 {
114                3600
115            } else {
116                self.jwt_expires_in_secs
117            },
118            jwt_issuer: self.jwt_issuer,
119            public_routes: if self.public_routes.is_empty() {
120                SecurityConfig::dev().public_routes
121            } else {
122                self.public_routes
123            },
124        })
125    }
126}