use std::fs;
use std::path::{Path, PathBuf};
use anyhow::{anyhow, Context, Result};
use serde::{Deserialize, Serialize};
const FILE_NAME: &str = "credentials.json";
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CredentialsFile {
pub token: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub api_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
}
pub fn config_dir() -> Result<PathBuf> {
if let Ok(dir) = std::env::var("SYLPHX_CONFIG_DIR") {
let p = PathBuf::from(dir);
if !p.as_os_str().is_empty() {
return Ok(p);
}
}
if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") {
if !xdg.is_empty() {
return Ok(PathBuf::from(xdg).join("sylphx"));
}
}
let home = dirs::home_dir().ok_or_else(|| anyhow!("cannot resolve home directory"))?;
Ok(home.join(".config").join("sylphx"))
}
pub fn credentials_path() -> Result<PathBuf> {
Ok(config_dir()?.join(FILE_NAME))
}
pub fn load() -> Result<Option<CredentialsFile>> {
let path = credentials_path()?;
if !path.exists() {
return Ok(None);
}
let raw = fs::read_to_string(&path)
.with_context(|| format!("read credentials {}", path.display()))?;
let parsed: CredentialsFile = serde_json::from_str(&raw)
.with_context(|| format!("parse credentials {}", path.display()))?;
if parsed.token.trim().is_empty() {
return Ok(None);
}
Ok(Some(parsed))
}
pub fn save(creds: &CredentialsFile) -> Result<PathBuf> {
let dir = config_dir()?;
fs::create_dir_all(&dir).with_context(|| format!("create config dir {}", dir.display()))?;
let path = dir.join(FILE_NAME);
let body = serde_json::to_string_pretty(creds)?;
write_private(&path, body.as_bytes())?;
Ok(path)
}
pub fn clear() -> Result<bool> {
let path = credentials_path()?;
if path.exists() {
fs::remove_file(&path).with_context(|| format!("remove {}", path.display()))?;
return Ok(true);
}
Ok(false)
}
pub fn resolve_token(cli_token: Option<&str>) -> Result<String> {
if let Some(t) = cli_token.map(str::trim).filter(|t| !t.is_empty()) {
return Ok(t.to_string());
}
if let Ok(t) = std::env::var("SYLPHX_TOKEN") {
let t = t.trim().to_string();
if !t.is_empty() {
return Ok(t);
}
}
if let Some(c) = load()? {
return Ok(c.token);
}
Err(anyhow!(
"no Management token: run `sylphx login --token <svc_…>` or `sylphx login` (device flow), or set SYLPHX_TOKEN"
))
}
pub fn resolve_api_url(cli_url: Option<&str>, default_url: &str) -> String {
if let Some(u) = cli_url.map(str::trim).filter(|u| !u.is_empty()) {
return u.to_string();
}
if let Ok(u) = std::env::var("SYLPHX_API_URL") {
let u = u.trim().to_string();
if !u.is_empty() {
return u;
}
}
if let Ok(Some(c)) = load() {
if let Some(u) = c.api_url.filter(|u| !u.trim().is_empty()) {
return u;
}
}
default_url.to_string()
}
fn write_private(path: &Path, bytes: &[u8]) -> Result<()> {
use std::io::Write;
let mut f = fs::File::create(path).with_context(|| format!("create {}", path.display()))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = f.set_permissions(fs::Permissions::from_mode(0o600));
}
f.write_all(bytes)?;
f.sync_all()?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
static LOCK: Mutex<()> = Mutex::new(());
#[test]
fn save_load_clear_roundtrip() {
let _g = LOCK.lock().unwrap();
let dir = tempfile::tempdir().unwrap();
std::env::set_var("SYLPHX_CONFIG_DIR", dir.path());
let creds = CredentialsFile {
token: "svc_test_abc".into(),
api_url: Some("http://127.0.0.1:9/v1".into()),
source: Some("token".into()),
};
let path = save(&creds).unwrap();
assert!(path.exists());
let loaded = load().unwrap().unwrap();
assert_eq!(loaded, creds);
assert!(clear().unwrap());
assert!(load().unwrap().is_none());
std::env::remove_var("SYLPHX_CONFIG_DIR");
}
#[test]
fn resolve_token_prefers_cli_over_file() {
let _g = LOCK.lock().unwrap();
let dir = tempfile::tempdir().unwrap();
std::env::set_var("SYLPHX_CONFIG_DIR", dir.path());
std::env::remove_var("SYLPHX_TOKEN");
save(&CredentialsFile {
token: "svc_file".into(),
api_url: None,
source: Some("token".into()),
})
.unwrap();
assert_eq!(resolve_token(Some("svc_cli")).unwrap(), "svc_cli");
assert_eq!(resolve_token(None).unwrap(), "svc_file");
let _ = clear();
std::env::remove_var("SYLPHX_CONFIG_DIR");
}
}