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