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)]
215pub struct FormSection {
216 #[serde(default = "default_max_request_size", rename = "MaxRequestSize")]
218 pub max_request_size: usize,
219 #[serde(default = "default_max_file_size", rename = "MaxFileSize")]
221 pub max_file_size: u64,
222 #[serde(default = "default_max_field_size", rename = "MaxFieldSize")]
224 pub max_field_size: usize,
225 #[serde(default = "default_memory_threshold", rename = "MemoryThreshold")]
230 pub memory_threshold: usize,
231 #[serde(default, rename = "TempDir")]
234 pub temp_dir: Option<String>,
235}
236
237impl Default for FormSection {
238 fn default() -> Self {
239 Self {
240 max_request_size: default_max_request_size(),
241 max_file_size: default_max_file_size(),
242 max_field_size: default_max_field_size(),
243 memory_threshold: default_memory_threshold(),
244 temp_dir: None,
245 }
246 }
247}
248
249fn default_max_request_size() -> usize {
250 256 * 1024 * 1024
251}
252
253fn default_max_file_size() -> u64 {
254 128 * 1024 * 1024
255}
256
257fn default_max_field_size() -> usize {
258 1024 * 1024
259}
260
261fn default_memory_threshold() -> usize {
262 1024 * 1024
263}
264
265#[derive(Debug, Clone, Deserialize, Default)]
270pub struct AppOptions {
271 #[serde(default, rename = "App")]
273 pub app: AppSection,
274 #[serde(default, rename = "Jwt")]
276 pub jwt: JwtSection,
277 #[serde(default, rename = "Cors")]
279 pub cors: CorsSection,
280 #[serde(default, rename = "Tls")]
282 pub tls: TlsSection,
283 #[serde(default, rename = "RateLimit")]
285 pub rate_limit: RateLimitSection,
286 #[serde(default, rename = "Metrics")]
288 pub metrics: MetricsSection,
289 #[serde(default, rename = "Form")]
291 pub form: FormSection,
292}
293
294pub fn load_appsettings(mode: AppMode) -> Option<serde_json::Value> {
307 let mut base = read_json_file("appsettings.json")?;
308
309 let overlay_name = mode.overlay_filename();
310 if let Some(overlay) = read_json_file(overlay_name) {
311 merge_json(&mut base, overlay);
312 }
313
314 apply_well_known_env_overrides(&mut base);
316
317 apply_env_overrides(&mut base);
319
320 Some(base)
321}
322
323fn apply_well_known_env_overrides(config: &mut serde_json::Value) {
327 if let Ok(secret) = std::env::var("JWT_SECRET") {
328 if !secret.is_empty() {
329 set_json_value(config, &["Jwt", "Secret"], &secret);
330 }
331 }
332}
333
334fn apply_env_overrides(config: &mut serde_json::Value) {
336 for (key, value) in std::env::vars() {
337 if let Some(path) = key.strip_prefix("APP__") {
338 let segments: Vec<&str> = path.split("__").collect();
340 if segments.is_empty() {
341 continue;
342 }
343 set_json_value(config, &segments, &value);
344 }
345 }
346}
347
348fn set_json_value(obj: &mut serde_json::Value, segments: &[&str], value: &str) {
350 if segments.is_empty() {
351 return;
352 }
353
354 let key = segments[0];
355
356 if let serde_json::Value::Object(map) = obj {
357 if segments.len() == 1 {
358 let parsed =
360 serde_json::from_str(value).unwrap_or(serde_json::Value::String(value.to_string()));
361 map.insert(key.to_string(), parsed);
362 } else if let Some(child) = map.get_mut(key) {
363 set_json_value(child, &segments[1..], value);
365 } else {
366 let mut child = serde_json::json!({});
368 set_json_value(&mut child, &segments[1..], value);
369 map.insert(key.to_string(), child);
370 }
371 }
372}
373
374pub fn bind_config<T: for<'de> Deserialize<'de> + Default>(
376 config: &serde_json::Value,
377 section: &str,
378) -> T {
379 if section.is_empty() || section == "." {
380 serde_json::from_value(config.clone()).unwrap_or_else(|e| {
381 tracing::warn!(
382 "[Config] Failed to bind root config to {}: {}, using defaults",
383 std::any::type_name::<T>(),
384 e
385 );
386 T::default()
387 })
388 } else {
389 config
390 .get(section)
391 .map(|v| {
392 serde_json::from_value(v.clone()).unwrap_or_else(|e| {
393 tracing::warn!(
394 "[Config] Failed to bind section '{}' to {}: {}, using defaults",
395 section,
396 std::any::type_name::<T>(),
397 e
398 );
399 T::default()
400 })
401 })
402 .unwrap_or_default()
403 }
404}
405
406pub fn bind_root<T: for<'de> Deserialize<'de> + Default>(config: &serde_json::Value) -> T {
408 serde_json::from_value(config.clone()).unwrap_or_else(|e| {
409 tracing::warn!(
410 "[Config] Failed to bind root config to {}: {}, using defaults",
411 std::any::type_name::<T>(),
412 e
413 );
414 T::default()
415 })
416}
417
418fn read_json_file(path: impl AsRef<Path>) -> Option<serde_json::Value> {
423 let path = path.as_ref();
424
425 fn try_read(path: &Path) -> Option<serde_json::Value> {
427 let content = std::fs::read_to_string(path).ok()?;
428 serde_json::from_str(&content).ok()
429 }
430
431 let base = crate::paths::app_base();
434 let candidate = base.join(path);
435 if let Some(value) = try_read(&candidate) {
436 return Some(value);
437 }
438
439 if let Some(value) = try_read(path) {
441 return Some(value);
442 }
443
444 None
445}
446
447fn merge_json(base: &mut serde_json::Value, overlay: serde_json::Value) {
448 match (base, overlay) {
449 (serde_json::Value::Object(a), serde_json::Value::Object(b)) => {
450 for (k, v) in b {
451 merge_json(a.entry(k).or_insert(serde_json::Value::Null), v);
452 }
453 }
454 (a, b) => *a = b,
455 }
456}
457
458#[cfg(test)]
459mod tests {
460 use super::*;
461
462 #[test]
463 fn config_env_jwt_and_app_override_precedence() {
464 std::env::set_var("JWT_SECRET", "env-jwt-secret-that-is-long-enough-32");
465 let mut config = serde_json::json!({ "Jwt": { "Secret": "from-file" } });
466 apply_well_known_env_overrides(&mut config);
467 assert_eq!(
468 config["Jwt"]["Secret"],
469 "env-jwt-secret-that-is-long-enough-32"
470 );
471
472 std::env::set_var(
473 "APP__Jwt__Secret",
474 "from-app-override-secret-32chars-minimum",
475 );
476 apply_env_overrides(&mut config);
477 std::env::remove_var("JWT_SECRET");
478 std::env::remove_var("APP__Jwt__Secret");
479 assert_eq!(
480 config["Jwt"]["Secret"],
481 "from-app-override-secret-32chars-minimum"
482 );
483 }
484
485 #[test]
486 fn cors_section_binds_pascal_case_from_appsettings() {
487 let json = serde_json::json!({
490 "Cors": {
491 "Origins": ["https://www.lusida.net", "https://lusida.net"],
492 "Methods": ["GET", "POST"],
493 "Headers": ["Content-Type", "Authorization"],
494 "AllowCredentials": true,
495 "MaxAge": 3600
496 }
497 });
498 let opts: AppOptions = bind_root(&json);
499 assert_eq!(
500 opts.cors.origins,
501 vec![
502 "https://www.lusida.net".to_string(),
503 "https://lusida.net".to_string()
504 ]
505 );
506 assert_eq!(
507 opts.cors.methods,
508 vec!["GET".to_string(), "POST".to_string()]
509 );
510 assert!(opts.cors.allow_credentials);
511 assert_eq!(opts.cors.max_age, 3600);
512 assert!(!opts.cors.origins.iter().any(|o| o == "*"));
513 }
514
515 #[test]
516 fn production_overlay_merge_keeps_explicit_cors_origins() {
517 let mut base = serde_json::json!({
518 "App": { "Urls": ["http://localhost:5000"] },
519 "Cors": { "Origins": ["*"] },
520 "Jwt": { "Secret": "docbit-dev-secret-change-in-production" }
521 });
522 let overlay = serde_json::json!({
523 "App": { "Urls": ["http://0.0.0.0:8100"] },
524 "Cors": {
525 "Origins": [
526 "https://www.lusida.net",
527 "https://lusida.net"
528 ]
529 },
530 "Jwt": { "Secret": "" }
531 });
532 merge_json(&mut base, overlay);
533 let opts: AppOptions = bind_root(&base);
534 assert_eq!(opts.app.urls, vec!["http://0.0.0.0:8100".to_string()]);
535 assert_eq!(
536 opts.cors.origins,
537 vec![
538 "https://www.lusida.net".to_string(),
539 "https://lusida.net".to_string()
540 ]
541 );
542 assert!(opts.jwt.secret.is_empty());
543 }
544}