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}
185
186impl Default for RateLimitSection {
187 fn default() -> Self {
188 Self {
189 enabled: false,
190 requests_per_second: default_rate_limit_rps(),
191 burst_size: default_rate_limit_burst(),
192 max_tracked_ips: default_rate_limit_max_ips(),
193 trust_proxy: false,
194 }
195 }
196}
197
198fn default_rate_limit_rps() -> f64 {
199 100.0
200}
201
202fn default_rate_limit_burst() -> u32 {
203 200
204}
205
206fn default_rate_limit_max_ips() -> usize {
207 10_000
208}
209
210#[derive(Debug, Clone, Deserialize, Default)]
212pub struct MetricsSection {
213 #[serde(default, rename = "Enabled")]
214 pub enabled: bool,
215}
216
217#[derive(Debug, Clone, Deserialize, Default)]
223pub struct TlsSection {
224 #[serde(default, rename = "CertPath")]
226 pub cert_path: String,
227 #[serde(default, rename = "KeyPath")]
229 pub key_path: String,
230}
231
232#[derive(Debug, Clone, Deserialize)]
240pub struct FormSection {
241 #[serde(default = "default_max_request_size", rename = "MaxRequestSize")]
243 pub max_request_size: usize,
244 #[serde(default = "default_max_file_size", rename = "MaxFileSize")]
246 pub max_file_size: u64,
247 #[serde(default = "default_max_field_size", rename = "MaxFieldSize")]
249 pub max_field_size: usize,
250 #[serde(default = "default_memory_threshold", rename = "MemoryThreshold")]
255 pub memory_threshold: usize,
256 #[serde(default, rename = "TempDir")]
259 pub temp_dir: Option<String>,
260}
261
262impl Default for FormSection {
263 fn default() -> Self {
264 Self {
265 max_request_size: default_max_request_size(),
266 max_file_size: default_max_file_size(),
267 max_field_size: default_max_field_size(),
268 memory_threshold: default_memory_threshold(),
269 temp_dir: None,
270 }
271 }
272}
273
274fn default_max_request_size() -> usize {
275 256 * 1024 * 1024
276}
277
278fn default_max_file_size() -> u64 {
279 128 * 1024 * 1024
280}
281
282fn default_max_field_size() -> usize {
283 1024 * 1024
284}
285
286fn default_memory_threshold() -> usize {
287 1024 * 1024
288}
289
290#[derive(Debug, Clone, Deserialize, Default)]
295pub struct AppOptions {
296 #[serde(default, rename = "App")]
298 pub app: AppSection,
299 #[serde(default, rename = "Jwt")]
301 pub jwt: JwtSection,
302 #[serde(default, rename = "Cors")]
304 pub cors: CorsSection,
305 #[serde(default, rename = "Tls")]
307 pub tls: TlsSection,
308 #[serde(default, rename = "RateLimit")]
310 pub rate_limit: RateLimitSection,
311 #[serde(default, rename = "Metrics")]
313 pub metrics: MetricsSection,
314 #[serde(default, rename = "Form")]
316 pub form: FormSection,
317 #[serde(default, rename = "Culture")]
319 pub culture: CultureSection,
320}
321
322pub fn load_appsettings(mode: AppMode) -> Option<serde_json::Value> {
335 let mut base = read_json_file("appsettings.json")?;
336
337 let overlay_name = mode.overlay_filename();
338 if let Some(overlay) = read_json_file(overlay_name) {
339 merge_json(&mut base, overlay);
340 }
341
342 apply_well_known_env_overrides(&mut base);
344
345 apply_env_overrides(&mut base);
347
348 Some(base)
349}
350
351fn apply_well_known_env_overrides(config: &mut serde_json::Value) {
355 if let Ok(secret) = std::env::var("JWT_SECRET") {
356 if !secret.is_empty() {
357 set_json_value(config, &["Jwt", "Secret"], &secret);
358 }
359 }
360}
361
362fn apply_env_overrides(config: &mut serde_json::Value) {
364 for (key, value) in std::env::vars() {
365 if let Some(path) = key.strip_prefix("APP__") {
366 let segments: Vec<&str> = path.split("__").collect();
368 if segments.is_empty() {
369 continue;
370 }
371 set_json_value(config, &segments, &value);
372 }
373 }
374}
375
376fn set_json_value(obj: &mut serde_json::Value, segments: &[&str], value: &str) {
378 if segments.is_empty() {
379 return;
380 }
381
382 let key = segments[0];
383
384 if let serde_json::Value::Object(map) = obj {
385 if segments.len() == 1 {
386 let parsed =
388 serde_json::from_str(value).unwrap_or(serde_json::Value::String(value.to_string()));
389 map.insert(key.to_string(), parsed);
390 } else if let Some(child) = map.get_mut(key) {
391 set_json_value(child, &segments[1..], value);
393 } else {
394 let mut child = serde_json::json!({});
396 set_json_value(&mut child, &segments[1..], value);
397 map.insert(key.to_string(), child);
398 }
399 }
400}
401
402pub fn bind_config<T: for<'de> Deserialize<'de> + Default>(
404 config: &serde_json::Value,
405 section: &str,
406) -> T {
407 if section.is_empty() || section == "." {
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 } else {
417 config
418 .get(section)
419 .map(|v| {
420 serde_json::from_value(v.clone()).unwrap_or_else(|e| {
421 tracing::warn!(
422 "[Config] Failed to bind section '{}' to {}: {}, using defaults",
423 section,
424 std::any::type_name::<T>(),
425 e
426 );
427 T::default()
428 })
429 })
430 .unwrap_or_default()
431 }
432}
433
434pub fn bind_root<T: for<'de> Deserialize<'de> + Default>(config: &serde_json::Value) -> T {
436 serde_json::from_value(config.clone()).unwrap_or_else(|e| {
437 tracing::warn!(
438 "[Config] Failed to bind root config to {}: {}, using defaults",
439 std::any::type_name::<T>(),
440 e
441 );
442 T::default()
443 })
444}
445
446fn read_json_file(path: impl AsRef<Path>) -> Option<serde_json::Value> {
451 let path = path.as_ref();
452
453 fn try_read(path: &Path) -> Option<serde_json::Value> {
455 let content = std::fs::read_to_string(path).ok()?;
456 serde_json::from_str(&content).ok()
457 }
458
459 let base = crate::paths::app_base();
462 let candidate = base.join(path);
463 if let Some(value) = try_read(&candidate) {
464 return Some(value);
465 }
466
467 if let Some(value) = try_read(path) {
469 return Some(value);
470 }
471
472 None
473}
474
475fn merge_json(base: &mut serde_json::Value, overlay: serde_json::Value) {
476 match (base, overlay) {
477 (serde_json::Value::Object(a), serde_json::Value::Object(b)) => {
478 for (k, v) in b {
479 merge_json(a.entry(k).or_insert(serde_json::Value::Null), v);
480 }
481 }
482 (a, b) => *a = b,
483 }
484}
485
486#[cfg(test)]
487mod tests {
488 use super::*;
489
490 #[test]
491 fn config_env_jwt_and_app_override_precedence() {
492 std::env::set_var("JWT_SECRET", "env-jwt-secret-that-is-long-enough-32");
493 let mut config = serde_json::json!({ "Jwt": { "Secret": "from-file" } });
494 apply_well_known_env_overrides(&mut config);
495 assert_eq!(
496 config["Jwt"]["Secret"],
497 "env-jwt-secret-that-is-long-enough-32"
498 );
499
500 std::env::set_var(
501 "APP__Jwt__Secret",
502 "from-app-override-secret-32chars-minimum",
503 );
504 apply_env_overrides(&mut config);
505 std::env::remove_var("JWT_SECRET");
506 std::env::remove_var("APP__Jwt__Secret");
507 assert_eq!(
508 config["Jwt"]["Secret"],
509 "from-app-override-secret-32chars-minimum"
510 );
511 }
512
513 #[test]
514 fn cors_section_binds_pascal_case_from_appsettings() {
515 let json = serde_json::json!({
518 "Cors": {
519 "Origins": ["https://www.lusida.net", "https://lusida.net"],
520 "Methods": ["GET", "POST"],
521 "Headers": ["Content-Type", "Authorization"],
522 "AllowCredentials": true,
523 "MaxAge": 3600
524 }
525 });
526 let opts: AppOptions = bind_root(&json);
527 assert_eq!(
528 opts.cors.origins,
529 vec![
530 "https://www.lusida.net".to_string(),
531 "https://lusida.net".to_string()
532 ]
533 );
534 assert_eq!(
535 opts.cors.methods,
536 vec!["GET".to_string(), "POST".to_string()]
537 );
538 assert!(opts.cors.allow_credentials);
539 assert_eq!(opts.cors.max_age, 3600);
540 assert!(!opts.cors.origins.iter().any(|o| o == "*"));
541 }
542
543 #[test]
544 fn culture_section_binds_default_and_falls_back_to_en() {
545 let json = serde_json::json!({ "Culture": { "Default": "zh-CN" } });
546 let opts: AppOptions = bind_root(&json);
547 assert_eq!(opts.culture.default, "zh-CN");
548
549 let opts: AppOptions = bind_root(&serde_json::json!({}));
550 assert_eq!(opts.culture.default, "en");
551 }
552
553 #[test]
554 fn production_overlay_merge_keeps_explicit_cors_origins() {
555 let mut base = serde_json::json!({
556 "App": { "Urls": ["http://localhost:5000"] },
557 "Cors": { "Origins": ["*"] },
558 "Jwt": { "Secret": "docbit-dev-secret-change-in-production" }
559 });
560 let overlay = serde_json::json!({
561 "App": { "Urls": ["http://0.0.0.0:8100"] },
562 "Cors": {
563 "Origins": [
564 "https://www.lusida.net",
565 "https://lusida.net"
566 ]
567 },
568 "Jwt": { "Secret": "" }
569 });
570 merge_json(&mut base, overlay);
571 let opts: AppOptions = bind_root(&base);
572 assert_eq!(opts.app.urls, vec!["http://0.0.0.0:8100".to_string()]);
573 assert_eq!(
574 opts.cors.origins,
575 vec![
576 "https://www.lusida.net".to_string(),
577 "https://lusida.net".to_string()
578 ]
579 );
580 assert!(opts.jwt.secret.is_empty());
581 }
582}