Skip to main content

rush_sync_server/core/
config.rs

1// src/core/config.rs - Cleaned and simplified
2use crate::core::api_key::ApiKey;
3use crate::core::constants::{DEFAULT_BUFFER_SIZE, DEFAULT_POLL_RATE};
4use crate::core::prelude::*;
5use crate::proxy::types::{ProxyConfig, ProxyConfigToml};
6use crate::ui::color::AppColor;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::path::Path;
10
11// TOML Configuration Structure
12#[derive(Debug, Serialize, Deserialize)]
13struct ConfigFile {
14    general: GeneralConfig,
15    #[serde(default)]
16    server: Option<ServerConfigToml>,
17    #[serde(default)]
18    logging: Option<LoggingConfigToml>,
19    #[serde(default)]
20    theme: Option<HashMap<String, ThemeDefinitionConfig>>,
21    language: LanguageConfig,
22    proxy: Option<ProxyConfigToml>,
23}
24
25#[derive(Debug, Serialize, Deserialize)]
26struct GeneralConfig {
27    max_messages: usize,
28    typewriter_delay: u64,
29    input_max_length: usize,
30    max_history: usize,
31    poll_rate: u64,
32    log_level: String,
33    #[serde(default = "default_theme")]
34    current_theme: String,
35}
36
37#[derive(Debug, Serialize, Deserialize)]
38struct LanguageConfig {
39    current: String,
40}
41
42#[derive(Debug, Serialize, Deserialize, Clone)]
43struct ServerConfigToml {
44    #[serde(default = "default_port_start")]
45    port_range_start: u16,
46    #[serde(default = "default_port_end")]
47    port_range_end: u16,
48    #[serde(default = "default_max_concurrent")]
49    max_concurrent: usize,
50    #[serde(default = "default_shutdown_timeout")]
51    shutdown_timeout: u64,
52    #[serde(default = "default_startup_delay")]
53    startup_delay_ms: u64,
54    #[serde(default = "default_workers")]
55    workers: usize,
56    #[serde(default = "default_auto_open_browser")]
57    auto_open_browser: bool,
58    #[serde(default = "default_bind_address")]
59    bind_address: String,
60
61    // TLS Configuration
62    #[serde(default = "default_enable_https")]
63    enable_https: bool,
64    #[serde(default = "default_https_port_offset")]
65    https_port_offset: u16,
66    #[serde(default = "default_cert_dir")]
67    cert_dir: String,
68    #[serde(default = "default_auto_cert")]
69    auto_cert: bool,
70    #[serde(default = "default_cert_validity_days")]
71    cert_validity_days: u32,
72
73    // Production Settings
74    #[serde(default = "default_use_lets_encrypt")]
75    use_lets_encrypt: bool,
76    #[serde(default = "default_production_domain")]
77    production_domain: String,
78    #[serde(default)]
79    acme_email: String,
80
81    // Security
82    #[serde(default)]
83    api_key: String,
84
85    // Rate Limiting
86    #[serde(default = "default_rate_limit_rps")]
87    rate_limit_rps: u32,
88    #[serde(default = "default_rate_limit_enabled")]
89    rate_limit_enabled: bool,
90}
91
92#[derive(Debug, Serialize, Deserialize, Clone)]
93struct LoggingConfigToml {
94    #[serde(default = "default_max_file_size")]
95    max_file_size_mb: u64,
96    #[serde(default = "default_max_archive_files")]
97    max_archive_files: u8,
98    #[serde(default = "default_compress_archives")]
99    compress_archives: bool,
100    #[serde(default = "default_log_requests")]
101    log_requests: bool,
102    #[serde(default = "default_log_security")]
103    log_security_alerts: bool,
104    #[serde(default = "default_log_performance")]
105    log_performance: bool,
106}
107
108#[derive(Debug, Serialize, Deserialize, Clone)]
109struct ThemeDefinitionConfig {
110    input_text: String,
111    input_bg: String,
112    output_text: String,
113    output_bg: String,
114    #[serde(default = "default_prefix")]
115    input_cursor_prefix: String,
116    #[serde(default = "default_input_color")]
117    input_cursor_color: String,
118    #[serde(default = "default_cursor")]
119    input_cursor: String,
120    #[serde(default = "default_cursor")]
121    output_cursor: String,
122    #[serde(default = "default_output_color")]
123    output_cursor_color: String,
124}
125
126// Default Functions
127fn default_theme() -> String {
128    "dark".into()
129}
130fn default_prefix() -> String {
131    "/// ".into()
132}
133fn default_input_color() -> String {
134    "LightBlue".into()
135}
136fn default_output_color() -> String {
137    "White".into()
138}
139fn default_cursor() -> String {
140    "PIPE".into()
141}
142
143// Server Defaults
144fn default_port_start() -> u16 {
145    8001
146}
147fn default_port_end() -> u16 {
148    8100
149}
150fn default_max_concurrent() -> usize {
151    100
152}
153fn default_shutdown_timeout() -> u64 {
154    5
155}
156fn default_startup_delay() -> u64 {
157    500
158}
159fn default_workers() -> usize {
160    1
161}
162fn default_auto_open_browser() -> bool {
163    true
164}
165
166// TLS Defaults
167fn default_enable_https() -> bool {
168    true
169}
170fn default_https_port_offset() -> u16 {
171    1000
172}
173fn default_cert_dir() -> String {
174    ".rss/certs".to_string()
175}
176fn default_auto_cert() -> bool {
177    true
178}
179fn default_cert_validity_days() -> u32 {
180    365
181}
182fn default_use_lets_encrypt() -> bool {
183    false
184}
185fn default_production_domain() -> String {
186    "localhost".to_string()
187}
188fn default_bind_address() -> String {
189    "127.0.0.1".to_string()
190}
191fn default_rate_limit_rps() -> u32 {
192    100
193}
194fn default_rate_limit_enabled() -> bool {
195    true
196}
197
198// Logging Defaults
199fn default_max_file_size() -> u64 {
200    100
201}
202fn default_max_archive_files() -> u8 {
203    9
204}
205fn default_compress_archives() -> bool {
206    true
207}
208fn default_log_requests() -> bool {
209    true
210}
211fn default_log_security() -> bool {
212    true
213}
214fn default_log_performance() -> bool {
215    true
216}
217
218// Main Configuration Structures
219#[derive(Clone)]
220pub struct Config {
221    config_path: Option<String>,
222    pub max_messages: usize,
223    pub typewriter_delay: Duration,
224    pub input_max_length: usize,
225    pub max_history: usize,
226    pub poll_rate: Duration,
227    pub log_level: String,
228    pub theme: Theme,
229    pub current_theme_name: String,
230    pub language: String,
231    pub debug_info: Option<String>,
232    pub server: ServerConfig,
233    pub logging: LoggingConfig,
234    pub proxy: ProxyConfig,
235}
236
237#[derive(Clone)]
238pub struct ServerConfig {
239    pub port_range_start: u16,
240    pub port_range_end: u16,
241    pub max_concurrent: usize,
242    pub shutdown_timeout: u64,
243    pub startup_delay_ms: u64,
244    pub workers: usize,
245    pub auto_open_browser: bool,
246    pub bind_address: String,
247
248    // TLS Configuration
249    pub enable_https: bool,
250    pub https_port_offset: u16,
251    pub cert_dir: String,
252    pub auto_cert: bool,
253    pub cert_validity_days: u32,
254    pub use_lets_encrypt: bool,
255    pub production_domain: String,
256    pub acme_email: String,
257
258    // Security
259    pub api_key: ApiKey,
260
261    // Rate Limiting
262    pub rate_limit_rps: u32,
263    pub rate_limit_enabled: bool,
264}
265
266#[derive(Clone)]
267pub struct LoggingConfig {
268    pub max_file_size_mb: u64,
269    pub max_archive_files: u8,
270    pub compress_archives: bool,
271    pub log_requests: bool,
272    pub log_security_alerts: bool,
273    pub log_performance: bool,
274}
275
276#[derive(Clone)]
277pub struct Theme {
278    pub input_text: AppColor,
279    pub input_bg: AppColor,
280    pub output_text: AppColor,
281    pub output_bg: AppColor,
282    pub input_cursor_prefix: String,
283    pub input_cursor_color: AppColor,
284    pub input_cursor: String,
285    pub output_cursor: String,
286    pub output_cursor_color: AppColor,
287}
288
289impl Default for Theme {
290    fn default() -> Self {
291        Self {
292            input_text: AppColor::new(Color::White),
293            input_bg: AppColor::new(Color::Black),
294            output_text: AppColor::new(Color::White),
295            output_bg: AppColor::new(Color::Black),
296            input_cursor_prefix: "/// ".into(),
297            input_cursor_color: AppColor::new(Color::LightBlue),
298            input_cursor: "PIPE".into(),
299            output_cursor: "PIPE".into(),
300            output_cursor_color: AppColor::new(Color::White),
301        }
302    }
303}
304
305impl Default for ServerConfig {
306    fn default() -> Self {
307        Self {
308            port_range_start: 8001,
309            port_range_end: 8100,
310            max_concurrent: 100,
311            shutdown_timeout: 5,
312            startup_delay_ms: 500,
313            workers: 1,
314            auto_open_browser: true,
315            bind_address: "127.0.0.1".to_string(),
316            enable_https: true,
317            https_port_offset: 1000,
318            cert_dir: ".rss/certs".to_string(),
319            auto_cert: true,
320            cert_validity_days: 365,
321            use_lets_encrypt: false,
322            production_domain: "localhost".to_string(),
323            acme_email: String::new(),
324            api_key: ApiKey::empty(),
325            rate_limit_rps: 100,
326            rate_limit_enabled: true,
327        }
328    }
329}
330
331impl Default for LoggingConfig {
332    fn default() -> Self {
333        Self {
334            max_file_size_mb: 100,
335            max_archive_files: 9,
336            compress_archives: true,
337            log_requests: true,
338            log_security_alerts: true,
339            log_performance: true,
340        }
341    }
342}
343
344impl Config {
345    pub async fn load() -> Result<Self> {
346        Self::load_with_messages(true).await
347    }
348
349    pub async fn load_with_messages(show_messages: bool) -> Result<Self> {
350        // Try existing configs
351        for path in crate::setup::setup_toml::get_config_paths() {
352            if path.exists() {
353                if let Ok(config) = Self::from_file(&path).await {
354                    if show_messages {
355                        Self::log_startup(&config);
356                    }
357                    Self::apply_language(&config).await;
358                    return Ok(config);
359                }
360            }
361        }
362
363        // Create new config
364        let path = crate::setup::setup_toml::ensure_config_exists().await?;
365        let mut config = Self::from_file(&path).await?;
366
367        if show_messages {
368            config.debug_info = Some(format!("New config: {}", path.display()));
369            Self::log_startup(&config);
370        }
371
372        Self::apply_language(&config).await;
373        Ok(config)
374    }
375
376    pub async fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
377        let content = tokio::fs::read_to_string(&path)
378            .await
379            .map_err(AppError::Io)?;
380        let file: ConfigFile =
381            toml::from_str(&content).map_err(|e| AppError::Validation(format!("TOML: {}", e)))?;
382
383        let poll_rate = Self::clamp(file.general.poll_rate, 16, 1000, 16);
384        let typewriter = Self::clamp(file.general.typewriter_delay, 0, 2000, 50);
385        let theme = Self::load_theme(&file).unwrap_or_default();
386
387        // Load server config
388        let server = file
389            .server
390            .map_or_else(ServerConfig::default, |s| {
391                // Env-var override for API key (RSS_API_KEY takes precedence over TOML)
392                let api_key = if let Ok(env_val) = std::env::var("RSS_API_KEY") {
393                    if !env_val.is_empty() {
394                        ApiKey::from_env(&env_val)
395                    } else {
396                        ApiKey::from_toml(&s.api_key)
397                    }
398                } else {
399                    ApiKey::from_toml(&s.api_key)
400                };
401
402                ServerConfig {
403                    port_range_start: s.port_range_start,
404                    port_range_end: s.port_range_end,
405                    max_concurrent: s.max_concurrent,
406                    shutdown_timeout: s.shutdown_timeout,
407                    startup_delay_ms: s.startup_delay_ms,
408                    workers: s.workers,
409                    auto_open_browser: s.auto_open_browser,
410                    bind_address: s.bind_address,
411                    enable_https: s.enable_https,
412                    https_port_offset: s.https_port_offset,
413                    cert_dir: s.cert_dir,
414                    auto_cert: s.auto_cert,
415                    cert_validity_days: s.cert_validity_days,
416                    use_lets_encrypt: s.use_lets_encrypt,
417                    production_domain: s.production_domain,
418                    acme_email: s.acme_email,
419                    api_key,
420                    rate_limit_rps: s.rate_limit_rps,
421                    rate_limit_enabled: s.rate_limit_enabled,
422                }
423            });
424
425        // Load logging config
426        let logging = file
427            .logging
428            .map_or_else(LoggingConfig::default, |l| LoggingConfig {
429                max_file_size_mb: l.max_file_size_mb,
430                max_archive_files: l.max_archive_files,
431                compress_archives: l.compress_archives,
432                log_requests: l.log_requests,
433                log_security_alerts: l.log_security_alerts,
434                log_performance: l.log_performance,
435            });
436
437        let config = Self {
438            config_path: Some(path.as_ref().to_string_lossy().into_owned()),
439            max_messages: file.general.max_messages,
440            typewriter_delay: Duration::from_millis(typewriter),
441            input_max_length: file.general.input_max_length,
442            max_history: file.general.max_history,
443            poll_rate: Duration::from_millis(poll_rate),
444            log_level: file.general.log_level,
445            theme,
446            current_theme_name: file.general.current_theme,
447            language: file.language.current,
448            debug_info: None,
449            proxy: {
450                let mut proxy = file.proxy.map(ProxyConfig::from).unwrap_or_default();
451                // Inject server-level settings so the proxy doesn't need to re-load config
452                proxy.production_domain = server.production_domain.clone();
453                proxy.use_lets_encrypt = server.use_lets_encrypt;
454                proxy
455            },
456            server,
457            logging,
458        };
459
460        // Auto-save corrected values
461        if poll_rate != file.general.poll_rate || typewriter != file.general.typewriter_delay {
462            let _ = config.save().await;
463        }
464
465        Ok(config)
466    }
467
468    pub async fn save(&self) -> Result<()> {
469        let Some(path) = &self.config_path else {
470            return Ok(());
471        };
472
473        let themes = Self::load_existing_themes().await.unwrap_or_default();
474        let file = ConfigFile {
475            general: GeneralConfig {
476                max_messages: self.max_messages,
477                typewriter_delay: self.typewriter_delay.as_millis() as u64,
478                input_max_length: self.input_max_length,
479                max_history: self.max_history,
480                poll_rate: self.poll_rate.as_millis() as u64,
481                log_level: self.log_level.clone(),
482                current_theme: self.current_theme_name.clone(),
483            },
484            server: Some(ServerConfigToml {
485                port_range_start: self.server.port_range_start,
486                port_range_end: self.server.port_range_end,
487                max_concurrent: self.server.max_concurrent,
488                shutdown_timeout: self.server.shutdown_timeout,
489                startup_delay_ms: self.server.startup_delay_ms,
490                workers: self.server.workers,
491                auto_open_browser: self.server.auto_open_browser,
492                bind_address: self.server.bind_address.clone(),
493                enable_https: self.server.enable_https,
494                https_port_offset: self.server.https_port_offset,
495                cert_dir: self.server.cert_dir.clone(),
496                auto_cert: self.server.auto_cert,
497                cert_validity_days: self.server.cert_validity_days,
498                use_lets_encrypt: self.server.use_lets_encrypt,
499                production_domain: self.server.production_domain.clone(),
500                acme_email: self.server.acme_email.clone(),
501                api_key: self.server.api_key.to_toml_value(),
502                rate_limit_rps: self.server.rate_limit_rps,
503                rate_limit_enabled: self.server.rate_limit_enabled,
504            }),
505            logging: Some(LoggingConfigToml {
506                max_file_size_mb: self.logging.max_file_size_mb,
507                max_archive_files: self.logging.max_archive_files,
508                compress_archives: self.logging.compress_archives,
509                log_requests: self.logging.log_requests,
510                log_security_alerts: self.logging.log_security_alerts,
511                log_performance: self.logging.log_performance,
512            }),
513            theme: if themes.is_empty() {
514                None
515            } else {
516                Some(themes)
517            },
518            language: LanguageConfig {
519                current: self.language.clone(),
520            },
521            proxy: Some(self.proxy.clone().into()),
522        };
523
524        let content = toml::to_string_pretty(&file)
525            .map_err(|e| AppError::Validation(format!("TOML: {}", e)))?;
526
527        // Ensure dir exists
528        if let Some(parent) = std::path::PathBuf::from(path).parent() {
529            tokio::fs::create_dir_all(parent)
530                .await
531                .map_err(AppError::Io)?;
532        }
533
534        tokio::fs::write(path, content).await.map_err(AppError::Io)
535    }
536
537    pub async fn change_theme(&mut self, name: &str) -> Result<()> {
538        let themes = Self::load_existing_themes().await?;
539        let def = themes
540            .get(name)
541            .ok_or_else(|| AppError::Validation(format!("Theme '{}' not found", name)))?;
542
543        self.theme = Theme::from_config(def)?;
544        self.current_theme_name = name.into();
545        self.save().await
546    }
547
548    pub fn get_performance_info(&self) -> String {
549        let fps = 1000.0 / self.poll_rate.as_millis() as f64;
550        let typewriter = if self.typewriter_delay.as_millis() > 0 {
551            1000.0 / self.typewriter_delay.as_millis() as f64
552        } else {
553            f64::INFINITY
554        };
555        format!(
556            "Performance: {:.1} FPS, Typewriter: {:.1} chars/sec, Max Servers: {}",
557            fps, typewriter, self.server.max_concurrent
558        )
559    }
560
561    // Helper methods
562    fn clamp(value: u64, min: u64, max: u64, default: u64) -> u64 {
563        if value < min || value > max {
564            default
565        } else {
566            value
567        }
568    }
569
570    fn load_theme(file: &ConfigFile) -> Option<Theme> {
571        let themes = file.theme.as_ref()?;
572        let def = themes.get(&file.general.current_theme)?;
573        Theme::from_config(def).ok()
574    }
575
576    async fn load_existing_themes() -> Result<HashMap<String, ThemeDefinitionConfig>> {
577        for path in crate::setup::setup_toml::get_config_paths() {
578            if path.exists() {
579                let content = tokio::fs::read_to_string(&path)
580                    .await
581                    .map_err(AppError::Io)?;
582                let file: ConfigFile = toml::from_str(&content)
583                    .map_err(|e| AppError::Validation(format!("TOML: {}", e)))?;
584
585                if let Some(themes) = file.theme {
586                    return Ok(themes);
587                }
588            }
589        }
590        Ok(HashMap::new())
591    }
592
593    async fn apply_language(config: &Config) {
594        let _ = crate::commands::lang::LanguageService::new()
595            .load_and_apply_from_config(config)
596            .await;
597    }
598
599    fn log_startup(config: &Config) {
600        if config.poll_rate.as_millis() < 16 {
601            log::warn!("Performance: poll_rate sehr niedrig!");
602        }
603        log::info!("Rush Sync Server v{}", crate::core::constants::VERSION);
604        log::info!(
605            "Server Config: Ports {}-{}, Max: {}",
606            config.server.port_range_start,
607            config.server.port_range_end,
608            config.server.max_concurrent
609        );
610    }
611}
612
613impl Theme {
614    fn from_config(def: &ThemeDefinitionConfig) -> Result<Self> {
615        Ok(Self {
616            input_text: AppColor::from_string(&def.input_text)?,
617            input_bg: AppColor::from_string(&def.input_bg)?,
618            output_text: AppColor::from_string(&def.output_text)?,
619            output_bg: AppColor::from_string(&def.output_bg)?,
620            input_cursor_prefix: def.input_cursor_prefix.clone(),
621            input_cursor_color: AppColor::from_string(&def.input_cursor_color)?,
622            input_cursor: def.input_cursor.clone(),
623            output_cursor: def.output_cursor.clone(),
624            output_cursor_color: AppColor::from_string(&def.output_cursor_color)?,
625        })
626    }
627}
628
629impl Default for Config {
630    fn default() -> Self {
631        Self {
632            config_path: None,
633            max_messages: DEFAULT_BUFFER_SIZE,
634            typewriter_delay: Duration::from_millis(50),
635            input_max_length: DEFAULT_BUFFER_SIZE,
636            max_history: 30,
637            poll_rate: Duration::from_millis(DEFAULT_POLL_RATE),
638            log_level: "info".into(),
639            theme: Theme::default(),
640            current_theme_name: "dark".into(),
641            language: crate::i18n::DEFAULT_LANGUAGE.into(),
642            debug_info: None,
643            server: ServerConfig::default(),
644            logging: LoggingConfig::default(),
645            proxy: ProxyConfig::default(),
646        }
647    }
648}