memstead_cli/auth/
credentials.rs1use std::collections::BTreeMap;
21use std::path::PathBuf;
22
23use anyhow::{Context, Result};
24use serde::{Deserialize, Serialize};
25use time::OffsetDateTime;
26use time::format_description::well_known::Rfc3339;
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct Entry {
31 pub token: String,
32 pub user_login: String,
33 #[serde(default)]
34 pub scopes: Vec<String>,
35 pub obtained_at: String,
37}
38
39impl Entry {
40 pub fn new(token: String, user_login: String, scopes: Vec<String>) -> Self {
41 let obtained_at = OffsetDateTime::now_utc()
42 .format(&Rfc3339)
43 .unwrap_or_else(|_| "1970-01-01T00:00:00Z".to_string());
44 Self {
45 token,
46 user_login,
47 scopes,
48 obtained_at,
49 }
50 }
51}
52
53#[derive(Debug, Default, Serialize, Deserialize)]
54struct Store {
55 #[serde(default)]
56 registries: BTreeMap<String, Entry>,
57}
58
59pub fn credentials_path() -> Result<PathBuf> {
63 let base = dirs::config_dir()
64 .context("no config directory resolvable on this platform (set $XDG_CONFIG_HOME)")?;
65 Ok(base.join("memstead").join("credentials"))
66}
67
68fn resolved_path() -> Result<PathBuf> {
73 if let Ok(override_path) = std::env::var("MEMSTEAD_CREDENTIALS_FILE")
74 && !override_path.is_empty()
75 {
76 return Ok(PathBuf::from(override_path));
77 }
78 credentials_path()
79}
80
81fn load_store() -> Result<Store> {
82 let path = resolved_path()?;
83 match std::fs::read_to_string(&path) {
84 Ok(s) => toml::from_str(&s)
85 .with_context(|| format!("parsing credentials file at {}", path.display())),
86 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Store::default()),
87 Err(e) => Err(e).with_context(|| format!("reading credentials file at {}", path.display())),
88 }
89}
90
91fn save_store(store: &Store) -> Result<()> {
92 let path = resolved_path()?;
93 if let Some(parent) = path.parent() {
94 std::fs::create_dir_all(parent)
95 .with_context(|| format!("creating credentials dir at {}", parent.display()))?;
96 }
97 let body = toml::to_string(store).context("serializing credentials TOML")?;
98 std::fs::write(&path, body)
99 .with_context(|| format!("writing credentials file at {}", path.display()))?;
100 tighten_permissions(&path)?;
101 Ok(())
102}
103
104#[cfg(unix)]
105fn tighten_permissions(path: &std::path::Path) -> Result<()> {
106 use std::os::unix::fs::PermissionsExt;
107 let perms = std::fs::Permissions::from_mode(0o600);
108 std::fs::set_permissions(path, perms)
109 .with_context(|| format!("setting mode 0600 on {}", path.display()))?;
110 Ok(())
111}
112
113#[cfg(not(unix))]
114fn tighten_permissions(_: &std::path::Path) -> Result<()> {
115 Ok(())
116}
117
118pub fn load_for(host: &str) -> Result<Option<Entry>> {
121 let store = load_store()?;
122 Ok(store.registries.get(&host.to_ascii_lowercase()).cloned())
123}
124
125pub fn save_for(host: &str, entry: Entry) -> Result<()> {
128 let mut store = load_store()?;
129 store.registries.insert(host.to_ascii_lowercase(), entry);
130 save_store(&store)
131}
132
133pub fn remove_for(host: &str) -> Result<bool> {
136 let mut store = load_store()?;
137 let removed = store
138 .registries
139 .remove(&host.to_ascii_lowercase())
140 .is_some();
141 if removed {
142 save_store(&store)?;
143 }
144 Ok(removed)
145}