config.rs
1 use clap::Parser; 2 use log::{debug, error, info}; 3 use rand::{self, Rng, distr::Alphanumeric}; 4 use serde::{Deserialize, Serialize}; 5 use std::net::{IpAddr, SocketAddr}; 6 7 use crate::db; 8 9 #[derive(Parser, Debug, Clone, Serialize, Deserialize)] 10 #[command(version, about = "prheri - Simple Monitor")] 11 pub struct Config { 12 /// Address to bind the server to 13 #[arg(short, long, default_value = "0.0.0.0", env = "prheri_ADDRESS")] 14 pub address: IpAddr, 15 16 /// Port to bind the server to 17 #[arg(short, long, default_value = "30000", env = "prheri_PORT")] 18 pub port: u16, 19 20 /// Update interval in seconds 21 #[arg(short = 'T', long, default_value = "2", value_parser = clap::value_parser!(u64).range(1..=30), env = "prheri_UPDATE_INTERVAL")] 22 pub update_interval: u64, 23 24 /// Authentication password bcrypt hash. 25 /// If provided, authentication will be required. Leave empty to disable authentication. 26 #[arg(short = 'H', long, env = "prheri_PASSWORD_HASH")] 27 pub password_hash: Option<String>, 28 29 /// Database path 30 #[arg(long, default_value = "./prheri-data/prheri.db", env = "prheri_DB_PATH")] 31 pub db_path: String, 32 33 /// JWT secret key for authentication tokens 34 #[arg(skip)] 35 pub jwt_secret: String, 36 } 37 38 impl Config { 39 pub fn socket_address(&self) -> SocketAddr { 40 SocketAddr::new(self.address, self.port) 41 } 42 } 43 44 pub fn parse_config() -> Config { 45 let mut config = Config::parse(); 46 47 // Ensure database directory exists 48 if let Some(parent) = std::path::Path::new(&config.db_path).parent() { 49 if !parent.exists() { 50 info!("Creating directory for database at: {}", parent.display()); 51 if let Err(e) = std::fs::create_dir_all(parent) { 52 error!("Failed to create database directory: {}", e); 53 std::process::exit(1); 54 } 55 } 56 } 57 58 if config.password_hash.is_some() { 59 // check if valid bcrypt 60 if !config.password_hash.as_ref().unwrap().starts_with("$2") { 61 error!("Invalid password: Password must be a valid bcrypt hash starting with '$2'"); 62 std::process::exit(1); 63 } 64 } 65 66 let db = match db::Database::new(&config.db_path.clone()) { 67 Ok(db) => db, 68 Err(e) => { 69 error!("Failed to open database: {}", e); 70 std::process::exit(1); 71 } 72 }; 73 74 match db.get_kv_str("jwt_secret") { 75 Ok(Some(secret)) => { 76 config.jwt_secret = secret; 77 } 78 Ok(None) => { 79 if config.jwt_secret.is_empty() { 80 // set to random value 81 let mut rng = rand::rng(); 82 let jwt_secret: String = (&mut rng) 83 .sample_iter(Alphanumeric) 84 .take(63) 85 .map(char::from) 86 .collect(); 87 config.jwt_secret = jwt_secret.clone(); 88 89 if let Err(e) = db.set_kv_str("jwt_secret", &jwt_secret) { 90 eprintln!("Failed to write to database: {}", e); 91 std::process::exit(1); 92 } 93 } 94 } 95 Err(e) => { 96 error!("Failed to read from database: {}", e); 97 std::process::exit(1); 98 } 99 } 100 101 debug!("Config: {:?}", config); 102 103 config 104 }