mecha10_auth/
credentials.rs1use crate::types::{AuthError, Credentials};
6use anyhow::{Context, Result};
7use std::path::PathBuf;
8
9pub fn default_credentials_path() -> PathBuf {
11 dirs::home_dir()
12 .unwrap_or_else(|| PathBuf::from("."))
13 .join(".mecha10")
14 .join("credentials.json")
15}
16
17pub struct CredentialsService {
19 credentials_path: PathBuf,
21}
22
23impl CredentialsService {
24 pub fn new() -> Self {
26 Self {
27 credentials_path: default_credentials_path(),
28 }
29 }
30
31 pub fn with_path(path: PathBuf) -> Self {
33 Self { credentials_path: path }
34 }
35
36 pub fn credentials_path(&self) -> &PathBuf {
38 &self.credentials_path
39 }
40
41 pub fn load(&self) -> Result<Option<Credentials>> {
45 if !self.credentials_path.exists() {
46 return Ok(None);
47 }
48
49 let content = std::fs::read_to_string(&self.credentials_path)
50 .with_context(|| format!("Failed to read credentials from {}", self.credentials_path.display()))?;
51
52 let credentials: Credentials = serde_json::from_str(&content).map_err(|e| AuthError::InvalidCredentials {
53 message: format!("Failed to parse credentials: {}", e),
54 })?;
55
56 Ok(Some(credentials))
57 }
58
59 pub fn save(&self, credentials: &Credentials) -> Result<()> {
63 if let Some(parent) = self.credentials_path.parent() {
65 std::fs::create_dir_all(parent)
66 .with_context(|| format!("Failed to create directory {}", parent.display()))?;
67 }
68
69 let content = serde_json::to_string_pretty(credentials).with_context(|| "Failed to serialize credentials")?;
70
71 std::fs::write(&self.credentials_path, content)
72 .with_context(|| format!("Failed to write credentials to {}", self.credentials_path.display()))?;
73
74 #[cfg(unix)]
76 {
77 use std::os::unix::fs::PermissionsExt;
78 let permissions = std::fs::Permissions::from_mode(0o600);
79 std::fs::set_permissions(&self.credentials_path, permissions)
80 .with_context(|| "Failed to set credentials file permissions")?;
81 }
82
83 Ok(())
84 }
85
86 pub fn delete(&self) -> Result<()> {
88 if self.credentials_path.exists() {
89 std::fs::remove_file(&self.credentials_path)
90 .with_context(|| format!("Failed to delete credentials from {}", self.credentials_path.display()))?;
91 }
92 Ok(())
93 }
94
95 pub fn get_api_key(&self) -> Result<Option<String>> {
99 match self.load()? {
100 Some(creds) if creds.is_valid() => Ok(Some(creds.api_key)),
101 _ => Ok(None),
102 }
103 }
104
105 pub fn is_logged_in(&self) -> bool {
107 self.load()
108 .map(|creds| creds.map(|c| c.is_valid()).unwrap_or(false))
109 .unwrap_or(false)
110 }
111
112 pub fn get_user_info(&self) -> Result<Option<(String, String, Option<String>)>> {
114 match self.load()? {
115 Some(creds) if creds.is_valid() => Ok(Some((creds.user_id, creds.email, creds.name))),
116 _ => Ok(None),
117 }
118 }
119}
120
121impl Default for CredentialsService {
122 fn default() -> Self {
123 Self::new()
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use crate::DEFAULT_AUTH_URL;
131 use chrono::Utc;
132 use tempfile::TempDir;
133
134 fn create_test_credentials() -> Credentials {
135 Credentials {
136 api_key: "mecha_test123abc456def".to_string(),
137 user_id: "usr_test123".to_string(),
138 email: "test@example.com".to_string(),
139 name: Some("Test User".to_string()),
140 authenticated_at: Utc::now(),
141 auth_url: DEFAULT_AUTH_URL.to_string(),
142 }
143 }
144
145 #[test]
146 fn test_save_and_load_credentials() {
147 let temp_dir = TempDir::new().unwrap();
148 let creds_path = temp_dir.path().join("credentials.json");
149
150 let service = CredentialsService::with_path(creds_path);
151 let creds = create_test_credentials();
152
153 service.save(&creds).unwrap();
155
156 let loaded = service.load().unwrap().unwrap();
158 assert_eq!(loaded.api_key, creds.api_key);
159 assert_eq!(loaded.user_id, creds.user_id);
160 assert_eq!(loaded.email, creds.email);
161 }
162
163 #[test]
164 fn test_delete_credentials() {
165 let temp_dir = TempDir::new().unwrap();
166 let creds_path = temp_dir.path().join("credentials.json");
167
168 let service = CredentialsService::with_path(creds_path.clone());
169 let creds = create_test_credentials();
170
171 service.save(&creds).unwrap();
173 assert!(creds_path.exists());
174
175 service.delete().unwrap();
176 assert!(!creds_path.exists());
177
178 let loaded = service.load().unwrap();
180 assert!(loaded.is_none());
181 }
182
183 #[test]
184 fn test_get_api_key() {
185 let temp_dir = TempDir::new().unwrap();
186 let creds_path = temp_dir.path().join("credentials.json");
187
188 let service = CredentialsService::with_path(creds_path);
189 let creds = create_test_credentials();
190
191 assert!(service.get_api_key().unwrap().is_none());
193
194 service.save(&creds).unwrap();
196 assert_eq!(service.get_api_key().unwrap().unwrap(), creds.api_key);
197 }
198
199 #[test]
200 fn test_is_logged_in() {
201 let temp_dir = TempDir::new().unwrap();
202 let creds_path = temp_dir.path().join("credentials.json");
203
204 let service = CredentialsService::with_path(creds_path);
205 let creds = create_test_credentials();
206
207 assert!(!service.is_logged_in());
209
210 service.save(&creds).unwrap();
212 assert!(service.is_logged_in());
213 }
214
215 #[test]
216 fn test_credentials_validity() {
217 let valid = Credentials {
218 api_key: "mecha_valid123".to_string(),
219 user_id: "usr_test".to_string(),
220 email: "test@example.com".to_string(),
221 name: None,
222 authenticated_at: Utc::now(),
223 auth_url: DEFAULT_AUTH_URL.to_string(),
224 };
225 assert!(valid.is_valid());
226
227 let invalid_prefix = Credentials {
228 api_key: "invalid_key".to_string(),
229 ..valid.clone()
230 };
231 assert!(!invalid_prefix.is_valid());
232
233 let empty_key = Credentials {
234 api_key: "".to_string(),
235 ..valid.clone()
236 };
237 assert!(!empty_key.is_valid());
238 }
239}