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}