/ src / config.rs
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  }