1use crate::encrypted_storage::{
12 EncryptedStorageManager, PasskeyInfo, RecentIdentity, Session, VaultInfo,
13};
14use anyhow::{Result, anyhow};
15use serde::{Deserialize, Serialize};
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct SessionInfo {
20 pub session_id: String,
21 pub four_words: String,
22 pub display_name: String,
23}
24
25impl From<Session> for SessionInfo {
26 fn from(session: Session) -> Self {
27 Self {
28 session_id: session.id,
29 four_words: session.four_words,
30 display_name: session.display_name,
31 }
32 }
33}
34
35pub struct AuthService {
37 storage_manager: EncryptedStorageManager,
38 active_session: Option<Session>,
39}
40
41impl AuthService {
42 pub fn new(storage_manager: EncryptedStorageManager) -> Self {
44 Self {
45 storage_manager,
46 active_session: None,
47 }
48 }
49
50 pub fn storage_manager(&self) -> &EncryptedStorageManager {
52 &self.storage_manager
53 }
54
55 pub fn storage_manager_mut(&mut self) -> &mut EncryptedStorageManager {
57 &mut self.storage_manager
58 }
59
60 pub async fn create_vault(
65 &mut self,
66 four_words: &str,
67 password: &str,
68 display_name: &str,
69 ) -> Result<String> {
70 tracing::info!("AuthService: Creating vault for {}", four_words);
71
72 let vault_id = self
73 .storage_manager
74 .create_vault(four_words, password, display_name)
75 .await?;
76
77 tracing::info!("AuthService: Vault created with ID: {}", vault_id);
78 Ok(vault_id)
79 }
80
81 pub async fn login(
85 &mut self,
86 four_words: &str,
87 password: &str,
88 _device_name: Option<&str>,
89 ) -> Result<SessionInfo> {
90 tracing::info!("AuthService: Login attempt for {}", four_words);
91
92 let session = self
94 .storage_manager
95 .login(four_words, password, None)
96 .await?;
97
98 let session_info = SessionInfo::from(session.clone());
99 self.active_session = Some(session);
100
101 tracing::info!("AuthService: Login successful for {}", four_words);
102 Ok(session_info)
103 }
104
105 pub async fn logout(&mut self) -> Result<()> {
107 if let Some(session) = &self.active_session {
108 tracing::info!("AuthService: Logging out {}", session.four_words);
109 self.storage_manager.logout(&session.id).await?;
110 self.active_session = None;
111 tracing::info!("AuthService: Logout successful");
112 Ok(())
113 } else {
114 Err(anyhow!("No active session to logout"))
115 }
116 }
117
118 pub fn get_current_session(&self) -> Option<SessionInfo> {
120 self.active_session.as_ref().map(|s| SessionInfo {
121 session_id: s.id.clone(),
122 four_words: s.four_words.clone(),
123 display_name: s.display_name.clone(),
124 })
125 }
126
127 pub fn is_logged_in(&self) -> bool {
129 self.active_session.is_some()
130 }
131
132 pub async fn list_vaults(&self) -> Result<Vec<VaultInfo>> {
134 self.storage_manager.list_vaults().await
135 }
136
137 pub async fn get_recent_identities(&self) -> Result<Vec<RecentIdentity>> {
139 Ok(self.storage_manager.get_recent_identities().await)
141 }
142
143 pub async fn remove_recent_identity(&mut self, four_words: &str) -> Result<()> {
145 self.storage_manager
146 .remove_recent_identity(four_words)
147 .await
148 }
149
150 pub async fn vault_exists(&self, four_words: &str) -> Result<bool> {
152 self.storage_manager.vault_exists(four_words).await
153 }
154
155 pub async fn delete_vault(&mut self, four_words: &str, password: &str) -> Result<()> {
157 tracing::warn!("AuthService: Deleting vault for {}", four_words);
158
159 let _ = self.login(four_words, password, None).await?;
161
162 self.storage_manager.delete_vault(four_words).await?;
163
164 if let Some(session) = &self.active_session
166 && session.four_words == four_words
167 {
168 self.active_session = None;
169 }
170
171 tracing::warn!("AuthService: Vault deleted for {}", four_words);
172 Ok(())
173 }
174
175 pub async fn passkey_register(
184 &mut self,
185 four_words: &str,
186 device_name: &str,
187 ) -> Result<PasskeyInfo> {
188 tracing::info!(
189 "AuthService: Registering passkey for {} on {}",
190 four_words,
191 device_name
192 );
193
194 let info = self
195 .storage_manager
196 .passkey_register(four_words, device_name)
197 .await?;
198
199 tracing::info!("AuthService: Passkey registered successfully");
200 Ok(info)
201 }
202
203 pub async fn passkey_register_webauthn(
207 &mut self,
208 four_words: &str,
209 device_name: &str,
210 credential: crate::encrypted_storage::passkey::WebAuthnCredential,
211 ) -> Result<PasskeyInfo> {
212 tracing::info!(
213 "AuthService: Registering WebAuthn passkey for {} on {}",
214 four_words,
215 device_name
216 );
217
218 let info = self
219 .storage_manager
220 .passkey_register_webauthn(four_words, device_name, credential)
221 .await?;
222
223 tracing::info!("AuthService: WebAuthn passkey registered successfully");
224 Ok(info)
225 }
226
227 pub async fn passkey_authenticate(&mut self, four_words: &str) -> Result<SessionInfo> {
231 tracing::info!("AuthService: Passkey authentication for {}", four_words);
232
233 let session = self
234 .storage_manager
235 .passkey_authenticate(four_words)
236 .await?;
237
238 let session_info = SessionInfo::from(session.clone());
239 self.active_session = Some(session);
240
241 tracing::info!("AuthService: Passkey authentication successful");
242 Ok(session_info)
243 }
244
245 pub async fn passkey_has_passkey(&self, four_words: &str) -> Result<bool> {
247 Ok(self.storage_manager.passkey_has_passkey(four_words).await)
248 }
249
250 pub async fn passkey_get_info(&self, four_words: &str) -> Result<PasskeyInfo> {
252 self.storage_manager.passkey_get_info(four_words).await
253 }
254
255 pub async fn passkey_delete(&mut self, four_words: &str) -> Result<()> {
257 tracing::warn!("AuthService: Deleting passkey for {}", four_words);
258 self.storage_manager.passkey_delete(four_words).await?;
259 tracing::warn!("AuthService: Passkey deleted");
260 Ok(())
261 }
262
263 pub async fn try_auto_login(&mut self) -> Result<Option<SessionInfo>> {
271 tracing::info!("AuthService: Attempting auto-login");
272
273 let recent = self.storage_manager.get_recent_identities().await;
275
276 if recent.is_empty() {
277 tracing::info!("AuthService: No recent identities for auto-login");
278 return Ok(None);
279 }
280
281 let last_identity = &recent[0];
282 tracing::info!(
283 "AuthService: Attempting auto-login for {}",
284 last_identity.four_words
285 );
286
287 if last_identity.has_passkey {
289 match self.passkey_authenticate(&last_identity.four_words).await {
290 Ok(session_info) => {
291 tracing::info!("AuthService: Auto-login successful via passkey");
292 return Ok(Some(session_info));
293 }
294 Err(e) => {
295 tracing::warn!("AuthService: Passkey auto-login failed: {}", e);
296 }
298 }
299 }
300
301 tracing::info!("AuthService: No auto-login available");
302 Ok(None)
303 }
304
305 pub async fn enable_auto_login(&mut self, password: &str) -> Result<()> {
309 let session = self
310 .active_session
311 .as_ref()
312 .ok_or_else(|| anyhow!("No active session"))?;
313
314 tracing::info!(
315 "AuthService: Enabling auto-login for {}",
316 session.four_words
317 );
318
319 self.storage_manager
321 .store_password_in_keyring(&session.four_words, password)
322 .await?;
323
324 tracing::info!("AuthService: Auto-login enabled");
325 Ok(())
326 }
327
328 pub async fn disable_auto_login(&mut self, four_words: &str) -> Result<()> {
330 tracing::info!("AuthService: Disabling auto-login for {}", four_words);
331
332 self.storage_manager
334 .remove_password_from_keyring(four_words)
335 .await?;
336
337 if self.passkey_has_passkey(four_words).await? {
339 self.passkey_delete(four_words).await?;
340 }
341
342 tracing::info!("AuthService: Auto-login disabled");
343 Ok(())
344 }
345
346 pub async fn switch_identity(&mut self, four_words: &str) -> Result<SessionInfo> {
352 tracing::info!("AuthService: Switching to identity {}", four_words);
353
354 if self.active_session.is_some() {
356 self.logout().await.ok(); }
358
359 if self.passkey_has_passkey(four_words).await? {
361 match self.passkey_authenticate(four_words).await {
362 Ok(session_info) => {
363 tracing::info!("AuthService: Identity switch successful via passkey");
364 return Ok(session_info);
365 }
366 Err(e) => {
367 tracing::warn!("AuthService: Passkey switch failed: {}", e);
368 return Err(anyhow!("Passkey authentication required but failed"));
369 }
370 }
371 }
372
373 Err(anyhow!(
374 "Cannot switch to identity without password or passkey"
375 ))
376 }
377}
378
379#[cfg(test)]
380mod tests {
381 use super::*;
382 use crate::encrypted_storage::StorageConfig;
383 use tempfile::TempDir;
384
385 #[tokio::test]
386 async fn test_auth_service_basic_flow() {
387 let temp_dir = TempDir::new().expect("Failed to create temp dir");
388 let config = StorageConfig {
389 vault_dir: temp_dir.path().to_path_buf(),
390 use_keyring: false,
391 ..Default::default()
392 };
393
394 let storage_manager = EncryptedStorageManager::new(config)
395 .await
396 .expect("Failed to create storage manager");
397
398 let mut auth_service = AuthService::new(storage_manager);
399
400 let vault_id = auth_service
402 .create_vault("ocean-forest-moon-star", "test-password", "Test User")
403 .await
404 .expect("Failed to create vault");
405
406 assert!(!vault_id.is_empty());
407
408 let session_info = auth_service
410 .login(
411 "ocean-forest-moon-star",
412 "test-password",
413 Some("Test Device"),
414 )
415 .await
416 .expect("Failed to login");
417
418 assert_eq!(session_info.four_words, "ocean-forest-moon-star");
419 assert_eq!(session_info.display_name, "Test User");
420 assert!(auth_service.is_logged_in());
421
422 let current = auth_service.get_current_session();
424 assert!(current.is_some());
425 assert_eq!(current.unwrap().four_words, "ocean-forest-moon-star");
426
427 auth_service.logout().await.expect("Failed to logout");
429 assert!(!auth_service.is_logged_in());
430 }
431
432 #[tokio::test]
433 async fn test_auth_service_recent_identities() {
434 let temp_dir = TempDir::new().expect("Failed to create temp dir");
435 let vault_subdir = temp_dir.path().join("vaults");
436 std::fs::create_dir_all(&vault_subdir).expect("Failed to create vault subdir");
437 let config = StorageConfig {
438 vault_dir: vault_subdir,
439 use_keyring: false,
440 ..Default::default()
441 };
442
443 let storage_manager = EncryptedStorageManager::new(config)
444 .await
445 .expect("Failed to create storage manager");
446
447 let mut auth_service = AuthService::new(storage_manager);
448
449 auth_service
451 .create_vault("ocean-forest-moon-star", "pass1", "User 1")
452 .await
453 .expect("Failed to create vault 1");
454
455 auth_service
456 .login("ocean-forest-moon-star", "pass1", Some("Device 1"))
457 .await
458 .expect("Failed to login 1");
459
460 auth_service.logout().await.expect("Failed to logout 1");
461
462 auth_service
464 .create_vault("river-cloud-stone-tree", "pass2", "User 2")
465 .await
466 .expect("Failed to create vault 2");
467
468 auth_service
469 .login("river-cloud-stone-tree", "pass2", Some("Device 2"))
470 .await
471 .expect("Failed to login 2");
472
473 let recent = auth_service
475 .get_recent_identities()
476 .await
477 .expect("Failed to get recent");
478
479 assert_eq!(recent.len(), 2);
480 assert_eq!(recent[0].four_words, "river-cloud-stone-tree");
482 assert_eq!(recent[1].four_words, "ocean-forest-moon-star");
483 }
484}