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>> {
55 if let Ok(api_key) = std::env::var("MECHA10_API_KEY") {
56 if !api_key.is_empty() {
57 return Ok(Some(Credentials {
58 api_key,
59 user_id: "control-plane".to_string(),
60 email: "control-plane@mecha.industries".to_string(),
61 name: None,
62 authenticated_at: chrono::Utc::now(),
63 auth_url: crate::DEFAULT_AUTH_URL.to_string(),
64 }));
65 }
66 }
67
68 if !self.credentials_path.exists() {
69 return Ok(None);
70 }
71
72 let content = std::fs::read_to_string(&self.credentials_path)
73 .with_context(|| format!("Failed to read credentials from {}", self.credentials_path.display()))?;
74
75 let credentials: Credentials = serde_json::from_str(&content).map_err(|e| AuthError::InvalidCredentials {
76 message: format!("Failed to parse credentials: {}", e),
77 })?;
78
79 Ok(Some(credentials))
80 }
81
82 pub fn save(&self, credentials: &Credentials) -> Result<()> {
86 if let Some(parent) = self.credentials_path.parent() {
87 std::fs::create_dir_all(parent)
88 .with_context(|| format!("Failed to create directory {}", parent.display()))?;
89 }
90
91 let content = serde_json::to_string_pretty(credentials).with_context(|| "Failed to serialize credentials")?;
92
93 std::fs::write(&self.credentials_path, content)
94 .with_context(|| format!("Failed to write credentials to {}", self.credentials_path.display()))?;
95
96 #[cfg(unix)]
97 {
98 use std::os::unix::fs::PermissionsExt;
99 let permissions = std::fs::Permissions::from_mode(0o600);
100 std::fs::set_permissions(&self.credentials_path, permissions)
101 .with_context(|| "Failed to set credentials file permissions")?;
102 }
103
104 Ok(())
105 }
106
107 pub fn delete(&self) -> Result<()> {
109 if self.credentials_path.exists() {
110 std::fs::remove_file(&self.credentials_path)
111 .with_context(|| format!("Failed to delete credentials from {}", self.credentials_path.display()))?;
112 }
113 Ok(())
114 }
115
116 pub fn get_api_key(&self) -> Result<Option<String>> {
120 match self.load()? {
121 Some(creds) if creds.is_valid() => Ok(Some(creds.api_key)),
122 _ => Ok(None),
123 }
124 }
125
126 pub fn is_logged_in(&self) -> bool {
128 self.load()
129 .map(|creds| creds.map(|c| c.is_valid()).unwrap_or(false))
130 .unwrap_or(false)
131 }
132
133 pub fn get_user_info(&self) -> Result<Option<(String, String, Option<String>)>> {
135 match self.load()? {
136 Some(creds) if creds.is_valid() => Ok(Some((creds.user_id, creds.email, creds.name))),
137 _ => Ok(None),
138 }
139 }
140}
141
142impl Default for CredentialsService {
143 fn default() -> Self {
144 Self::new()
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 use crate::DEFAULT_AUTH_URL;
152 use chrono::Utc;
153 use serial_test::serial;
154 use tempfile::TempDir;
155
156 fn create_test_credentials() -> Credentials {
157 Credentials {
158 api_key: "mecha_test123abc456def".to_string(),
159 user_id: "usr_test123".to_string(),
160 email: "test@example.com".to_string(),
161 name: Some("Test User".to_string()),
162 authenticated_at: Utc::now(),
163 auth_url: DEFAULT_AUTH_URL.to_string(),
164 }
165 }
166
167 #[test]
168 #[serial]
169 fn test_save_and_load_credentials() {
170 let temp_dir = TempDir::new().unwrap();
171 let creds_path = temp_dir.path().join("credentials.json");
172
173 let service = CredentialsService::with_path(creds_path);
174 let creds = create_test_credentials();
175
176 service.save(&creds).unwrap();
177
178 let loaded = service.load().unwrap().unwrap();
179 assert_eq!(loaded.api_key, creds.api_key);
180 assert_eq!(loaded.user_id, creds.user_id);
181 assert_eq!(loaded.email, creds.email);
182 }
183
184 #[test]
185 #[serial]
186 fn test_delete_credentials() {
187 let temp_dir = TempDir::new().unwrap();
188 let creds_path = temp_dir.path().join("credentials.json");
189
190 let service = CredentialsService::with_path(creds_path.clone());
191 let creds = create_test_credentials();
192
193 service.save(&creds).unwrap();
194 assert!(creds_path.exists());
195
196 service.delete().unwrap();
197 assert!(!creds_path.exists());
198
199 let loaded = service.load().unwrap();
200 assert!(loaded.is_none());
201 }
202
203 #[test]
204 #[serial]
205 fn test_get_api_key() {
206 let temp_dir = TempDir::new().unwrap();
207 let creds_path = temp_dir.path().join("credentials.json");
208
209 let service = CredentialsService::with_path(creds_path);
210 let creds = create_test_credentials();
211
212 assert!(service.get_api_key().unwrap().is_none());
213
214 service.save(&creds).unwrap();
215 assert_eq!(service.get_api_key().unwrap().unwrap(), creds.api_key);
216 }
217
218 #[test]
219 #[serial]
220 fn get_api_key_prefers_mecha10_api_key_env_over_the_credentials_file() {
221 let temp_dir = TempDir::new().unwrap();
222 let creds_path = temp_dir.path().join("credentials.json");
223
224 let service = CredentialsService::with_path(creds_path);
225 service.save(&create_test_credentials()).unwrap();
226
227 std::env::set_var("MECHA10_API_KEY", "9d6d598ddb4ce0e4e48eef4dc3610af");
228 let result = service.get_api_key();
229 std::env::remove_var("MECHA10_API_KEY");
230
231 assert_eq!(result.unwrap().unwrap(), "9d6d598ddb4ce0e4e48eef4dc3610af");
232 }
233
234 #[test]
235 #[serial]
236 fn get_api_key_falls_back_to_the_credentials_file_when_env_is_unset() {
237 let temp_dir = TempDir::new().unwrap();
238 let creds_path = temp_dir.path().join("credentials.json");
239
240 let service = CredentialsService::with_path(creds_path);
241 let creds = create_test_credentials();
242 service.save(&creds).unwrap();
243
244 std::env::remove_var("MECHA10_API_KEY");
245
246 assert_eq!(service.get_api_key().unwrap().unwrap(), creds.api_key);
247 }
248
249 #[test]
250 #[serial]
251 fn test_is_logged_in() {
252 let temp_dir = TempDir::new().unwrap();
253 let creds_path = temp_dir.path().join("credentials.json");
254
255 let service = CredentialsService::with_path(creds_path);
256 let creds = create_test_credentials();
257
258 std::env::remove_var("MECHA10_API_KEY");
259 assert!(!service.is_logged_in());
260
261 service.save(&creds).unwrap();
262 assert!(service.is_logged_in());
263 }
264
265 #[test]
266 fn test_credentials_validity() {
267 let valid = Credentials {
268 api_key: "mecha_valid123".to_string(),
269 user_id: "usr_test".to_string(),
270 email: "test@example.com".to_string(),
271 name: None,
272 authenticated_at: Utc::now(),
273 auth_url: DEFAULT_AUTH_URL.to_string(),
274 };
275 assert!(valid.is_valid());
276
277 let non_mecha_prefixed = Credentials {
278 api_key: "9d6d598ddb4ce0e4e48eef4dc3610af".to_string(),
279 ..valid.clone()
280 };
281 assert!(non_mecha_prefixed.is_valid());
282
283 let empty_key = Credentials {
284 api_key: "".to_string(),
285 ..valid.clone()
286 };
287 assert!(!empty_key.is_valid());
288 }
289}