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 #[allow(dead_code)]
40 pub fn with_path(path: PathBuf) -> Self {
41 Self { credentials_path: path }
42 }
43
44 pub fn default_credentials_path() -> PathBuf {
46 dirs::home_dir()
47 .unwrap_or_else(|| PathBuf::from("."))
48 .join(".mecha10")
49 .join("credentials.json")
50 }
51
52 pub fn credentials_path(&self) -> &PathBuf {
54 &self.credentials_path
55 }
56
57 pub fn load(&self) -> Result<Option<Credentials>> {
61 if !self.credentials_path.exists() {
62 return Ok(None);
63 }
64
65 let content = std::fs::read_to_string(&self.credentials_path)
66 .with_context(|| format!("Failed to read credentials from {}", self.credentials_path.display()))?;
67
68 let credentials: Credentials = serde_json::from_str(&content).map_err(|e| AuthError::InvalidCredentials {
69 message: format!("Failed to parse credentials: {}", e),
70 })?;
71
72 Ok(Some(credentials))
73 }
74
75 pub fn save(&self, credentials: &Credentials) -> Result<()> {
79 if let Some(parent) = self.credentials_path.parent() {
81 std::fs::create_dir_all(parent)
82 .with_context(|| format!("Failed to create directory {}", parent.display()))?;
83 }
84
85 let content = serde_json::to_string_pretty(credentials).with_context(|| "Failed to serialize credentials")?;
86
87 std::fs::write(&self.credentials_path, content)
88 .with_context(|| format!("Failed to write credentials to {}", self.credentials_path.display()))?;
89
90 #[cfg(unix)]
92 {
93 use std::os::unix::fs::PermissionsExt;
94 let permissions = std::fs::Permissions::from_mode(0o600);
95 std::fs::set_permissions(&self.credentials_path, permissions)
96 .with_context(|| "Failed to set credentials file permissions")?;
97 }
98
99 Ok(())
100 }
101
102 pub fn delete(&self) -> Result<()> {
104 if self.credentials_path.exists() {
105 std::fs::remove_file(&self.credentials_path)
106 .with_context(|| format!("Failed to delete credentials from {}", self.credentials_path.display()))?;
107 }
108 Ok(())
109 }
110
111 pub fn get_api_key(&self) -> Result<Option<String>> {
115 match self.load()? {
116 Some(creds) if creds.is_valid() => Ok(Some(creds.api_key)),
117 _ => Ok(None),
118 }
119 }
120
121 pub fn is_logged_in(&self) -> bool {
123 self.load()
124 .map(|creds| creds.map(|c| c.is_valid()).unwrap_or(false))
125 .unwrap_or(false)
126 }
127
128 pub fn get_user_info(&self) -> Result<Option<(String, String, Option<String>)>> {
130 match self.load()? {
131 Some(creds) if creds.is_valid() => Ok(Some((creds.user_id, creds.email, creds.name))),
132 _ => Ok(None),
133 }
134 }
135}
136
137impl Default for CredentialsService {
138 fn default() -> Self {
139 Self::new()
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use chrono::Utc;
147 use tempfile::TempDir;
148
149 fn create_test_credentials() -> Credentials {
150 Credentials {
151 api_key: "mecha_test123abc456def".to_string(),
152 user_id: "usr_test123".to_string(),
153 email: "test@example.com".to_string(),
154 name: Some("Test User".to_string()),
155 authenticated_at: Utc::now(),
156 auth_url: DEFAULT_AUTH_URL.to_string(),
157 }
158 }
159
160 #[test]
161 fn test_save_and_load_credentials() {
162 let temp_dir = TempDir::new().unwrap();
163 let creds_path = temp_dir.path().join("credentials.json");
164
165 let service = CredentialsService::with_path(creds_path);
166 let creds = create_test_credentials();
167
168 service.save(&creds).unwrap();
170
171 let loaded = service.load().unwrap().unwrap();
173 assert_eq!(loaded.api_key, creds.api_key);
174 assert_eq!(loaded.user_id, creds.user_id);
175 assert_eq!(loaded.email, creds.email);
176 }
177
178 #[test]
179 fn test_delete_credentials() {
180 let temp_dir = TempDir::new().unwrap();
181 let creds_path = temp_dir.path().join("credentials.json");
182
183 let service = CredentialsService::with_path(creds_path.clone());
184 let creds = create_test_credentials();
185
186 service.save(&creds).unwrap();
188 assert!(creds_path.exists());
189
190 service.delete().unwrap();
191 assert!(!creds_path.exists());
192
193 let loaded = service.load().unwrap();
195 assert!(loaded.is_none());
196 }
197
198 #[test]
199 fn test_get_api_key() {
200 let temp_dir = TempDir::new().unwrap();
201 let creds_path = temp_dir.path().join("credentials.json");
202
203 let service = CredentialsService::with_path(creds_path);
204 let creds = create_test_credentials();
205
206 assert!(service.get_api_key().unwrap().is_none());
208
209 service.save(&creds).unwrap();
211 assert_eq!(service.get_api_key().unwrap().unwrap(), creds.api_key);
212 }
213
214 #[test]
215 fn test_is_logged_in() {
216 let temp_dir = TempDir::new().unwrap();
217 let creds_path = temp_dir.path().join("credentials.json");
218
219 let service = CredentialsService::with_path(creds_path);
220 let creds = create_test_credentials();
221
222 assert!(!service.is_logged_in());
224
225 service.save(&creds).unwrap();
227 assert!(service.is_logged_in());
228 }
229
230 #[test]
231 fn test_credentials_validity() {
232 let valid = Credentials {
233 api_key: "mecha_valid123".to_string(),
234 user_id: "usr_test".to_string(),
235 email: "test@example.com".to_string(),
236 name: None,
237 authenticated_at: Utc::now(),
238 auth_url: DEFAULT_AUTH_URL.to_string(),
239 };
240 assert!(valid.is_valid());
241
242 let invalid_prefix = Credentials {
243 api_key: "invalid_key".to_string(),
244 ..valid.clone()
245 };
246 assert!(!invalid_prefix.is_valid());
247
248 let empty_key = Credentials {
249 api_key: "".to_string(),
250 ..valid.clone()
251 };
252 assert!(!empty_key.is_valid());
253 }
254}