1use std::sync::Arc;
6use tauri::State;
7
8use crate::auth::{AuthManager, AuthResult, ServerInfo, Session, SessionVerifier};
9
10pub struct AuthManagerWrapper(pub Arc<AuthManager>);
12
13pub struct SessionVerifierWrapper(pub Arc<tokio::sync::Mutex<Option<SessionVerifier>>>);
15
16#[tauri::command]
19#[specta::specta]
20pub async fn auth_initialize(
21 auth_manager: State<'_, AuthManagerWrapper>,
22 database: State<'_, crate::commands::DatabaseWrapper>,
23 credentials: State<'_, crate::commands::CredentialStoreWrapper>,
24) -> Result<Option<Session>, String> {
25 if let Some(session) = auth_manager.0.get_session().await {
27 return Ok(Some(session));
28 }
29
30 log::info!("[AuthManager] Restoring session from storage...");
32
33 {
41 let db_service = {
42 let db = database.0.lock().map_err(|e| e.to_string())?;
43 std::sync::Arc::new(db.service())
44 };
45 let locked: Option<String> = crate::storage::db_service::DatabaseService::query_optional(
46 &*db_service,
47 crate::storage::db_service::Query::new(
48 "SELECT u.id FROM users u
49 JOIN user_pins p ON p.user_id = u.id
50 WHERE u.is_active = 1",
51 ),
52 |row| row.get(0),
53 )
54 .await
55 .unwrap_or(None);
56
57 if let Some(user_id) = locked {
58 log::info!(
59 "[AuthManager] Active profile {} is PIN-protected; not restoring its session",
60 user_id
61 );
62 return Ok(None);
63 }
64 }
65
66 let active_session =
68 match crate::commands::storage::storage_get_active_session(database, credentials).await {
69 Ok(Some(session)) => session,
70 Ok(None) => {
71 log::info!("[AuthManager] No active session in storage");
72 return Ok(None);
73 }
74 Err(e) => {
75 log::error!("[AuthManager] Failed to get active session: {}", e);
76 return Err(e);
77 }
78 };
79
80 let normalized_url = crate::auth::AuthManager::normalize_url(&active_session.server_url)?;
82
83 let session = Session {
84 user_id: active_session.user_id,
85 username: active_session.username,
86 server_id: active_session.server_id,
87 server_url: normalized_url,
88 server_name: active_session.server_name,
89 access_token: active_session.access_token,
90 verified: false, needs_reauth: false,
92 };
93
94 auth_manager.0.set_session(Some(session.clone())).await;
96
97 log::info!(
98 "[AuthManager] Session restored for user: {} with normalized URL: {}",
99 session.username,
100 session.server_url
101 );
102 Ok(Some(session))
103}
104
105#[tauri::command]
107#[specta::specta]
108pub async fn auth_connect_to_server(
109 server_url: String,
110 auth_manager: State<'_, AuthManagerWrapper>,
111) -> Result<ServerInfo, String> {
112 auth_manager.0.connect_to_server(&server_url).await
113}
114
115#[tauri::command]
117#[specta::specta]
118pub async fn auth_login(
119 server_url: String,
120 username: String,
121 password: String,
122 device_id: String,
123 auth_manager: State<'_, AuthManagerWrapper>,
124) -> Result<AuthResult, String> {
125 let result = auth_manager
126 .0
127 .login(&server_url, &username, &password, &device_id)
128 .await?;
129
130 let normalized_url = crate::auth::AuthManager::normalize_url(&server_url)?;
132
133 let session = Session {
134 user_id: result.user.id.clone(),
135 username: result.user.name.clone(),
136 server_id: result.server_id.clone(),
137 server_url: normalized_url,
138 server_name: String::new(), access_token: result.access_token.clone(),
140 verified: true,
141 needs_reauth: false,
142 };
143
144 auth_manager.0.set_session(Some(session)).await;
145
146 Ok(result)
147}
148
149#[tauri::command]
151#[specta::specta]
152pub async fn auth_verify_session(
153 server_url: String,
154 user_id: String,
155 access_token: String,
156 device_id: String,
157 auth_manager: State<'_, AuthManagerWrapper>,
158) -> Result<bool, String> {
159 match auth_manager
160 .0
161 .verify_session(&server_url, &user_id, &access_token, &device_id)
162 .await
163 {
164 Ok(_) => Ok(true),
165 Err(e) => {
166 log::warn!("[AuthCommands] Session verification failed: {}", e);
167 Ok(false)
168 }
169 }
170}
171
172#[tauri::command]
174#[specta::specta]
175pub async fn auth_logout(
176 server_url: String,
177 access_token: String,
178 device_id: String,
179 auth_manager: State<'_, AuthManagerWrapper>,
180 session_verifier: State<'_, SessionVerifierWrapper>,
181) -> Result<(), String> {
182 let mut verifier_guard = session_verifier.0.lock().await;
184 if let Some(verifier) = verifier_guard.take() {
185 verifier.stop();
186 }
187 drop(verifier_guard);
188
189 auth_manager
191 .0
192 .logout(&server_url, &access_token, &device_id)
193 .await?;
194
195 auth_manager.0.set_session(None).await;
197
198 Ok(())
199}
200
201#[tauri::command]
203#[specta::specta]
204pub async fn auth_get_session(
205 auth_manager: State<'_, AuthManagerWrapper>,
206) -> Result<Option<Session>, String> {
207 Ok(auth_manager.0.get_session().await)
208}
209
210#[tauri::command]
212#[specta::specta]
213pub async fn auth_set_session(
214 session: Option<Session>,
215 auth_manager: State<'_, AuthManagerWrapper>,
216) -> Result<(), String> {
217 let normalized_session = match session {
219 Some(mut s) => {
220 s.server_url = crate::auth::AuthManager::normalize_url(&s.server_url)?;
221 Some(s)
222 }
223 None => None,
224 };
225
226 auth_manager.0.set_session(normalized_session).await;
227 Ok(())
228}
229
230#[tauri::command]
232#[specta::specta]
233pub async fn auth_start_verification(
234 device_id: String,
235 app_handle: tauri::AppHandle,
236 auth_manager: State<'_, AuthManagerWrapper>,
237 session_verifier: State<'_, SessionVerifierWrapper>,
238) -> Result<(), String> {
239 let mut verifier_guard = session_verifier.0.lock().await;
240
241 if let Some(verifier) = verifier_guard.take() {
243 verifier.stop();
244 }
245
246 let manager = auth_manager.0.clone();
248
249 let mut verifier = SessionVerifier::new(manager, device_id);
251 verifier.set_app_handle(app_handle);
252 verifier.start().await;
253
254 *verifier_guard = Some(verifier);
255
256 Ok(())
257}
258
259#[tauri::command]
261#[specta::specta]
262pub async fn auth_stop_verification(
263 session_verifier: State<'_, SessionVerifierWrapper>,
264) -> Result<(), String> {
265 let mut verifier_guard = session_verifier.0.lock().await;
266
267 if let Some(verifier) = verifier_guard.take() {
268 verifier.stop();
269 }
270
271 Ok(())
272}
273
274#[tauri::command]
276#[specta::specta]
277pub async fn auth_reauthenticate(
278 password: String,
279 device_id: String,
280 auth_manager: State<'_, AuthManagerWrapper>,
281) -> Result<AuthResult, String> {
282 let session = auth_manager
284 .0
285 .get_session()
286 .await
287 .ok_or_else(|| "No active session to re-authenticate".to_string())?;
288
289 let result = auth_manager
291 .0
292 .login(
293 &session.server_url,
294 &session.username,
295 &password,
296 &device_id,
297 )
298 .await?;
299
300 let updated_session = Session {
302 user_id: result.user.id.clone(),
303 username: result.user.name.clone(),
304 server_id: result.server_id.clone(),
305 server_url: session.server_url,
306 server_name: session.server_name,
307 access_token: result.access_token.clone(),
308 verified: true,
309 needs_reauth: false,
310 };
311
312 auth_manager.0.set_session(Some(updated_session)).await;
313
314 Ok(result)
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320
321 #[test]
322 fn test_session_serialization() {
323 let session = Session {
324 user_id: "user-123".to_string(),
325 username: "john_doe".to_string(),
326 server_id: "server-456".to_string(),
327 server_url: "https://jellyfin.example.com".to_string(),
328 server_name: "My Jellyfin".to_string(),
329 access_token: "token-789-xyz".to_string(),
330 verified: true,
331 needs_reauth: false,
332 };
333
334 let json = serde_json::to_string(&session);
336 assert!(json.is_ok());
337 let serialized = json.unwrap();
338 assert!(serialized.contains("user-123"));
339 assert!(serialized.contains("john_doe"));
340 assert!(serialized.contains("server-456"));
341 }
342
343 #[test]
344 fn test_session_deserialization() {
345 let json = r#"{
346 "userId": "user-123",
347 "username": "john_doe",
348 "serverId": "server-456",
349 "serverUrl": "https://jellyfin.example.com",
350 "serverName": "My Jellyfin",
351 "accessToken": "token-789",
352 "verified": true,
353 "needsReauth": false
354 }"#;
355
356 let result: Result<Session, _> = serde_json::from_str(json);
357 assert!(result.is_ok());
358
359 let session = result.unwrap();
360 assert_eq!(session.user_id, "user-123");
361 assert_eq!(session.username, "john_doe");
362 assert_eq!(session.server_id, "server-456");
363 assert!(session.verified);
364 assert!(!session.needs_reauth);
365 }
366
367 #[test]
368 fn test_session_roundtrip() {
369 let original = Session {
370 user_id: "user-999".to_string(),
371 username: "alice".to_string(),
372 server_id: "server-111".to_string(),
373 server_url: "https://server.local".to_string(),
374 server_name: "Home Server".to_string(),
375 access_token: "very-long-token-string".to_string(),
376 verified: true,
377 needs_reauth: false,
378 };
379
380 let json = serde_json::to_string(&original).unwrap();
381 let deserialized: Session = serde_json::from_str(&json).unwrap();
382
383 assert_eq!(original.user_id, deserialized.user_id);
384 assert_eq!(original.username, deserialized.username);
385 assert_eq!(original.server_id, deserialized.server_id);
386 assert_eq!(original.server_url, deserialized.server_url);
387 assert_eq!(original.access_token, deserialized.access_token);
388 assert_eq!(original.verified, deserialized.verified);
389 }
390
391 #[test]
392 fn test_session_clone() {
393 let session = Session {
394 user_id: "user-clone".to_string(),
395 username: "test_user".to_string(),
396 server_id: "server-clone".to_string(),
397 server_url: "https://clone.example.com".to_string(),
398 server_name: "Clone Server".to_string(),
399 access_token: "clone-token".to_string(),
400 verified: false,
401 needs_reauth: true,
402 };
403
404 let cloned = session.clone();
405 assert_eq!(session.user_id, cloned.user_id);
406 assert_eq!(session.username, cloned.username);
407 assert_eq!(session.verified, cloned.verified);
408 assert_eq!(session.needs_reauth, cloned.needs_reauth);
409 }
410
411 #[test]
412 fn test_session_unverified() {
413 let session = Session {
414 user_id: "user-unverified".to_string(),
415 username: "newuser".to_string(),
416 server_id: "server-new".to_string(),
417 server_url: "https://new.example.com".to_string(),
418 server_name: "New Server".to_string(),
419 access_token: "new-token".to_string(),
420 verified: false,
421 needs_reauth: true,
422 };
423
424 let json = serde_json::to_string(&session).unwrap();
425 assert!(json.contains("false")); assert!(json.contains("true")); let deserialized: Session = serde_json::from_str(&json).unwrap();
429 assert!(!deserialized.verified);
430 assert!(deserialized.needs_reauth);
431 }
432
433 #[test]
434 fn test_session_debug() {
435 let session = Session {
436 user_id: "user-debug".to_string(),
437 username: "debug_user".to_string(),
438 server_id: "server-debug".to_string(),
439 server_url: "https://debug.example.com".to_string(),
440 server_name: "Debug Server".to_string(),
441 access_token: "debug-token".to_string(),
442 verified: true,
443 needs_reauth: false,
444 };
445
446 let debug_str = format!("{:?}", session);
447 assert!(debug_str.contains("user-debug"));
448 assert!(debug_str.contains("Session"));
449 }
450
451 #[test]
452 fn test_auth_manager_wrapper_structure() {
453 assert!(std::mem::size_of::<AuthManagerWrapper>() > 0);
455 }
456
457 #[test]
458 fn test_session_verifier_wrapper_structure() {
459 assert!(std::mem::size_of::<SessionVerifierWrapper>() > 0);
461 }
462
463 #[test]
464 fn test_session_with_special_characters() {
465 let session = Session {
466 user_id: "user-special-éñ".to_string(),
467 username: "user@example.com".to_string(),
468 server_id: "server/123".to_string(),
469 server_url: "https://jellyfin.example.com:8096".to_string(),
470 server_name: "My Jellyfin (v10.8.0)".to_string(),
471 access_token: "token+with/special=chars".to_string(),
472 verified: true,
473 needs_reauth: false,
474 };
475
476 let json = serde_json::to_string(&session).unwrap();
477 let deserialized: Session = serde_json::from_str(&json).unwrap();
478
479 assert_eq!(session.username, deserialized.username);
480 assert_eq!(session.server_name, deserialized.server_name);
481 assert_eq!(session.access_token, deserialized.access_token);
482 }
483
484 #[test]
485 fn test_session_field_presence() {
486 let session = Session {
487 user_id: "u1".to_string(),
488 username: "user1".to_string(),
489 server_id: "s1".to_string(),
490 server_url: "url1".to_string(),
491 server_name: "name1".to_string(),
492 access_token: "token1".to_string(),
493 verified: true,
494 needs_reauth: false,
495 };
496
497 let json = serde_json::to_string(&session).unwrap();
498
499 assert!(json.contains("userId"));
501 assert!(json.contains("username"));
502 assert!(json.contains("serverId"));
503 assert!(json.contains("serverUrl"));
504 assert!(json.contains("serverName"));
505 assert!(json.contains("accessToken"));
506 assert!(json.contains("verified"));
507 assert!(json.contains("needsReauth"));
508 }
509}