mecha10_cli/services/
credentials.rs1use crate::types::credentials::{AuthError, Credentials};
6use anyhow::{Context, Result};
7use std::path::PathBuf;
8
9pub const DEFAULT_AUTH_URL: &str = "https://api.mecha.industries/auth";
11
12pub const AUTH_URL_ENV_VAR: &str = "MECHA10_AUTH_URL";
14
15pub fn get_auth_url() -> String {
21 std::env::var(AUTH_URL_ENV_VAR).unwrap_or_else(|_| DEFAULT_AUTH_URL.to_string())
22}
23
24pub struct CredentialsService {
26 credentials_path: PathBuf,
28}
29
30impl CredentialsService {
31 pub fn new() -> Self {
33 Self {
34 credentials_path: Self::default_credentials_path(),
35 }
36 }
37
38 pub fn with_path(path: PathBuf) -> Self {
40 Self { credentials_path: path }
41 }
42
43 pub fn default_credentials_path() -> PathBuf {
45 dirs::home_dir()
46 .unwrap_or_else(|| PathBuf::from("."))
47 .join(".mecha10")
48 .join("credentials.json")
49 }
50
51 pub fn credentials_path(&self) -> &PathBuf {
53 &self.credentials_path
54 }
55
56 pub fn load(&self) -> Result<Option<Credentials>> {
60 if !self.credentials_path.exists() {
61 return Ok(None);
62 }
63
64 let content = std::fs::read_to_string(&self.credentials_path)
65 .with_context(|| format!("Failed to read credentials from {}", self.credentials_path.display()))?;
66
67 let credentials: Credentials = serde_json::from_str(&content).map_err(|e| AuthError::InvalidCredentials {
68 message: format!("Failed to parse credentials: {}", e),
69 })?;
70
71 Ok(Some(credentials))
72 }
73
74 pub fn save(&self, credentials: &Credentials) -> Result<()> {
78 if let Some(parent) = self.credentials_path.parent() {
80 std::fs::create_dir_all(parent)
81 .with_context(|| format!("Failed to create directory {}", parent.display()))?;
82 }
83
84 let content = serde_json::to_string_pretty(credentials).with_context(|| "Failed to serialize credentials")?;
85
86 std::fs::write(&self.credentials_path, content)
87 .with_context(|| format!("Failed to write credentials to {}", self.credentials_path.display()))?;
88
89 #[cfg(unix)]
91 {
92 use std::os::unix::fs::PermissionsExt;
93 let permissions = std::fs::Permissions::from_mode(0o600);
94 std::fs::set_permissions(&self.credentials_path, permissions)
95 .with_context(|| "Failed to set credentials file permissions")?;
96 }
97
98 Ok(())
99 }
100
101 pub fn delete(&self) -> Result<()> {
103 if self.credentials_path.exists() {
104 std::fs::remove_file(&self.credentials_path)
105 .with_context(|| format!("Failed to delete credentials from {}", self.credentials_path.display()))?;
106 }
107 Ok(())
108 }
109
110 pub fn get_api_key(&self) -> Result<Option<String>> {
114 match self.load()? {
115 Some(creds) if creds.is_valid() => Ok(Some(creds.api_key)),
116 _ => Ok(None),
117 }
118 }
119
120 pub fn is_logged_in(&self) -> bool {
122 self.load()
123 .map(|creds| creds.map(|c| c.is_valid()).unwrap_or(false))
124 .unwrap_or(false)
125 }
126
127 pub fn get_user_info(&self) -> Result<Option<(String, String, Option<String>)>> {
129 match self.load()? {
130 Some(creds) if creds.is_valid() => Ok(Some((creds.user_id, creds.email, creds.name))),
131 _ => Ok(None),
132 }
133 }
134}
135
136impl Default for CredentialsService {
137 fn default() -> Self {
138 Self::new()
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use chrono::Utc;
146 use tempfile::TempDir;
147
148 fn create_test_credentials() -> Credentials {
149 Credentials {
150 api_key: "mecha_test123abc456def".to_string(),
151 user_id: "usr_test123".to_string(),
152 email: "test@example.com".to_string(),
153 name: Some("Test User".to_string()),
154 authenticated_at: Utc::now(),
155 auth_url: DEFAULT_AUTH_URL.to_string(),
156 }
157 }
158
159 #[test]
160 fn test_save_and_load_credentials() {
161 let temp_dir = TempDir::new().unwrap();
162 let creds_path = temp_dir.path().join("credentials.json");
163
164 let service = CredentialsService::with_path(creds_path);
165 let creds = create_test_credentials();
166
167 service.save(&creds).unwrap();
169
170 let loaded = service.load().unwrap().unwrap();
172 assert_eq!(loaded.api_key, creds.api_key);
173 assert_eq!(loaded.user_id, creds.user_id);
174 assert_eq!(loaded.email, creds.email);
175 }
176
177 #[test]
178 fn test_delete_credentials() {
179 let temp_dir = TempDir::new().unwrap();
180 let creds_path = temp_dir.path().join("credentials.json");
181
182 let service = CredentialsService::with_path(creds_path.clone());
183 let creds = create_test_credentials();
184
185 service.save(&creds).unwrap();
187 assert!(creds_path.exists());
188
189 service.delete().unwrap();
190 assert!(!creds_path.exists());
191
192 let loaded = service.load().unwrap();
194 assert!(loaded.is_none());
195 }
196
197 #[test]
198 fn test_get_api_key() {
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.get_api_key().unwrap().is_none());
207
208 service.save(&creds).unwrap();
210 assert_eq!(service.get_api_key().unwrap().unwrap(), creds.api_key);
211 }
212
213 #[test]
214 fn test_is_logged_in() {
215 let temp_dir = TempDir::new().unwrap();
216 let creds_path = temp_dir.path().join("credentials.json");
217
218 let service = CredentialsService::with_path(creds_path);
219 let creds = create_test_credentials();
220
221 assert!(!service.is_logged_in());
223
224 service.save(&creds).unwrap();
226 assert!(service.is_logged_in());
227 }
228
229 #[test]
230 fn test_credentials_validity() {
231 let valid = Credentials {
232 api_key: "mecha_valid123".to_string(),
233 user_id: "usr_test".to_string(),
234 email: "test@example.com".to_string(),
235 name: None,
236 authenticated_at: Utc::now(),
237 auth_url: DEFAULT_AUTH_URL.to_string(),
238 };
239 assert!(valid.is_valid());
240
241 let invalid_prefix = Credentials {
242 api_key: "invalid_key".to_string(),
243 ..valid.clone()
244 };
245 assert!(!invalid_prefix.is_valid());
246
247 let empty_key = Credentials {
248 api_key: "".to_string(),
249 ..valid.clone()
250 };
251 assert!(!empty_key.is_valid());
252 }
253}