hammerwork_web/
config.rs

1//! Configuration for the Hammerwork web dashboard.
2//!
3//! This module provides comprehensive configuration options for the web dashboard,
4//! including server settings, authentication, WebSocket configuration, and more.
5//!
6//! # Examples
7//!
8//! ## Basic Configuration
9//!
10//! ```rust
11//! use hammerwork_web::config::DashboardConfig;
12//!
13//! let config = DashboardConfig::new()
14//!     .with_bind_address("127.0.0.1", 8080)
15//!     .with_database_url("postgresql://localhost/hammerwork");
16//!
17//! assert_eq!(config.bind_addr(), "127.0.0.1:8080");
18//! ```
19//!
20//! ## Configuration with Authentication
21//!
22//! ```rust
23//! use hammerwork_web::config::{DashboardConfig, AuthConfig};
24//! use std::time::Duration;
25//!
26//! let config = DashboardConfig::new()
27//!     .with_auth("admin", "$2b$12$hash...")
28//!     .with_cors(true);
29//!
30//! assert!(config.auth.enabled);
31//! assert_eq!(config.auth.username, "admin");
32//! assert!(config.enable_cors);
33//! ```
34//!
35//! ## Loading from File
36//!
37//! ```rust,no_run
38//! use hammerwork_web::config::DashboardConfig;
39//!
40//! // Create a configuration file (dashboard.toml)
41//! let config_content = r#"
42//! bind_address = "0.0.0.0"
43//! port = 9090
44//! database_url = "postgresql://localhost/hammerwork"
45//! enable_cors = true
46//!
47//! [auth]
48//! enabled = true
49//! username = "admin"
50//! "#;
51//!
52//! std::fs::write("dashboard.toml", config_content)?;
53//!
54//! // Load the configuration
55//! let config = DashboardConfig::from_file("dashboard.toml")?;
56//! assert_eq!(config.port, 9090);
57//! assert!(config.enable_cors);
58//!
59//! // Clean up
60//! std::fs::remove_file("dashboard.toml")?;
61//! # Ok::<(), Box<dyn std::error::Error>>(())
62//! ```
63
64use serde::{Deserialize, Serialize};
65use std::path::PathBuf;
66use std::time::Duration;
67
68/// Main configuration for the web dashboard.
69///
70/// This struct contains all configuration options for the Hammerwork web dashboard,
71/// including server settings, database connection, authentication, and WebSocket options.
72///
73/// # Examples
74///
75/// ```rust
76/// use hammerwork_web::config::DashboardConfig;
77/// use std::path::PathBuf;
78///
79/// // Create with defaults
80/// let config = DashboardConfig::default();
81/// assert_eq!(config.bind_address, "127.0.0.1");
82/// assert_eq!(config.port, 8080);
83///
84/// // Use builder pattern
85/// let config = DashboardConfig::new()
86///     .with_bind_address("0.0.0.0", 9090)
87///     .with_database_url("postgresql://localhost/hammerwork")
88///     .with_cors(true);
89///
90/// assert_eq!(config.bind_addr(), "0.0.0.0:9090");
91/// assert!(config.enable_cors);
92/// ```
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct DashboardConfig {
95    /// Server bind address
96    pub bind_address: String,
97
98    /// Server port
99    pub port: u16,
100
101    /// Database connection URL
102    pub database_url: String,
103
104    /// Database connection pool size
105    pub pool_size: u32,
106
107    /// Directory containing static assets (HTML, CSS, JS)
108    pub static_dir: PathBuf,
109
110    /// Authentication configuration
111    pub auth: AuthConfig,
112
113    /// WebSocket configuration
114    pub websocket: WebSocketConfig,
115
116    /// Enable CORS for cross-origin requests
117    pub enable_cors: bool,
118
119    /// Request timeout duration
120    pub request_timeout: Duration,
121}
122
123impl Default for DashboardConfig {
124    fn default() -> Self {
125        Self {
126            bind_address: "127.0.0.1".to_string(),
127            port: 8080,
128            database_url: "postgresql://localhost/hammerwork".to_string(),
129            pool_size: 5,
130            static_dir: PathBuf::from("./assets"),
131            auth: AuthConfig::default(),
132            websocket: WebSocketConfig::default(),
133            enable_cors: false,
134            request_timeout: Duration::from_secs(30),
135        }
136    }
137}
138
139impl DashboardConfig {
140    /// Create a new configuration with defaults.
141    ///
142    /// # Examples
143    ///
144    /// ```rust
145    /// use hammerwork_web::config::DashboardConfig;
146    ///
147    /// let config = DashboardConfig::new();
148    /// assert_eq!(config.bind_address, "127.0.0.1");
149    /// assert_eq!(config.port, 8080);
150    /// assert_eq!(config.database_url, "postgresql://localhost/hammerwork");
151    /// ```
152    pub fn new() -> Self {
153        Self::default()
154    }
155
156    /// Set the server bind address and port.
157    ///
158    /// # Examples
159    ///
160    /// ```rust
161    /// use hammerwork_web::config::DashboardConfig;
162    ///
163    /// let config = DashboardConfig::new()
164    ///     .with_bind_address("0.0.0.0", 9090);
165    ///
166    /// assert_eq!(config.bind_address, "0.0.0.0");
167    /// assert_eq!(config.port, 9090);
168    /// assert_eq!(config.bind_addr(), "0.0.0.0:9090");
169    /// ```
170    pub fn with_bind_address(mut self, address: &str, port: u16) -> Self {
171        self.bind_address = address.to_string();
172        self.port = port;
173        self
174    }
175
176    /// Set the database URL.
177    ///
178    /// Supports both PostgreSQL and MySQL database URLs.
179    ///
180    /// # Examples
181    ///
182    /// ```rust
183    /// use hammerwork_web::config::DashboardConfig;
184    ///
185    /// // PostgreSQL
186    /// let pg_config = DashboardConfig::new()
187    ///     .with_database_url("postgresql://user:pass@localhost/hammerwork");
188    /// assert_eq!(pg_config.database_url, "postgresql://user:pass@localhost/hammerwork");
189    ///
190    /// // MySQL
191    /// let mysql_config = DashboardConfig::new()
192    ///     .with_database_url("mysql://root:password@localhost/hammerwork");
193    /// assert_eq!(mysql_config.database_url, "mysql://root:password@localhost/hammerwork");
194    /// ```
195    pub fn with_database_url(mut self, url: &str) -> Self {
196        self.database_url = url.to_string();
197        self
198    }
199
200    /// Set the static assets directory.
201    ///
202    /// # Examples
203    ///
204    /// ```rust
205    /// use hammerwork_web::config::DashboardConfig;
206    /// use std::path::PathBuf;
207    ///
208    /// let config = DashboardConfig::new()
209    ///     .with_static_dir(PathBuf::from("/var/www/dashboard"));
210    ///
211    /// assert_eq!(config.static_dir, PathBuf::from("/var/www/dashboard"));
212    /// ```
213    pub fn with_static_dir(mut self, dir: PathBuf) -> Self {
214        self.static_dir = dir;
215        self
216    }
217
218    /// Enable authentication with username and password hash.
219    ///
220    /// The password should be a bcrypt hash for security. When authentication is enabled,
221    /// all API endpoints and WebSocket connections will require basic authentication.
222    ///
223    /// # Examples
224    ///
225    /// ```rust
226    /// use hammerwork_web::config::DashboardConfig;
227    ///
228    /// let config = DashboardConfig::new()
229    ///     .with_auth("admin", "$2b$12$hash...");
230    ///
231    /// assert!(config.auth.enabled);
232    /// assert_eq!(config.auth.username, "admin");
233    /// assert_eq!(config.auth.password_hash, "$2b$12$hash...");
234    /// ```
235    pub fn with_auth(mut self, username: &str, password_hash: &str) -> Self {
236        self.auth.enabled = true;
237        self.auth.username = username.to_string();
238        self.auth.password_hash = password_hash.to_string();
239        self
240    }
241
242    /// Enable or disable CORS support.
243    ///
244    /// When enabled, the server will accept cross-origin requests from any domain.
245    /// This is useful for development or when the dashboard is accessed from different domains.
246    ///
247    /// # Examples
248    ///
249    /// ```rust
250    /// use hammerwork_web::config::DashboardConfig;
251    ///
252    /// let config = DashboardConfig::new()
253    ///     .with_cors(true);
254    ///
255    /// assert!(config.enable_cors);
256    ///
257    /// let config = DashboardConfig::new()
258    ///     .with_cors(false);
259    ///
260    /// assert!(!config.enable_cors);
261    /// ```
262    pub fn with_cors(mut self, enabled: bool) -> Self {
263        self.enable_cors = enabled;
264        self
265    }
266
267    /// Load configuration from a TOML file
268    pub fn from_file(path: &str) -> crate::Result<Self> {
269        let content = std::fs::read_to_string(path)?;
270        let config: Self = toml::from_str(&content)?;
271        Ok(config)
272    }
273
274    /// Save configuration to a TOML file
275    pub fn save_to_file(&self, path: &str) -> crate::Result<()> {
276        let content = toml::to_string_pretty(self)?;
277        std::fs::write(path, content)?;
278        Ok(())
279    }
280
281    /// Get the full bind address (address:port)
282    pub fn bind_addr(&self) -> String {
283        format!("{}:{}", self.bind_address, self.port)
284    }
285}
286
287/// Authentication configuration for the web dashboard.
288///
289/// Controls authentication behavior including credentials, session management,
290/// and security policies like rate limiting and account lockout.
291///
292/// # Examples
293///
294/// ```rust
295/// use hammerwork_web::config::AuthConfig;
296/// use std::time::Duration;
297///
298/// // Default configuration (authentication enabled)
299/// let auth_config = AuthConfig::default();
300/// assert!(auth_config.enabled);
301/// assert_eq!(auth_config.username, "admin");
302/// assert_eq!(auth_config.max_failed_attempts, 5);
303///
304/// // Custom configuration
305/// let auth_config = AuthConfig {
306///     enabled: true,
307///     username: "dashboard_admin".to_string(),
308///     password_hash: "$2b$12$hash...".to_string(),
309///     session_timeout: Duration::from_secs(4 * 60 * 60), // 4 hours
310///     max_failed_attempts: 3,
311///     lockout_duration: Duration::from_secs(30 * 60), // 30 minutes
312/// };
313///
314/// assert_eq!(auth_config.username, "dashboard_admin");
315/// assert_eq!(auth_config.max_failed_attempts, 3);
316/// ```
317#[derive(Debug, Clone, Serialize, Deserialize)]
318pub struct AuthConfig {
319    /// Whether authentication is enabled
320    pub enabled: bool,
321
322    /// Username for basic authentication
323    pub username: String,
324
325    /// Bcrypt hash of the password
326    pub password_hash: String,
327
328    /// Session timeout duration
329    pub session_timeout: Duration,
330
331    /// Maximum number of failed login attempts
332    pub max_failed_attempts: u32,
333
334    /// Lockout duration after max failed attempts
335    pub lockout_duration: Duration,
336}
337
338impl Default for AuthConfig {
339    fn default() -> Self {
340        Self {
341            enabled: true, // Enable auth by default for security
342            username: "admin".to_string(),
343            password_hash: String::new(),
344            session_timeout: Duration::from_secs(8 * 60 * 60), // 8 hours
345            max_failed_attempts: 5,
346            lockout_duration: Duration::from_secs(15 * 60), // 15 minutes
347        }
348    }
349}
350
351/// WebSocket configuration
352#[derive(Debug, Clone, Serialize, Deserialize)]
353pub struct WebSocketConfig {
354    /// Ping interval to keep connections alive
355    pub ping_interval: Duration,
356
357    /// Maximum number of concurrent WebSocket connections
358    pub max_connections: usize,
359
360    /// Buffer size for WebSocket messages
361    pub message_buffer_size: usize,
362
363    /// Maximum message size in bytes
364    pub max_message_size: usize,
365}
366
367impl Default for WebSocketConfig {
368    fn default() -> Self {
369        Self {
370            ping_interval: Duration::from_secs(30),
371            max_connections: 100,
372            message_buffer_size: 1024,
373            max_message_size: 64 * 1024, // 64KB
374        }
375    }
376}
377
378#[cfg(test)]
379mod tests {
380    use super::*;
381    use tempfile::tempdir;
382
383    #[test]
384    fn test_config_creation() {
385        let config = DashboardConfig::new()
386            .with_bind_address("0.0.0.0", 9090)
387            .with_database_url("mysql://localhost/test")
388            .with_cors(true);
389
390        assert_eq!(config.bind_address, "0.0.0.0");
391        assert_eq!(config.port, 9090);
392        assert_eq!(config.database_url, "mysql://localhost/test");
393        assert!(config.enable_cors);
394        assert_eq!(config.bind_addr(), "0.0.0.0:9090");
395    }
396
397    #[test]
398    fn test_config_file_operations() {
399        let dir = tempdir().unwrap();
400        let config_path = dir.path().join("config.toml");
401
402        let config = DashboardConfig::new()
403            .with_bind_address("192.168.1.100", 8888)
404            .with_database_url("postgresql://test/db");
405
406        // Save config
407        config.save_to_file(config_path.to_str().unwrap()).unwrap();
408
409        // Load config
410        let loaded_config = DashboardConfig::from_file(config_path.to_str().unwrap()).unwrap();
411
412        assert_eq!(loaded_config.bind_address, "192.168.1.100");
413        assert_eq!(loaded_config.port, 8888);
414        assert_eq!(loaded_config.database_url, "postgresql://test/db");
415    }
416
417    #[test]
418    fn test_auth_config_defaults() {
419        let auth = AuthConfig::default();
420        assert!(auth.enabled); // Auth is enabled by default for security
421        assert_eq!(auth.username, "admin");
422        assert_eq!(auth.max_failed_attempts, 5);
423        assert_eq!(auth.lockout_duration.as_secs(), 15 * 60); // 15 minutes
424        assert_eq!(auth.session_timeout.as_secs(), 8 * 60 * 60); // 8 hours
425    }
426
427    #[test]
428    fn test_websocket_config_defaults() {
429        let ws_config = WebSocketConfig::default();
430        assert_eq!(ws_config.ping_interval, Duration::from_secs(30));
431        assert_eq!(ws_config.max_connections, 100);
432        assert_eq!(ws_config.max_message_size, 64 * 1024);
433    }
434}