1use std::env;
2
3use super::SecurityError;
4
5#[derive(Debug, Clone)]
7pub struct SecurityConfig {
8 pub jwt_secret: String,
10 pub jwt_expires_in_secs: u64,
12 pub jwt_issuer: Option<String>,
14 pub public_routes: Vec<String>,
16}
17
18impl SecurityConfig {
19 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 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}