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)]
76pub struct JwtSection {
77 #[serde(default, rename = "Secret")]
79 pub secret: String,
80
81 #[serde(
86 default = "default_jwt_expiration_minutes",
87 rename = "ExpirationMinutes"
88 )]
89 pub expiration_minutes: u64,
90}
91
92fn default_jwt_expiration_minutes() -> u64 {
93 60
94}
95
96impl Default for JwtSection {
97 fn default() -> Self {
98 Self {
99 secret: String::new(),
100 expiration_minutes: default_jwt_expiration_minutes(),
101 }
102 }
103}
104
105#[derive(Debug, Clone, Deserialize)]
110#[serde(rename_all = "PascalCase")]
111pub struct CorsSection {
112 #[serde(default = "default_origins")]
114 pub origins: Vec<String>,
115 #[serde(default = "default_cors_methods")]
117 pub methods: Vec<String>,
118 #[serde(default = "default_cors_headers")]
120 pub headers: Vec<String>,
121 #[serde(default)]
123 pub expose_headers: Vec<String>,
124 #[serde(default)]
126 pub allow_credentials: bool,
127 #[serde(default = "default_max_age")]
129 pub max_age: u32,
130}
131
132impl Default for CorsSection {
133 fn default() -> Self {
134 Self {
135 origins: default_origins(),
136 methods: default_cors_methods(),
137 headers: default_cors_headers(),
138 expose_headers: Vec::new(),
139 allow_credentials: false,
140 max_age: default_max_age(),
141 }
142 }
143}
144
145fn default_origins() -> Vec<String> {
146 vec!["*".to_string()]
147}
148
149fn default_cors_methods() -> Vec<String> {
150 vec![
151 "GET".to_string(),
152 "POST".to_string(),
153 "PUT".to_string(),
154 "DELETE".to_string(),
155 "PATCH".to_string(),
156 "OPTIONS".to_string(),
157 ]
158}
159
160fn default_cors_headers() -> Vec<String> {
161 vec!["Content-Type".to_string(), "Authorization".to_string()]
162}
163
164fn default_max_age() -> u32 {
165 86400
166}
167
168#[derive(Debug, Clone, Deserialize)]
175pub struct CultureSection {
176 #[serde(default = "default_culture", rename = "Default")]
178 pub default: String,
179}
180
181impl Default for CultureSection {
182 fn default() -> Self {
183 Self {
184 default: default_culture(),
185 }
186 }
187}
188
189fn default_culture() -> String {
190 "en".to_string()
191}
192
193#[derive(Debug, Clone, Deserialize)]
195pub struct RateLimitSection {
196 #[serde(default, rename = "Enabled")]
198 pub enabled: bool,
199 #[serde(default = "default_rate_limit_rps", rename = "RequestsPerSecond")]
201 pub requests_per_second: f64,
202 #[serde(default = "default_rate_limit_burst", rename = "BurstSize")]
204 pub burst_size: u32,
205 #[serde(default = "default_rate_limit_max_ips", rename = "MaxTrackedIps")]
207 pub max_tracked_ips: usize,
208 #[serde(default, rename = "TrustProxy")]
211 pub trust_proxy: bool,
212 #[serde(default, rename = "TrustedProxies")]
221 pub trusted_proxies: Vec<String>,
222}
223
224impl Default for RateLimitSection {
225 fn default() -> Self {
226 Self {
227 enabled: false,
228 requests_per_second: default_rate_limit_rps(),
229 burst_size: default_rate_limit_burst(),
230 max_tracked_ips: default_rate_limit_max_ips(),
231 trust_proxy: false,
232 trusted_proxies: Vec::new(),
233 }
234 }
235}
236
237fn default_rate_limit_rps() -> f64 {
238 100.0
239}
240
241fn default_rate_limit_burst() -> u32 {
242 200
243}
244
245fn default_rate_limit_max_ips() -> usize {
246 10_000
247}
248
249#[derive(Debug, Clone, Deserialize, Default)]
251pub struct MetricsSection {
252 #[serde(default, rename = "Enabled")]
253 pub enabled: bool,
254}
255
256#[derive(Debug, Clone, Deserialize, Default)]
262pub struct TlsSection {
263 #[serde(default, rename = "CertPath")]
265 pub cert_path: String,
266 #[serde(default, rename = "KeyPath")]
268 pub key_path: String,
269}
270
271#[derive(Debug, Clone, Deserialize)]
279pub struct FormSection {
280 #[serde(default = "default_max_request_size", rename = "MaxRequestSize")]
282 pub max_request_size: usize,
283 #[serde(default = "default_max_file_size", rename = "MaxFileSize")]
285 pub max_file_size: u64,
286 #[serde(default = "default_max_field_size", rename = "MaxFieldSize")]
288 pub max_field_size: usize,
289 #[serde(default = "default_memory_threshold", rename = "MemoryThreshold")]
294 pub memory_threshold: usize,
295 #[serde(default, rename = "TempDir")]
298 pub temp_dir: Option<String>,
299}
300
301impl Default for FormSection {
302 fn default() -> Self {
303 Self {
304 max_request_size: default_max_request_size(),
305 max_file_size: default_max_file_size(),
306 max_field_size: default_max_field_size(),
307 memory_threshold: default_memory_threshold(),
308 temp_dir: None,
309 }
310 }
311}
312
313fn default_max_request_size() -> usize {
314 256 * 1024 * 1024
315}
316
317fn default_max_file_size() -> u64 {
318 128 * 1024 * 1024
319}
320
321fn default_max_field_size() -> usize {
322 1024 * 1024
323}
324
325fn default_memory_threshold() -> usize {
326 1024 * 1024
327}
328
329#[derive(Debug, Clone, Deserialize, Default)]
334pub struct AppOptions {
335 #[serde(default, rename = "App")]
337 pub app: AppSection,
338 #[serde(default, rename = "Jwt")]
340 pub jwt: JwtSection,
341 #[serde(default, rename = "Cors")]
343 pub cors: CorsSection,
344 #[serde(default, rename = "Tls")]
346 pub tls: TlsSection,
347 #[serde(default, rename = "RateLimit")]
349 pub rate_limit: RateLimitSection,
350 #[serde(default, rename = "Metrics")]
352 pub metrics: MetricsSection,
353 #[serde(default, rename = "Form")]
355 pub form: FormSection,
356 #[serde(default, rename = "Culture")]
358 pub culture: CultureSection,
359}
360
361pub fn load_appsettings(mode: AppMode) -> Option<serde_json::Value> {
374 let mut base = read_json_file("appsettings.json")?;
375
376 let overlay_name = mode.overlay_filename();
377 if let Some(overlay) = read_json_file(overlay_name) {
378 merge_json(&mut base, overlay);
379 }
380
381 apply_well_known_env_overrides(&mut base);
383
384 apply_env_overrides(&mut base);
386
387 Some(base)
388}
389
390fn apply_well_known_env_overrides(config: &mut serde_json::Value) {
394 if let Ok(secret) = std::env::var("JWT_SECRET") {
395 if !secret.is_empty() {
396 set_json_value(config, &["Jwt", "Secret"], &secret);
397 }
398 }
399}
400
401fn apply_env_overrides(config: &mut serde_json::Value) {
403 for (key, value) in std::env::vars() {
404 if let Some(path) = key.strip_prefix("APP__") {
405 let segments: Vec<&str> = path.split("__").collect();
407 if segments.is_empty() {
408 continue;
409 }
410 set_json_value(config, &segments, &value);
411 }
412 }
413}
414
415fn set_json_value(obj: &mut serde_json::Value, segments: &[&str], value: &str) {
417 if segments.is_empty() {
418 return;
419 }
420
421 let key = segments[0];
422
423 if let serde_json::Value::Object(map) = obj {
424 if segments.len() == 1 {
425 let parsed =
427 serde_json::from_str(value).unwrap_or(serde_json::Value::String(value.to_string()));
428 map.insert(key.to_string(), parsed);
429 } else if let Some(child) = map.get_mut(key) {
430 set_json_value(child, &segments[1..], value);
432 } else {
433 let mut child = serde_json::json!({});
435 set_json_value(&mut child, &segments[1..], value);
436 map.insert(key.to_string(), child);
437 }
438 }
439}
440
441pub fn bind_config<T: for<'de> Deserialize<'de> + Default>(
443 config: &serde_json::Value,
444 section: &str,
445) -> T {
446 if section.is_empty() || section == "." {
447 serde_json::from_value(config.clone()).unwrap_or_else(|e| {
448 tracing::warn!(
449 "[Config] Failed to bind root config to {}: {}, using defaults",
450 std::any::type_name::<T>(),
451 e
452 );
453 T::default()
454 })
455 } else {
456 config
457 .get(section)
458 .map(|v| {
459 serde_json::from_value(v.clone()).unwrap_or_else(|e| {
460 tracing::warn!(
461 "[Config] Failed to bind section '{}' to {}: {}, using defaults",
462 section,
463 std::any::type_name::<T>(),
464 e
465 );
466 T::default()
467 })
468 })
469 .unwrap_or_default()
470 }
471}
472
473pub fn bind_root<T: for<'de> Deserialize<'de> + Default>(config: &serde_json::Value) -> T {
475 serde_json::from_value(config.clone()).unwrap_or_else(|e| {
476 tracing::warn!(
477 "[Config] Failed to bind root config to {}: {}, using defaults",
478 std::any::type_name::<T>(),
479 e
480 );
481 T::default()
482 })
483}
484
485fn read_json_file(path: impl AsRef<Path>) -> Option<serde_json::Value> {
490 let path = path.as_ref();
491
492 fn try_read(path: &Path) -> Option<serde_json::Value> {
494 let content = std::fs::read_to_string(path).ok()?;
495 serde_json::from_str(&content).ok()
496 }
497
498 let base = crate::paths::app_base();
501 let candidate = base.join(path);
502 if let Some(value) = try_read(&candidate) {
503 return Some(value);
504 }
505
506 if let Some(value) = try_read(path) {
508 return Some(value);
509 }
510
511 None
512}
513
514fn merge_json(base: &mut serde_json::Value, overlay: serde_json::Value) {
515 match (base, overlay) {
516 (serde_json::Value::Object(a), serde_json::Value::Object(b)) => {
517 for (k, v) in b {
518 merge_json(a.entry(k).or_insert(serde_json::Value::Null), v);
519 }
520 }
521 (a, b) => *a = b,
522 }
523}
524
525#[cfg(test)]
526mod tests {
527 use super::*;
528
529 #[test]
530 fn config_env_jwt_and_app_override_precedence() {
531 std::env::set_var("JWT_SECRET", "env-jwt-secret-that-is-long-enough-32");
532 let mut config = serde_json::json!({ "Jwt": { "Secret": "from-file" } });
533 apply_well_known_env_overrides(&mut config);
534 assert_eq!(
535 config["Jwt"]["Secret"],
536 "env-jwt-secret-that-is-long-enough-32"
537 );
538
539 std::env::set_var(
540 "APP__Jwt__Secret",
541 "from-app-override-secret-32chars-minimum",
542 );
543 apply_env_overrides(&mut config);
544 std::env::remove_var("JWT_SECRET");
545 std::env::remove_var("APP__Jwt__Secret");
546 assert_eq!(
547 config["Jwt"]["Secret"],
548 "from-app-override-secret-32chars-minimum"
549 );
550 }
551
552 #[test]
553 fn cors_section_binds_pascal_case_from_appsettings() {
554 let json = serde_json::json!({
557 "Cors": {
558 "Origins": ["https://www.lusida.net", "https://lusida.net"],
559 "Methods": ["GET", "POST"],
560 "Headers": ["Content-Type", "Authorization"],
561 "AllowCredentials": true,
562 "MaxAge": 3600
563 }
564 });
565 let opts: AppOptions = bind_root(&json);
566 assert_eq!(
567 opts.cors.origins,
568 vec![
569 "https://www.lusida.net".to_string(),
570 "https://lusida.net".to_string()
571 ]
572 );
573 assert_eq!(
574 opts.cors.methods,
575 vec!["GET".to_string(), "POST".to_string()]
576 );
577 assert!(opts.cors.allow_credentials);
578 assert_eq!(opts.cors.max_age, 3600);
579 assert!(!opts.cors.origins.iter().any(|o| o == "*"));
580 }
581
582 #[test]
583 fn culture_section_binds_default_and_falls_back_to_en() {
584 let json = serde_json::json!({ "Culture": { "Default": "zh-CN" } });
585 let opts: AppOptions = bind_root(&json);
586 assert_eq!(opts.culture.default, "zh-CN");
587
588 let opts: AppOptions = bind_root(&serde_json::json!({}));
589 assert_eq!(opts.culture.default, "en");
590 }
591
592 #[test]
593 fn jwt_section_binds_expiration_minutes_with_60_minute_default() {
594 let opts: AppOptions = bind_root(&serde_json::json!({}));
595 assert_eq!(opts.jwt.expiration_minutes, 60);
596 assert!(opts.jwt.secret.is_empty());
597
598 let opts: AppOptions = bind_root(&serde_json::json!({
599 "Jwt": { "Secret": "s", "ExpirationMinutes": 15 }
600 }));
601 assert_eq!(opts.jwt.expiration_minutes, 15);
602 }
603
604 #[test]
605 fn production_overlay_merge_keeps_explicit_cors_origins() {
606 let mut base = serde_json::json!({
607 "App": { "Urls": ["http://localhost:5000"] },
608 "Cors": { "Origins": ["*"] },
609 "Jwt": { "Secret": "docbit-dev-secret-change-in-production" }
610 });
611 let overlay = serde_json::json!({
612 "App": { "Urls": ["http://0.0.0.0:8100"] },
613 "Cors": {
614 "Origins": [
615 "https://www.lusida.net",
616 "https://lusida.net"
617 ]
618 },
619 "Jwt": { "Secret": "" }
620 });
621 merge_json(&mut base, overlay);
622 let opts: AppOptions = bind_root(&base);
623 assert_eq!(opts.app.urls, vec!["http://0.0.0.0:8100".to_string()]);
624 assert_eq!(
625 opts.cors.origins,
626 vec![
627 "https://www.lusida.net".to_string(),
628 "https://lusida.net".to_string()
629 ]
630 );
631 assert!(opts.jwt.secret.is_empty());
632 }
633}