hero/interfaces/websocket/server/src/config.rs
2025-07-30 08:36:55 +02:00

221 lines
6.3 KiB
Rust

use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerConfig {
/// Server host address
#[serde(default = "default_host")]
pub host: String,
/// Server port
#[serde(default = "default_port")]
pub port: u16,
/// Redis connection URL
#[serde(default = "default_redis_url")]
pub redis_url: String,
/// Enable authentication
#[serde(default)]
pub auth: bool,
/// Enable TLS/WSS
#[serde(default)]
pub tls: bool,
/// Path to TLS certificate file
pub cert: Option<String>,
/// Path to TLS private key file
pub key: Option<String>,
/// Separate port for TLS connections
pub tls_port: Option<u16>,
/// Enable webhook handling
#[serde(default)]
pub webhooks: bool,
/// Circles configuration - maps circle names to lists of member public keys
#[serde(default)]
pub circles: HashMap<String, Vec<String>>,
}
impl Default for ServerConfig {
fn default() -> Self {
Self {
host: default_host(),
port: default_port(),
redis_url: default_redis_url(),
auth: false,
tls: false,
cert: None,
key: None,
tls_port: None,
webhooks: false,
circles: HashMap::new(),
}
}
}
impl ServerConfig {
/// Load configuration from a JSON file
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, ConfigError> {
let content = fs::read_to_string(path.as_ref())
.map_err(|e| ConfigError::FileRead(path.as_ref().to_path_buf(), e))?;
let config: ServerConfig = serde_json::from_str(&content)
.map_err(|e| ConfigError::JsonParse(e))?;
config.validate()?;
Ok(config)
}
/// Save configuration to a JSON file
pub fn to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), ConfigError> {
let content = serde_json::to_string_pretty(self)
.map_err(|e| ConfigError::JsonSerialize(e))?;
fs::write(path.as_ref(), content)
.map_err(|e| ConfigError::FileWrite(path.as_ref().to_path_buf(), e))?;
Ok(())
}
/// Validate the configuration
pub fn validate(&self) -> Result<(), ConfigError> {
// Validate TLS configuration
if self.tls && (self.cert.is_none() || self.key.is_none()) {
return Err(ConfigError::InvalidTlsConfig(
"TLS is enabled but certificate or key path is missing".to_string()
));
}
// Validate that circles are not empty if auth is enabled
if self.auth && self.circles.is_empty() {
return Err(ConfigError::InvalidAuthConfig(
"Authentication is enabled but no circles are configured".to_string()
));
}
Ok(())
}
/// Create a sample configuration file
pub fn create_sample() -> Self {
let mut circles = HashMap::new();
circles.insert(
"example_circle".to_string(),
vec![
"0x1234567890abcdef1234567890abcdef12345678".to_string(),
"0xabcdef1234567890abcdef1234567890abcdef12".to_string(),
]
);
Self {
host: "127.0.0.1".to_string(),
port: 8443,
redis_url: "redis://127.0.0.1/".to_string(),
auth: true,
tls: false,
cert: Some("cert.pem".to_string()),
key: Some("key.pem".to_string()),
tls_port: Some(8444),
webhooks: false,
circles,
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
#[error("Failed to read config file {0}: {1}")]
FileRead(std::path::PathBuf, std::io::Error),
#[error("Failed to write config file {0}: {1}")]
FileWrite(std::path::PathBuf, std::io::Error),
#[error("Failed to parse JSON config: {0}")]
JsonParse(serde_json::Error),
#[error("Failed to serialize JSON config: {0}")]
JsonSerialize(serde_json::Error),
#[error("Invalid TLS configuration: {0}")]
InvalidTlsConfig(String),
#[error("Invalid authentication configuration: {0}")]
InvalidAuthConfig(String),
}
// Default value functions
fn default_host() -> String {
"127.0.0.1".to_string()
}
fn default_port() -> u16 {
8443
}
fn default_redis_url() -> String {
"redis://127.0.0.1/".to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::NamedTempFile;
#[test]
fn test_config_serialization() {
let config = ServerConfig::create_sample();
let json = serde_json::to_string_pretty(&config).unwrap();
let deserialized: ServerConfig = serde_json::from_str(&json).unwrap();
assert_eq!(config.host, deserialized.host);
assert_eq!(config.port, deserialized.port);
assert_eq!(config.circles.len(), deserialized.circles.len());
}
#[test]
fn test_config_file_operations() {
let config = ServerConfig::create_sample();
let temp_file = NamedTempFile::new().unwrap();
// Test writing
config.to_file(temp_file.path()).unwrap();
// Test reading
let loaded_config = ServerConfig::from_file(temp_file.path()).unwrap();
assert_eq!(config.host, loaded_config.host);
assert_eq!(config.circles.len(), loaded_config.circles.len());
}
#[test]
fn test_config_validation() {
let mut config = ServerConfig::default();
// Valid config should pass
assert!(config.validate().is_ok());
// TLS enabled without cert/key should fail
config.tls = true;
assert!(config.validate().is_err());
// Fix TLS config
config.cert = Some("cert.pem".to_string());
config.key = Some("key.pem".to_string());
assert!(config.validate().is_ok());
// Auth enabled without circles should fail
config.auth = true;
assert!(config.validate().is_err());
// Add circles
config.circles.insert("test".to_string(), vec!["pubkey".to_string()]);
assert!(config.validate().is_ok());
}
}