1use crate::mode::AppMode;
9use serde::Deserialize;
10use std::path::Path;
11
12pub trait IAppOptions: for<'de> Deserialize<'de> + Default + Send + Sync + 'static {}
21
22impl<T> IAppOptions for T where T: for<'de> Deserialize<'de> + Default + Send + Sync + 'static {}
23
24#[derive(Debug, Clone, Deserialize)]
30pub struct AppSection {
31 #[serde(default, rename = "Name")]
33 pub name: String,
34 #[serde(default = "default_urls", rename = "Urls")]
37 pub urls: Vec<String>,
38 #[serde(default = "default_max_body_size", rename = "MaxBodySize")]
40 pub max_body_size: usize,
41 #[serde(default = "default_max_connections", rename = "MaxConnections")]
43 pub max_connections: usize,
44}
45
46impl Default for AppSection {
47 fn default() -> Self {
48 Self {
49 name: String::new(),
50 urls: default_urls(),
51 max_body_size: default_max_body_size(),
52 max_connections: default_max_connections(),
53 }
54 }
55}
56
57fn default_urls() -> Vec<String> {
58 vec!["http://0.0.0.0:5000".to_string()]
59}
60
61fn default_max_body_size() -> usize {
62 10 * 1024 * 1024 }
64
65fn default_max_connections() -> usize {
66 10_000
67}
68
69#[derive(Debug, Clone, Deserialize, Default)]
71pub struct JwtSection {
72 #[serde(default, rename = "Secret")]
74 pub secret: String,
75}
76
77#[derive(Debug, Clone, Deserialize)]
82#[serde(rename_all = "PascalCase")]
83pub struct CorsSection {
84 #[serde(default = "default_origins")]
86 pub origins: Vec<String>,
87 #[serde(default = "default_cors_methods")]
89 pub methods: Vec<String>,
90 #[serde(default = "default_cors_headers")]
92 pub headers: Vec<String>,
93 #[serde(default)]
95 pub expose_headers: Vec<String>,
96 #[serde(default)]
98 pub allow_credentials: bool,
99 #[serde(default = "default_max_age")]
101 pub max_age: u32,
102}
103
104impl Default for CorsSection {
105 fn default() -> Self {
106 Self {
107 origins: default_origins(),
108 methods: default_cors_methods(),
109 headers: default_cors_headers(),
110 expose_headers: Vec::new(),
111 allow_credentials: false,
112 max_age: default_max_age(),
113 }
114 }
115}
116
117fn default_origins() -> Vec<String> {
118 vec!["*".to_string()]
119}
120
121fn default_cors_methods() -> Vec<String> {
122 vec![
123 "GET".to_string(),
124 "POST".to_string(),
125 "PUT".to_string(),
126 "DELETE".to_string(),
127 "PATCH".to_string(),
128 "OPTIONS".to_string(),
129 ]
130}
131
132fn default_cors_headers() -> Vec<String> {
133 vec!["Content-Type".to_string(), "Authorization".to_string()]
134}
135
136fn default_max_age() -> u32 {
137 86400
138}
139
140#[derive(Debug, Clone, Deserialize)]
142pub struct RateLimitSection {
143 #[serde(default, rename = "Enabled")]
145 pub enabled: bool,
146 #[serde(default = "default_rate_limit_rps", rename = "RequestsPerSecond")]
148 pub requests_per_second: f64,
149 #[serde(default = "default_rate_limit_burst", rename = "BurstSize")]
151 pub burst_size: u32,
152 #[serde(default = "default_rate_limit_max_ips", rename = "MaxTrackedIps")]
154 pub max_tracked_ips: usize,
155 #[serde(default, rename = "TrustProxy")]
158 pub trust_proxy: bool,
159}
160
161impl Default for RateLimitSection {
162 fn default() -> Self {
163 Self {
164 enabled: false,
165 requests_per_second: default_rate_limit_rps(),
166 burst_size: default_rate_limit_burst(),
167 max_tracked_ips: default_rate_limit_max_ips(),
168 trust_proxy: false,
169 }
170 }
171}
172
173fn default_rate_limit_rps() -> f64 {
174 100.0
175}
176
177fn default_rate_limit_burst() -> u32 {
178 200
179}
180
181fn default_rate_limit_max_ips() -> usize {
182 10_000
183}
184
185#[derive(Debug, Clone, Deserialize, Default)]
187pub struct MetricsSection {
188 #[serde(default, rename = "Enabled")]
189 pub enabled: bool,
190}
191
192#[derive(Debug, Clone, Deserialize, Default)]
198pub struct TlsSection {
199 #[serde(default, rename = "CertPath")]
201 pub cert_path: String,
202 #[serde(default, rename = "KeyPath")]
204 pub key_path: String,
205}
206
207#[derive(Debug, Clone, Deserialize, Default)]
212pub struct AppOptions {
213 #[serde(default, rename = "App")]
215 pub app: AppSection,
216 #[serde(default, rename = "Jwt")]
218 pub jwt: JwtSection,
219 #[serde(default, rename = "Cors")]
221 pub cors: CorsSection,
222 #[serde(default, rename = "Tls")]
224 pub tls: TlsSection,
225 #[serde(default, rename = "RateLimit")]
227 pub rate_limit: RateLimitSection,
228 #[serde(default, rename = "Metrics")]
230 pub metrics: MetricsSection,
231}
232
233pub fn load_appsettings(mode: AppMode) -> Option<serde_json::Value> {
246 let mut base = read_json_file("appsettings.json")?;
247
248 let overlay_name = mode.overlay_filename();
249 if let Some(overlay) = read_json_file(overlay_name) {
250 merge_json(&mut base, overlay);
251 }
252
253 apply_well_known_env_overrides(&mut base);
255
256 apply_env_overrides(&mut base);
258
259 Some(base)
260}
261
262fn apply_well_known_env_overrides(config: &mut serde_json::Value) {
266 if let Ok(secret) = std::env::var("JWT_SECRET") {
267 if !secret.is_empty() {
268 set_json_value(config, &["Jwt", "Secret"], &secret);
269 }
270 }
271}
272
273fn apply_env_overrides(config: &mut serde_json::Value) {
275 for (key, value) in std::env::vars() {
276 if let Some(path) = key.strip_prefix("APP__") {
277 let segments: Vec<&str> = path.split("__").collect();
279 if segments.is_empty() {
280 continue;
281 }
282 set_json_value(config, &segments, &value);
283 }
284 }
285}
286
287fn set_json_value(obj: &mut serde_json::Value, segments: &[&str], value: &str) {
289 if segments.is_empty() {
290 return;
291 }
292
293 let key = segments[0];
294
295 if let serde_json::Value::Object(map) = obj {
296 if segments.len() == 1 {
297 let parsed =
299 serde_json::from_str(value).unwrap_or(serde_json::Value::String(value.to_string()));
300 map.insert(key.to_string(), parsed);
301 } else if let Some(child) = map.get_mut(key) {
302 set_json_value(child, &segments[1..], value);
304 } else {
305 let mut child = serde_json::json!({});
307 set_json_value(&mut child, &segments[1..], value);
308 map.insert(key.to_string(), child);
309 }
310 }
311}
312
313pub fn bind_config<T: for<'de> Deserialize<'de> + Default>(
315 config: &serde_json::Value,
316 section: &str,
317) -> T {
318 if section.is_empty() || section == "." {
319 serde_json::from_value(config.clone()).unwrap_or_else(|e| {
320 tracing::warn!("[Config] Failed to bind root config to {}: {}, using defaults", std::any::type_name::<T>(), e);
321 T::default()
322 })
323 } else {
324 config
325 .get(section)
326 .map(|v| serde_json::from_value(v.clone()).unwrap_or_else(|e| {
327 tracing::warn!("[Config] Failed to bind section '{}' to {}: {}, using defaults", section, std::any::type_name::<T>(), e);
328 T::default()
329 }))
330 .unwrap_or_default()
331 }
332}
333
334pub fn bind_root<T: for<'de> Deserialize<'de> + Default>(config: &serde_json::Value) -> T {
336 serde_json::from_value(config.clone()).unwrap_or_else(|e| {
337 tracing::warn!("[Config] Failed to bind root config to {}: {}, using defaults", std::any::type_name::<T>(), e);
338 T::default()
339 })
340}
341
342fn read_json_file(path: impl AsRef<Path>) -> Option<serde_json::Value> {
347 let path = path.as_ref();
348
349 fn try_read(path: &Path) -> Option<serde_json::Value> {
351 let content = std::fs::read_to_string(path).ok()?;
352 serde_json::from_str(&content).ok()
353 }
354
355 let base = crate::paths::app_base();
358 let candidate = base.join(path);
359 if let Some(value) = try_read(&candidate) {
360 return Some(value);
361 }
362
363 if let Some(value) = try_read(path) {
365 return Some(value);
366 }
367
368 None
369}
370
371fn merge_json(base: &mut serde_json::Value, overlay: serde_json::Value) {
372 match (base, overlay) {
373 (serde_json::Value::Object(a), serde_json::Value::Object(b)) => {
374 for (k, v) in b {
375 merge_json(a.entry(k).or_insert(serde_json::Value::Null), v);
376 }
377 }
378 (a, b) => *a = b,
379 }
380}
381
382#[cfg(test)]
383mod tests {
384 use super::*;
385
386 #[test]
387 fn config_env_jwt_and_app_override_precedence() {
388 std::env::set_var("JWT_SECRET", "env-jwt-secret-that-is-long-enough-32");
389 let mut config = serde_json::json!({ "Jwt": { "Secret": "from-file" } });
390 apply_well_known_env_overrides(&mut config);
391 assert_eq!(
392 config["Jwt"]["Secret"],
393 "env-jwt-secret-that-is-long-enough-32"
394 );
395
396 std::env::set_var(
397 "APP__Jwt__Secret",
398 "from-app-override-secret-32chars-minimum",
399 );
400 apply_env_overrides(&mut config);
401 std::env::remove_var("JWT_SECRET");
402 std::env::remove_var("APP__Jwt__Secret");
403 assert_eq!(
404 config["Jwt"]["Secret"],
405 "from-app-override-secret-32chars-minimum"
406 );
407 }
408
409 #[test]
410 fn cors_section_binds_pascal_case_from_appsettings() {
411 let json = serde_json::json!({
414 "Cors": {
415 "Origins": ["https://www.lusida.net", "https://lusida.net"],
416 "Methods": ["GET", "POST"],
417 "Headers": ["Content-Type", "Authorization"],
418 "AllowCredentials": true,
419 "MaxAge": 3600
420 }
421 });
422 let opts: AppOptions = bind_root(&json);
423 assert_eq!(
424 opts.cors.origins,
425 vec![
426 "https://www.lusida.net".to_string(),
427 "https://lusida.net".to_string()
428 ]
429 );
430 assert_eq!(opts.cors.methods, vec!["GET".to_string(), "POST".to_string()]);
431 assert!(opts.cors.allow_credentials);
432 assert_eq!(opts.cors.max_age, 3600);
433 assert!(!opts.cors.origins.iter().any(|o| o == "*"));
434 }
435
436 #[test]
437 fn production_overlay_merge_keeps_explicit_cors_origins() {
438 let mut base = serde_json::json!({
439 "App": { "Urls": ["http://localhost:5000"] },
440 "Cors": { "Origins": ["*"] },
441 "Jwt": { "Secret": "docbit-dev-secret-change-in-production" }
442 });
443 let overlay = serde_json::json!({
444 "App": { "Urls": ["http://0.0.0.0:8100"] },
445 "Cors": {
446 "Origins": [
447 "https://www.lusida.net",
448 "https://lusida.net"
449 ]
450 },
451 "Jwt": { "Secret": "" }
452 });
453 merge_json(&mut base, overlay);
454 let opts: AppOptions = bind_root(&base);
455 assert_eq!(opts.app.urls, vec!["http://0.0.0.0:8100".to_string()]);
456 assert_eq!(
457 opts.cors.origins,
458 vec![
459 "https://www.lusida.net".to_string(),
460 "https://lusida.net".to_string()
461 ]
462 );
463 assert!(opts.jwt.secret.is_empty());
464 }
465}