gitorii 0.6.3

A human-first Git client with simplified commands, snapshots, multi-platform mirrors and built-in secret scanning
//! Local API key storage for torii cloud features.
//!
//! Storage: `~/.config/torii/auth.toml` (chmod 600 on Unix).
//!
//! Override: env var `TORII_API_KEY` always wins. Useful for CI.

use std::fs;
use std::path::PathBuf;

use crate::error::{Result, ToriiError};

const ENV_VAR: &str = "TORII_API_KEY";
const FILE_NAME: &str = "auth.toml";

#[derive(Debug, Clone, Default)]
pub struct ApiKey {
    pub key: String,
    /// Endpoint root used to validate this key. `https://api.gitorii.com` by
    /// default; can be overridden per-key for self-hosted / local dev.
    pub endpoint: String,
}

pub fn default_endpoint() -> String {
    std::env::var("TORII_API_ENDPOINT")
        .unwrap_or_else(|_| "https://api.gitorii.com".to_string())
}

/// Resolve an API key from env (highest priority) or local file.
/// Returns None when no key is configured — caller decides whether that's
/// an error or a sign to skip cloud features.
pub fn load() -> Option<ApiKey> {
    if let Ok(env_key) = std::env::var(ENV_VAR) {
        if !env_key.is_empty() {
            return Some(ApiKey {
                key: env_key,
                endpoint: default_endpoint(),
            });
        }
    }
    let path = config_path()?;
    if !path.exists() {
        return None;
    }
    let text = fs::read_to_string(&path).ok()?;
    parse(&text)
}

/// Persist a key to `~/.config/torii/auth.toml` with chmod 600.
pub fn save(key: &str, endpoint: &str) -> Result<()> {
    let path = config_path().ok_or_else(|| {
        ToriiError::InvalidConfig("could not resolve config dir".to_string())
    })?;
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)
            .map_err(|e| ToriiError::InvalidConfig(format!("create dir: {}", e)))?;
    }
    let content = format!(
        "# torii API key — generated by `torii auth login`. Do not share.\n\
         key = \"{}\"\n\
         endpoint = \"{}\"\n",
        key, endpoint,
    );
    fs::write(&path, content)
        .map_err(|e| ToriiError::InvalidConfig(format!("write {}: {}", path.display(), e)))?;
    restrict_permissions(&path);
    Ok(())
}

/// Delete the on-disk key. Idempotent — missing file is OK.
pub fn delete() -> Result<()> {
    let Some(path) = config_path() else {
        return Ok(());
    };
    if path.exists() {
        fs::remove_file(&path)
            .map_err(|e| ToriiError::InvalidConfig(format!("remove {}: {}", path.display(), e)))?;
    }
    Ok(())
}

fn config_path() -> Option<PathBuf> {
    dirs::config_dir().map(|d| d.join("torii").join(FILE_NAME))
}

fn parse(text: &str) -> Option<ApiKey> {
    let mut key = String::new();
    let mut endpoint = default_endpoint();
    for line in text.lines() {
        let line = line.trim();
        if line.is_empty() || line.starts_with('#') {
            continue;
        }
        let (k, v) = line.split_once('=')?;
        let k = k.trim();
        let v = v.trim().trim_matches('"').to_string();
        match k {
            "key" => key = v,
            "endpoint" => endpoint = v,
            _ => {}
        }
    }
    if key.is_empty() {
        None
    } else {
        Some(ApiKey { key, endpoint })
    }
}

#[cfg(unix)]
fn restrict_permissions(path: &std::path::Path) {
    use std::os::unix::fs::PermissionsExt;
    let _ = fs::set_permissions(path, fs::Permissions::from_mode(0o600));
}

#[cfg(not(unix))]
fn restrict_permissions(_: &std::path::Path) {}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_minimal() {
        let k = parse("key = \"gitorii_sk_abc\"").unwrap();
        assert_eq!(k.key, "gitorii_sk_abc");
    }

    #[test]
    fn parse_with_endpoint() {
        let k = parse("key = \"x\"\nendpoint = \"http://localhost:8080\"").unwrap();
        assert_eq!(k.endpoint, "http://localhost:8080");
    }

    #[test]
    fn parse_empty_returns_none() {
        assert!(parse("").is_none());
        assert!(parse("# comment only").is_none());
    }

    #[test]
    fn parse_ignores_unknown_keys() {
        let k = parse("key = \"x\"\nrandom = \"y\"").unwrap();
        assert_eq!(k.key, "x");
    }
}