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)]
91pub struct JwtSection {
92 #[serde(default, rename = "Secret")]
94 pub secret: String,
95
96 #[serde(default, rename = "SecretFile")]
104 pub secret_file: Option<String>,
105
106 #[serde(
111 default = "default_jwt_expiration_minutes",
112 rename = "ExpirationMinutes"
113 )]
114 pub expiration_minutes: u64,
115}
116
117fn default_jwt_expiration_minutes() -> u64 {
118 60
119}
120
121impl Default for JwtSection {
122 fn default() -> Self {
123 Self {
124 secret: String::new(),
125 secret_file: None,
126 expiration_minutes: default_jwt_expiration_minutes(),
127 }
128 }
129}
130
131#[derive(Debug, Clone, Deserialize)]
136#[serde(rename_all = "PascalCase")]
137pub struct CorsSection {
138 #[serde(default = "default_origins")]
140 pub origins: Vec<String>,
141 #[serde(default = "default_cors_methods")]
143 pub methods: Vec<String>,
144 #[serde(default = "default_cors_headers")]
146 pub headers: Vec<String>,
147 #[serde(default)]
149 pub expose_headers: Vec<String>,
150 #[serde(default)]
152 pub allow_credentials: bool,
153 #[serde(default = "default_max_age")]
155 pub max_age: u32,
156}
157
158impl Default for CorsSection {
159 fn default() -> Self {
160 Self {
161 origins: default_origins(),
162 methods: default_cors_methods(),
163 headers: default_cors_headers(),
164 expose_headers: Vec::new(),
165 allow_credentials: false,
166 max_age: default_max_age(),
167 }
168 }
169}
170
171fn default_origins() -> Vec<String> {
172 vec!["*".to_string()]
173}
174
175fn default_cors_methods() -> Vec<String> {
176 vec![
177 "GET".to_string(),
178 "POST".to_string(),
179 "PUT".to_string(),
180 "DELETE".to_string(),
181 "PATCH".to_string(),
182 "OPTIONS".to_string(),
183 ]
184}
185
186fn default_cors_headers() -> Vec<String> {
187 vec!["Content-Type".to_string(), "Authorization".to_string()]
188}
189
190fn default_max_age() -> u32 {
191 86400
192}
193
194#[derive(Debug, Clone, Deserialize)]
201pub struct CultureSection {
202 #[serde(default = "default_culture", rename = "Default")]
204 pub default: String,
205}
206
207impl Default for CultureSection {
208 fn default() -> Self {
209 Self {
210 default: default_culture(),
211 }
212 }
213}
214
215fn default_culture() -> String {
216 "en".to_string()
217}
218
219#[derive(Debug, Clone, Deserialize)]
221pub struct RateLimitSection {
222 #[serde(default, rename = "Enabled")]
224 pub enabled: bool,
225 #[serde(default = "default_rate_limit_rps", rename = "RequestsPerSecond")]
227 pub requests_per_second: f64,
228 #[serde(default = "default_rate_limit_burst", rename = "BurstSize")]
230 pub burst_size: u32,
231 #[serde(default = "default_rate_limit_max_ips", rename = "MaxTrackedIps")]
233 pub max_tracked_ips: usize,
234 #[serde(default, rename = "TrustProxy")]
237 pub trust_proxy: bool,
238 #[serde(default, rename = "TrustedProxies")]
247 pub trusted_proxies: Vec<String>,
248}
249
250impl Default for RateLimitSection {
251 fn default() -> Self {
252 Self {
253 enabled: false,
254 requests_per_second: default_rate_limit_rps(),
255 burst_size: default_rate_limit_burst(),
256 max_tracked_ips: default_rate_limit_max_ips(),
257 trust_proxy: false,
258 trusted_proxies: Vec::new(),
259 }
260 }
261}
262
263fn default_rate_limit_rps() -> f64 {
264 100.0
265}
266
267fn default_rate_limit_burst() -> u32 {
268 200
269}
270
271fn default_rate_limit_max_ips() -> usize {
272 10_000
273}
274
275#[derive(Debug, Clone, Deserialize, Default)]
277pub struct MetricsSection {
278 #[serde(default, rename = "Enabled")]
279 pub enabled: bool,
280}
281
282#[derive(Debug, Clone, Deserialize, Default)]
288pub struct TlsSection {
289 #[serde(default, rename = "CertPath")]
291 pub cert_path: String,
292 #[serde(default, rename = "KeyPath")]
294 pub key_path: String,
295}
296
297#[derive(Debug, Clone, Deserialize)]
305pub struct FormSection {
306 #[serde(default = "default_max_request_size", rename = "MaxRequestSize")]
308 pub max_request_size: usize,
309 #[serde(default = "default_max_file_size", rename = "MaxFileSize")]
311 pub max_file_size: u64,
312 #[serde(default = "default_max_field_size", rename = "MaxFieldSize")]
314 pub max_field_size: usize,
315 #[serde(default = "default_memory_threshold", rename = "MemoryThreshold")]
320 pub memory_threshold: usize,
321 #[serde(default, rename = "TempDir")]
324 pub temp_dir: Option<String>,
325}
326
327impl Default for FormSection {
328 fn default() -> Self {
329 Self {
330 max_request_size: default_max_request_size(),
331 max_file_size: default_max_file_size(),
332 max_field_size: default_max_field_size(),
333 memory_threshold: default_memory_threshold(),
334 temp_dir: None,
335 }
336 }
337}
338
339fn default_max_request_size() -> usize {
340 256 * 1024 * 1024
341}
342
343fn default_max_file_size() -> u64 {
344 128 * 1024 * 1024
345}
346
347fn default_max_field_size() -> usize {
348 1024 * 1024
349}
350
351fn default_memory_threshold() -> usize {
352 1024 * 1024
353}
354
355#[derive(Debug, Clone, Deserialize, Default)]
360pub struct AppOptions {
361 #[serde(default, rename = "App")]
363 pub app: AppSection,
364 #[serde(default, rename = "Jwt")]
366 pub jwt: JwtSection,
367 #[serde(default, rename = "Cors")]
369 pub cors: CorsSection,
370 #[serde(default, rename = "Tls")]
372 pub tls: TlsSection,
373 #[serde(default, rename = "RateLimit")]
375 pub rate_limit: RateLimitSection,
376 #[serde(default, rename = "Metrics")]
378 pub metrics: MetricsSection,
379 #[serde(default, rename = "Form")]
381 pub form: FormSection,
382 #[serde(default, rename = "Culture")]
384 pub culture: CultureSection,
385}
386
387pub fn load_appsettings(mode: AppMode) -> Option<serde_json::Value> {
400 let mut base = read_json_file("appsettings.json")?;
401
402 let overlay_name = mode.overlay_filename();
403 if let Some(overlay) = read_json_file(overlay_name) {
404 merge_json(&mut base, overlay);
405 }
406
407 apply_well_known_env_overrides(&mut base);
409
410 apply_env_overrides(&mut base);
412
413 Some(base)
414}
415
416fn apply_well_known_env_overrides(config: &mut serde_json::Value) {
420 if let Ok(secret) = std::env::var("JWT_SECRET") {
421 if !secret.is_empty() {
422 set_json_value(config, &["Jwt", "Secret"], &secret);
423 }
424 }
425}
426
427fn apply_env_overrides(config: &mut serde_json::Value) {
429 for (key, value) in std::env::vars() {
430 if let Some(path) = key.strip_prefix("APP__") {
431 let segments: Vec<&str> = path.split("__").collect();
433 if segments.is_empty() {
434 continue;
435 }
436 set_json_value(config, &segments, &value);
437 }
438 }
439}
440
441fn set_json_value(obj: &mut serde_json::Value, segments: &[&str], value: &str) {
443 if segments.is_empty() {
444 return;
445 }
446
447 let key = segments[0];
448
449 if let serde_json::Value::Object(map) = obj {
450 if segments.len() == 1 {
451 let parsed =
453 serde_json::from_str(value).unwrap_or(serde_json::Value::String(value.to_string()));
454 map.insert(key.to_string(), parsed);
455 } else if let Some(child) = map.get_mut(key) {
456 set_json_value(child, &segments[1..], value);
458 } else {
459 let mut child = serde_json::json!({});
461 set_json_value(&mut child, &segments[1..], value);
462 map.insert(key.to_string(), child);
463 }
464 }
465}
466
467pub fn bind_config<T: for<'de> Deserialize<'de> + Default>(
469 config: &serde_json::Value,
470 section: &str,
471) -> T {
472 if section.is_empty() || section == "." {
473 serde_json::from_value(config.clone()).unwrap_or_else(|e| {
474 tracing::warn!(
475 "[Config] Failed to bind root config to {}: {}, using defaults",
476 std::any::type_name::<T>(),
477 e
478 );
479 T::default()
480 })
481 } else {
482 config
483 .get(section)
484 .map(|v| {
485 serde_json::from_value(v.clone()).unwrap_or_else(|e| {
486 tracing::warn!(
487 "[Config] Failed to bind section '{}' to {}: {}, using defaults",
488 section,
489 std::any::type_name::<T>(),
490 e
491 );
492 T::default()
493 })
494 })
495 .unwrap_or_default()
496 }
497}
498
499pub fn bind_root<T: for<'de> Deserialize<'de> + Default>(config: &serde_json::Value) -> T {
501 serde_json::from_value(config.clone()).unwrap_or_else(|e| {
502 tracing::warn!(
503 "[Config] Failed to bind root config to {}: {}, using defaults",
504 std::any::type_name::<T>(),
505 e
506 );
507 T::default()
508 })
509}
510
511fn read_json_file(path: impl AsRef<Path>) -> Option<serde_json::Value> {
516 let path = path.as_ref();
517
518 fn try_read(path: &Path) -> Option<serde_json::Value> {
520 let content = std::fs::read_to_string(path).ok()?;
521 serde_json::from_str(&content).ok()
522 }
523
524 let base = crate::paths::app_base();
527 let candidate = base.join(path);
528 if let Some(value) = try_read(&candidate) {
529 return Some(value);
530 }
531
532 if let Some(value) = try_read(path) {
534 return Some(value);
535 }
536
537 None
538}
539
540fn merge_json(base: &mut serde_json::Value, overlay: serde_json::Value) {
541 match (base, overlay) {
542 (serde_json::Value::Object(a), serde_json::Value::Object(b)) => {
543 for (k, v) in b {
544 merge_json(a.entry(k).or_insert(serde_json::Value::Null), v);
545 }
546 }
547 (a, b) => *a = b,
548 }
549}
550
551#[cfg(test)]
552mod tests {
553 use super::*;
554
555 #[test]
556 fn config_env_jwt_and_app_override_precedence() {
557 std::env::set_var("JWT_SECRET", "env-jwt-secret-that-is-long-enough-32");
558 let mut config = serde_json::json!({ "Jwt": { "Secret": "from-file" } });
559 apply_well_known_env_overrides(&mut config);
560 assert_eq!(
561 config["Jwt"]["Secret"],
562 "env-jwt-secret-that-is-long-enough-32"
563 );
564
565 std::env::set_var(
566 "APP__Jwt__Secret",
567 "from-app-override-secret-32chars-minimum",
568 );
569 apply_env_overrides(&mut config);
570 std::env::remove_var("JWT_SECRET");
571 std::env::remove_var("APP__Jwt__Secret");
572 assert_eq!(
573 config["Jwt"]["Secret"],
574 "from-app-override-secret-32chars-minimum"
575 );
576 }
577
578 #[test]
579 fn cors_section_binds_pascal_case_from_appsettings() {
580 let json = serde_json::json!({
583 "Cors": {
584 "Origins": ["https://www.lusida.net", "https://lusida.net"],
585 "Methods": ["GET", "POST"],
586 "Headers": ["Content-Type", "Authorization"],
587 "AllowCredentials": true,
588 "MaxAge": 3600
589 }
590 });
591 let opts: AppOptions = bind_root(&json);
592 assert_eq!(
593 opts.cors.origins,
594 vec![
595 "https://www.lusida.net".to_string(),
596 "https://lusida.net".to_string()
597 ]
598 );
599 assert_eq!(
600 opts.cors.methods,
601 vec!["GET".to_string(), "POST".to_string()]
602 );
603 assert!(opts.cors.allow_credentials);
604 assert_eq!(opts.cors.max_age, 3600);
605 assert!(!opts.cors.origins.iter().any(|o| o == "*"));
606 }
607
608 #[test]
609 fn culture_section_binds_default_and_falls_back_to_en() {
610 let json = serde_json::json!({ "Culture": { "Default": "zh-CN" } });
611 let opts: AppOptions = bind_root(&json);
612 assert_eq!(opts.culture.default, "zh-CN");
613
614 let opts: AppOptions = bind_root(&serde_json::json!({}));
615 assert_eq!(opts.culture.default, "en");
616 }
617
618 #[test]
619 fn jwt_section_binds_expiration_minutes_with_60_minute_default() {
620 let opts: AppOptions = bind_root(&serde_json::json!({}));
621 assert_eq!(opts.jwt.expiration_minutes, 60);
622 assert!(opts.jwt.secret.is_empty());
623
624 let opts: AppOptions = bind_root(&serde_json::json!({
625 "Jwt": { "Secret": "s", "ExpirationMinutes": 15 }
626 }));
627 assert_eq!(opts.jwt.expiration_minutes, 15);
628 }
629
630 #[test]
631 fn jwt_section_binds_secret_file_and_defaults_to_none() {
632 let opts: AppOptions = bind_root(&serde_json::json!({}));
635 assert_eq!(opts.jwt.secret_file, None);
636 assert_eq!(JwtSection::default().secret_file, None);
637
638 let opts: AppOptions = bind_root(&serde_json::json!({
639 "Jwt": { "SecretFile": "data/jwt.key" }
640 }));
641 assert_eq!(opts.jwt.secret_file.as_deref(), Some("data/jwt.key"));
642 assert!(
643 opts.jwt.secret.is_empty(),
644 "binding records the path; the host reads the file"
645 );
646
647 let opts: AppOptions = bind_root(&serde_json::json!({
649 "Jwt": { "Secret": "explicit-secret", "SecretFile": "data/jwt.key" }
650 }));
651 assert_eq!(opts.jwt.secret, "explicit-secret");
652 assert_eq!(opts.jwt.secret_file.as_deref(), Some("data/jwt.key"));
653 }
654
655 #[test]
656 fn production_overlay_merge_keeps_explicit_cors_origins() {
657 let mut base = serde_json::json!({
658 "App": { "Urls": ["http://localhost:5000"] },
659 "Cors": { "Origins": ["*"] },
660 "Jwt": { "Secret": "docbit-dev-secret-change-in-production" }
661 });
662 let overlay = serde_json::json!({
663 "App": { "Urls": ["http://0.0.0.0:8100"] },
664 "Cors": {
665 "Origins": [
666 "https://www.lusida.net",
667 "https://lusida.net"
668 ]
669 },
670 "Jwt": { "Secret": "" }
671 });
672 merge_json(&mut base, overlay);
673 let opts: AppOptions = bind_root(&base);
674 assert_eq!(opts.app.urls, vec!["http://0.0.0.0:8100".to_string()]);
675 assert_eq!(
676 opts.cors.origins,
677 vec![
678 "https://www.lusida.net".to_string(),
679 "https://lusida.net".to_string()
680 ]
681 );
682 assert!(opts.jwt.secret.is_empty());
683 }
684}