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)]
147pub struct CultureSection {
148 #[serde(default = "default_culture", rename = "Default")]
150 pub default: String,
151}
152
153impl Default for CultureSection {
154 fn default() -> Self {
155 Self {
156 default: default_culture(),
157 }
158 }
159}
160
161fn default_culture() -> String {
162 "en".to_string()
163}
164
165#[derive(Debug, Clone, Deserialize)]
167pub struct RateLimitSection {
168 #[serde(default, rename = "Enabled")]
170 pub enabled: bool,
171 #[serde(default = "default_rate_limit_rps", rename = "RequestsPerSecond")]
173 pub requests_per_second: f64,
174 #[serde(default = "default_rate_limit_burst", rename = "BurstSize")]
176 pub burst_size: u32,
177 #[serde(default = "default_rate_limit_max_ips", rename = "MaxTrackedIps")]
179 pub max_tracked_ips: usize,
180 #[serde(default, rename = "TrustProxy")]
183 pub trust_proxy: bool,
184 #[serde(default, rename = "TrustedProxies")]
193 pub trusted_proxies: Vec<String>,
194}
195
196impl Default for RateLimitSection {
197 fn default() -> Self {
198 Self {
199 enabled: false,
200 requests_per_second: default_rate_limit_rps(),
201 burst_size: default_rate_limit_burst(),
202 max_tracked_ips: default_rate_limit_max_ips(),
203 trust_proxy: false,
204 trusted_proxies: Vec::new(),
205 }
206 }
207}
208
209fn default_rate_limit_rps() -> f64 {
210 100.0
211}
212
213fn default_rate_limit_burst() -> u32 {
214 200
215}
216
217fn default_rate_limit_max_ips() -> usize {
218 10_000
219}
220
221#[derive(Debug, Clone, Deserialize, Default)]
223pub struct MetricsSection {
224 #[serde(default, rename = "Enabled")]
225 pub enabled: bool,
226}
227
228#[derive(Debug, Clone, Deserialize, Default)]
234pub struct TlsSection {
235 #[serde(default, rename = "CertPath")]
237 pub cert_path: String,
238 #[serde(default, rename = "KeyPath")]
240 pub key_path: String,
241}
242
243#[derive(Debug, Clone, Deserialize)]
251pub struct FormSection {
252 #[serde(default = "default_max_request_size", rename = "MaxRequestSize")]
254 pub max_request_size: usize,
255 #[serde(default = "default_max_file_size", rename = "MaxFileSize")]
257 pub max_file_size: u64,
258 #[serde(default = "default_max_field_size", rename = "MaxFieldSize")]
260 pub max_field_size: usize,
261 #[serde(default = "default_memory_threshold", rename = "MemoryThreshold")]
266 pub memory_threshold: usize,
267 #[serde(default, rename = "TempDir")]
270 pub temp_dir: Option<String>,
271}
272
273impl Default for FormSection {
274 fn default() -> Self {
275 Self {
276 max_request_size: default_max_request_size(),
277 max_file_size: default_max_file_size(),
278 max_field_size: default_max_field_size(),
279 memory_threshold: default_memory_threshold(),
280 temp_dir: None,
281 }
282 }
283}
284
285fn default_max_request_size() -> usize {
286 256 * 1024 * 1024
287}
288
289fn default_max_file_size() -> u64 {
290 128 * 1024 * 1024
291}
292
293fn default_max_field_size() -> usize {
294 1024 * 1024
295}
296
297fn default_memory_threshold() -> usize {
298 1024 * 1024
299}
300
301#[derive(Debug, Clone, Deserialize, Default)]
306pub struct AppOptions {
307 #[serde(default, rename = "App")]
309 pub app: AppSection,
310 #[serde(default, rename = "Jwt")]
312 pub jwt: JwtSection,
313 #[serde(default, rename = "Cors")]
315 pub cors: CorsSection,
316 #[serde(default, rename = "Tls")]
318 pub tls: TlsSection,
319 #[serde(default, rename = "RateLimit")]
321 pub rate_limit: RateLimitSection,
322 #[serde(default, rename = "Metrics")]
324 pub metrics: MetricsSection,
325 #[serde(default, rename = "Form")]
327 pub form: FormSection,
328 #[serde(default, rename = "Culture")]
330 pub culture: CultureSection,
331}
332
333pub fn load_appsettings(mode: AppMode) -> Option<serde_json::Value> {
346 let mut base = read_json_file("appsettings.json")?;
347
348 let overlay_name = mode.overlay_filename();
349 if let Some(overlay) = read_json_file(overlay_name) {
350 merge_json(&mut base, overlay);
351 }
352
353 apply_well_known_env_overrides(&mut base);
355
356 apply_env_overrides(&mut base);
358
359 Some(base)
360}
361
362fn apply_well_known_env_overrides(config: &mut serde_json::Value) {
366 if let Ok(secret) = std::env::var("JWT_SECRET") {
367 if !secret.is_empty() {
368 set_json_value(config, &["Jwt", "Secret"], &secret);
369 }
370 }
371}
372
373fn apply_env_overrides(config: &mut serde_json::Value) {
375 for (key, value) in std::env::vars() {
376 if let Some(path) = key.strip_prefix("APP__") {
377 let segments: Vec<&str> = path.split("__").collect();
379 if segments.is_empty() {
380 continue;
381 }
382 set_json_value(config, &segments, &value);
383 }
384 }
385}
386
387fn set_json_value(obj: &mut serde_json::Value, segments: &[&str], value: &str) {
389 if segments.is_empty() {
390 return;
391 }
392
393 let key = segments[0];
394
395 if let serde_json::Value::Object(map) = obj {
396 if segments.len() == 1 {
397 let parsed =
399 serde_json::from_str(value).unwrap_or(serde_json::Value::String(value.to_string()));
400 map.insert(key.to_string(), parsed);
401 } else if let Some(child) = map.get_mut(key) {
402 set_json_value(child, &segments[1..], value);
404 } else {
405 let mut child = serde_json::json!({});
407 set_json_value(&mut child, &segments[1..], value);
408 map.insert(key.to_string(), child);
409 }
410 }
411}
412
413pub fn bind_config<T: for<'de> Deserialize<'de> + Default>(
415 config: &serde_json::Value,
416 section: &str,
417) -> T {
418 if section.is_empty() || section == "." {
419 serde_json::from_value(config.clone()).unwrap_or_else(|e| {
420 tracing::warn!(
421 "[Config] Failed to bind root config to {}: {}, using defaults",
422 std::any::type_name::<T>(),
423 e
424 );
425 T::default()
426 })
427 } else {
428 config
429 .get(section)
430 .map(|v| {
431 serde_json::from_value(v.clone()).unwrap_or_else(|e| {
432 tracing::warn!(
433 "[Config] Failed to bind section '{}' to {}: {}, using defaults",
434 section,
435 std::any::type_name::<T>(),
436 e
437 );
438 T::default()
439 })
440 })
441 .unwrap_or_default()
442 }
443}
444
445pub fn bind_root<T: for<'de> Deserialize<'de> + Default>(config: &serde_json::Value) -> T {
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}
456
457fn read_json_file(path: impl AsRef<Path>) -> Option<serde_json::Value> {
462 let path = path.as_ref();
463
464 fn try_read(path: &Path) -> Option<serde_json::Value> {
466 let content = std::fs::read_to_string(path).ok()?;
467 serde_json::from_str(&content).ok()
468 }
469
470 let base = crate::paths::app_base();
473 let candidate = base.join(path);
474 if let Some(value) = try_read(&candidate) {
475 return Some(value);
476 }
477
478 if let Some(value) = try_read(path) {
480 return Some(value);
481 }
482
483 None
484}
485
486fn merge_json(base: &mut serde_json::Value, overlay: serde_json::Value) {
487 match (base, overlay) {
488 (serde_json::Value::Object(a), serde_json::Value::Object(b)) => {
489 for (k, v) in b {
490 merge_json(a.entry(k).or_insert(serde_json::Value::Null), v);
491 }
492 }
493 (a, b) => *a = b,
494 }
495}
496
497#[cfg(test)]
498mod tests {
499 use super::*;
500
501 #[test]
502 fn config_env_jwt_and_app_override_precedence() {
503 std::env::set_var("JWT_SECRET", "env-jwt-secret-that-is-long-enough-32");
504 let mut config = serde_json::json!({ "Jwt": { "Secret": "from-file" } });
505 apply_well_known_env_overrides(&mut config);
506 assert_eq!(
507 config["Jwt"]["Secret"],
508 "env-jwt-secret-that-is-long-enough-32"
509 );
510
511 std::env::set_var(
512 "APP__Jwt__Secret",
513 "from-app-override-secret-32chars-minimum",
514 );
515 apply_env_overrides(&mut config);
516 std::env::remove_var("JWT_SECRET");
517 std::env::remove_var("APP__Jwt__Secret");
518 assert_eq!(
519 config["Jwt"]["Secret"],
520 "from-app-override-secret-32chars-minimum"
521 );
522 }
523
524 #[test]
525 fn cors_section_binds_pascal_case_from_appsettings() {
526 let json = serde_json::json!({
529 "Cors": {
530 "Origins": ["https://www.lusida.net", "https://lusida.net"],
531 "Methods": ["GET", "POST"],
532 "Headers": ["Content-Type", "Authorization"],
533 "AllowCredentials": true,
534 "MaxAge": 3600
535 }
536 });
537 let opts: AppOptions = bind_root(&json);
538 assert_eq!(
539 opts.cors.origins,
540 vec![
541 "https://www.lusida.net".to_string(),
542 "https://lusida.net".to_string()
543 ]
544 );
545 assert_eq!(
546 opts.cors.methods,
547 vec!["GET".to_string(), "POST".to_string()]
548 );
549 assert!(opts.cors.allow_credentials);
550 assert_eq!(opts.cors.max_age, 3600);
551 assert!(!opts.cors.origins.iter().any(|o| o == "*"));
552 }
553
554 #[test]
555 fn culture_section_binds_default_and_falls_back_to_en() {
556 let json = serde_json::json!({ "Culture": { "Default": "zh-CN" } });
557 let opts: AppOptions = bind_root(&json);
558 assert_eq!(opts.culture.default, "zh-CN");
559
560 let opts: AppOptions = bind_root(&serde_json::json!({}));
561 assert_eq!(opts.culture.default, "en");
562 }
563
564 #[test]
565 fn production_overlay_merge_keeps_explicit_cors_origins() {
566 let mut base = serde_json::json!({
567 "App": { "Urls": ["http://localhost:5000"] },
568 "Cors": { "Origins": ["*"] },
569 "Jwt": { "Secret": "docbit-dev-secret-change-in-production" }
570 });
571 let overlay = serde_json::json!({
572 "App": { "Urls": ["http://0.0.0.0:8100"] },
573 "Cors": {
574 "Origins": [
575 "https://www.lusida.net",
576 "https://lusida.net"
577 ]
578 },
579 "Jwt": { "Secret": "" }
580 });
581 merge_json(&mut base, overlay);
582 let opts: AppOptions = bind_root(&base);
583 assert_eq!(opts.app.urls, vec!["http://0.0.0.0:8100".to_string()]);
584 assert_eq!(
585 opts.cors.origins,
586 vec![
587 "https://www.lusida.net".to_string(),
588 "https://lusida.net".to_string()
589 ]
590 );
591 assert!(opts.jwt.secret.is_empty());
592 }
593}