oauth-db-cli 0.1.0

Command-line tool for managing OAuth-DB platform
Documentation
use crate::error::{CliError, Result};
use directories::ProjectDirs;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
    pub server: ServerConfig,
    pub output: OutputConfig,
    pub log: LogConfig,
    #[serde(default)]
    pub accounts: Vec<Account>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerConfig {
    pub url: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutputConfig {
    #[serde(default = "default_format")]
    pub format: String,
    #[serde(default = "default_color")]
    pub color: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogConfig {
    #[serde(default = "default_log_level")]
    pub level: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Account {
    pub name: String,
    pub server: String,
    pub username: String,
    pub token: String,
    #[serde(default)]
    pub default: bool,
    pub role: String,
}

fn default_format() -> String {
    "table".to_string()
}

fn default_color() -> bool {
    true
}

fn default_log_level() -> String {
    "info".to_string()
}

impl Default for Config {
    fn default() -> Self {
        Self {
            server: ServerConfig {
                url: "http://localhost:38080".to_string(),
            },
            output: OutputConfig {
                format: default_format(),
                color: default_color(),
            },
            log: LogConfig {
                level: default_log_level(),
            },
            accounts: Vec::new(),
        }
    }
}

impl Config {
    pub fn config_dir() -> Result<PathBuf> {
        ProjectDirs::from("com", "oauth-db", "oauth-db-cli")
            .map(|dirs| dirs.config_dir().to_path_buf())
            .ok_or_else(|| CliError::ConfigError("Failed to determine config directory".to_string()))
    }

    pub fn config_path() -> Result<PathBuf> {
        Ok(Self::config_dir()?.join("config.toml"))
    }

    pub fn load() -> Result<Self> {
        let path = Self::config_path()?;
        if !path.exists() {
            return Ok(Self::default());
        }

        let content = fs::read_to_string(&path)?;
        let config: Config = toml::from_str(&content)?;
        Ok(config)
    }

    pub fn save(&self) -> Result<()> {
        let dir = Self::config_dir()?;
        fs::create_dir_all(&dir)?;

        let path = Self::config_path()?;
        let content = toml::to_string_pretty(self)
            .map_err(|e| CliError::ConfigError(format!("Failed to serialize config: {}", e)))?;

        fs::write(&path, content)?;

        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            let mut perms = fs::metadata(&path)?.permissions();
            perms.set_mode(0o600);
            fs::set_permissions(&path, perms)?;
        }

        Ok(())
    }

    pub fn get_default_account(&self) -> Option<&Account> {
        self.accounts.iter().find(|a| a.default)
    }

    pub fn get_account(&self, name: &str) -> Option<&Account> {
        self.accounts.iter().find(|a| a.name == name)
    }

    pub fn add_account(&mut self, account: Account) {
        if account.default {
            for acc in &mut self.accounts {
                acc.default = false;
            }
        }
        self.accounts.push(account);
    }

    pub fn remove_account(&mut self, name: &str) -> bool {
        if let Some(pos) = self.accounts.iter().position(|a| a.name == name) {
            self.accounts.remove(pos);
            true
        } else {
            false
        }
    }

    pub fn set_default_account(&mut self, name: &str) -> bool {
        let mut found = false;
        for acc in &mut self.accounts {
            if acc.name == name {
                acc.default = true;
                found = true;
            } else {
                acc.default = false;
            }
        }
        found
    }
}