use crate::types::{AuthError, Credentials};
use anyhow::{Context, Result};
use mecha10_core::fs_utils::home_subpath;
use std::path::PathBuf;
pub fn default_credentials_path() -> PathBuf {
home_subpath(&[".mecha10", "credentials.json"])
}
pub struct CredentialsService {
credentials_path: PathBuf,
}
impl CredentialsService {
pub fn new() -> Self {
Self {
credentials_path: default_credentials_path(),
}
}
pub fn with_path(path: PathBuf) -> Self {
Self { credentials_path: path }
}
pub fn credentials_path(&self) -> &PathBuf {
&self.credentials_path
}
pub fn load(&self) -> Result<Option<Credentials>> {
if !self.credentials_path.exists() {
return Ok(None);
}
let content = std::fs::read_to_string(&self.credentials_path)
.with_context(|| format!("Failed to read credentials from {}", self.credentials_path.display()))?;
let credentials: Credentials = serde_json::from_str(&content).map_err(|e| AuthError::InvalidCredentials {
message: format!("Failed to parse credentials: {}", e),
})?;
Ok(Some(credentials))
}
pub fn save(&self, credentials: &Credentials) -> Result<()> {
if let Some(parent) = self.credentials_path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("Failed to create directory {}", parent.display()))?;
}
let content = serde_json::to_string_pretty(credentials).with_context(|| "Failed to serialize credentials")?;
std::fs::write(&self.credentials_path, content)
.with_context(|| format!("Failed to write credentials to {}", self.credentials_path.display()))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let permissions = std::fs::Permissions::from_mode(0o600);
std::fs::set_permissions(&self.credentials_path, permissions)
.with_context(|| "Failed to set credentials file permissions")?;
}
Ok(())
}
pub fn delete(&self) -> Result<()> {
if self.credentials_path.exists() {
std::fs::remove_file(&self.credentials_path)
.with_context(|| format!("Failed to delete credentials from {}", self.credentials_path.display()))?;
}
Ok(())
}
pub fn get_api_key(&self) -> Result<Option<String>> {
match self.load()? {
Some(creds) if creds.is_valid() => Ok(Some(creds.api_key)),
_ => Ok(None),
}
}
pub fn is_logged_in(&self) -> bool {
self.load()
.map(|creds| creds.map(|c| c.is_valid()).unwrap_or(false))
.unwrap_or(false)
}
pub fn get_user_info(&self) -> Result<Option<(String, String, Option<String>)>> {
match self.load()? {
Some(creds) if creds.is_valid() => Ok(Some((creds.user_id, creds.email, creds.name))),
_ => Ok(None),
}
}
}
impl Default for CredentialsService {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::DEFAULT_AUTH_URL;
use chrono::Utc;
use tempfile::TempDir;
fn create_test_credentials() -> Credentials {
Credentials {
api_key: "mecha_test123abc456def".to_string(),
user_id: "usr_test123".to_string(),
email: "test@example.com".to_string(),
name: Some("Test User".to_string()),
authenticated_at: Utc::now(),
auth_url: DEFAULT_AUTH_URL.to_string(),
}
}
#[test]
fn test_save_and_load_credentials() {
let temp_dir = TempDir::new().unwrap();
let creds_path = temp_dir.path().join("credentials.json");
let service = CredentialsService::with_path(creds_path);
let creds = create_test_credentials();
service.save(&creds).unwrap();
let loaded = service.load().unwrap().unwrap();
assert_eq!(loaded.api_key, creds.api_key);
assert_eq!(loaded.user_id, creds.user_id);
assert_eq!(loaded.email, creds.email);
}
#[test]
fn test_delete_credentials() {
let temp_dir = TempDir::new().unwrap();
let creds_path = temp_dir.path().join("credentials.json");
let service = CredentialsService::with_path(creds_path.clone());
let creds = create_test_credentials();
service.save(&creds).unwrap();
assert!(creds_path.exists());
service.delete().unwrap();
assert!(!creds_path.exists());
let loaded = service.load().unwrap();
assert!(loaded.is_none());
}
#[test]
fn test_get_api_key() {
let temp_dir = TempDir::new().unwrap();
let creds_path = temp_dir.path().join("credentials.json");
let service = CredentialsService::with_path(creds_path);
let creds = create_test_credentials();
assert!(service.get_api_key().unwrap().is_none());
service.save(&creds).unwrap();
assert_eq!(service.get_api_key().unwrap().unwrap(), creds.api_key);
}
#[test]
fn test_is_logged_in() {
let temp_dir = TempDir::new().unwrap();
let creds_path = temp_dir.path().join("credentials.json");
let service = CredentialsService::with_path(creds_path);
let creds = create_test_credentials();
assert!(!service.is_logged_in());
service.save(&creds).unwrap();
assert!(service.is_logged_in());
}
#[test]
fn test_credentials_validity() {
let valid = Credentials {
api_key: "mecha_valid123".to_string(),
user_id: "usr_test".to_string(),
email: "test@example.com".to_string(),
name: None,
authenticated_at: Utc::now(),
auth_url: DEFAULT_AUTH_URL.to_string(),
};
assert!(valid.is_valid());
let invalid_prefix = Credentials {
api_key: "invalid_key".to_string(),
..valid.clone()
};
assert!(!invalid_prefix.is_valid());
let empty_key = Credentials {
api_key: "".to_string(),
..valid.clone()
};
assert!(!empty_key.is_valid());
}
}