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