1use std::path::PathBuf;
2
3use anyhow::{Context, bail};
4use serde::{Deserialize, Serialize};
5
6pub 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 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 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 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
85pub 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 #[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}