//! The site token, kept in Windows Credential Manager (one entry per site). use serde::{Deserialize, Serialize}; const SERVICE: &str = "ODIAN Launcher"; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct SiteToken { /// Engine session token: 24 h, and gone whenever the site's Engine API restarts. pub access_token: String, /// Unix seconds. pub expires_at: i64, pub username: String, /// The site MCP's launcher session: no expiry, ended only by Sign out, an admin, or /// the ODIAN user disappearing. It renews `access_token` without asking the operator. #[serde(default)] pub launcher_token: Option, } impl SiteToken { pub fn expired(&self) -> bool { let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .map(|d| d.as_secs() as i64) .unwrap_or(0); self.expires_at <= now } } fn entry(site_url: &str) -> Option { if site_url.is_empty() { return None; } keyring::Entry::new(SERVICE, site_url).ok() } pub fn load(site_url: &str) -> Option { let secret = entry(site_url)?.get_password().ok()?; serde_json::from_str(&secret).ok() } pub fn save(site_url: &str, token: &SiteToken) -> Result<(), String> { let entry = entry(site_url).ok_or("no DHP configured")?; let secret = serde_json::to_string(token).map_err(|e| e.to_string())?; entry.set_password(&secret).map_err(|e| format!("cannot store the sign-in in Credential Manager: {e}")) } pub fn clear(site_url: &str) { if let Some(entry) = entry(site_url) { let _ = entry.delete_credential(); } }