Skip to main content

broccoli_cli/
auth.rs

1use std::path::PathBuf;
2
3use anyhow::{Context, bail};
4use serde::{Deserialize, Serialize};
5
6/// Resolved credentials for authenticating with a Broccoli server.
7pub struct Credentials {
8    pub server: String,
9    pub token: String,
10}
11
12#[derive(Serialize, Deserialize, Default)]
13struct CredentialsFile {
14    entries: Vec<CredentialEntry>,
15}
16
17#[derive(Serialize, Deserialize)]
18struct CredentialEntry {
19    server: String,
20    token: String,
21}
22
23fn credentials_path() -> PathBuf {
24    dirs::config_dir()
25        .unwrap_or_else(|| PathBuf::from("."))
26        .join("broccoli")
27        .join("credentials.json")
28}
29
30pub fn resolve_credentials(
31    server: Option<&str>,
32    token: Option<&str>,
33) -> anyhow::Result<Credentials> {
34    // If both are provided explicitly, use them
35    if let (Some(s), Some(t)) = (server, token) {
36        return Ok(Credentials {
37            server: s.to_string(),
38            token: t.to_string(),
39        });
40    }
41
42    // Check env vars
43    let env_server = std::env::var("BROCCOLI_URL").ok();
44    let env_token = std::env::var("BROCCOLI_TOKEN").ok();
45
46    let resolved_server = server.map(String::from).or(env_server);
47    let resolved_token = token.map(String::from).or(env_token);
48
49    if let (Some(s), Some(t)) = (resolved_server.as_ref(), resolved_token.as_ref()) {
50        return Ok(Credentials {
51            server: s.clone(),
52            token: t.clone(),
53        });
54    }
55
56    // Load saved credentials
57    let creds_path = credentials_path();
58    if creds_path.exists() {
59        let content =
60            std::fs::read_to_string(&creds_path).context("Failed to read credentials file")?;
61        let file: CredentialsFile =
62            serde_json::from_str(&content).context("Failed to parse credentials file")?;
63
64        if let Some(target_server) = &resolved_server {
65            if let Some(entry) = file.entries.iter().find(|e| &e.server == target_server) {
66                return Ok(Credentials {
67                    server: entry.server.clone(),
68                    token: resolved_token.unwrap_or_else(|| entry.token.clone()),
69                });
70            }
71        } else if let Some(entry) = file.entries.first() {
72            return Ok(Credentials {
73                server: entry.server.clone(),
74                token: resolved_token.unwrap_or_else(|| entry.token.clone()),
75            });
76        }
77    }
78
79    bail!(
80        "No credentials found.\n\
81         Run `broccoli login` to authenticate, or pass --server and --token."
82    );
83}
84
85/// Saves credentials for a server to ~/.config/broccoli/credentials.json.
86pub fn save_credentials(server: &str, token: &str) -> anyhow::Result<()> {
87    let creds_path = credentials_path();
88
89    let mut file = if creds_path.exists() {
90        let content = std::fs::read_to_string(&creds_path).unwrap_or_default();
91        serde_json::from_str::<CredentialsFile>(&content).unwrap_or_default()
92    } else {
93        CredentialsFile::default()
94    };
95
96    if let Some(entry) = file.entries.iter_mut().find(|e| e.server == server) {
97        entry.token = token.to_string();
98    } else {
99        file.entries.push(CredentialEntry {
100            server: server.to_string(),
101            token: token.to_string(),
102        });
103    }
104
105    if let Some(parent) = creds_path.parent() {
106        std::fs::create_dir_all(parent).context("Failed to create config directory")?;
107    }
108
109    let content = serde_json::to_string_pretty(&file)?;
110    std::fs::write(&creds_path, &content).context("Failed to write credentials file")?;
111
112    // Set file permissions to 0600 (owner-only read/write)
113    #[cfg(unix)]
114    {
115        use std::os::unix::fs::PermissionsExt;
116        let perms = std::fs::Permissions::from_mode(0o600);
117        std::fs::set_permissions(&creds_path, perms)?;
118    }
119
120    Ok(())
121}