use std::sync::Arc; use tauri::State; use crate::auth::{AuthManager, SessionVerifier, ServerInfo, AuthResult, Session}; /// Wrapper for AuthManager to manage in Tauri state pub struct AuthManagerWrapper(pub Arc); /// Wrapper for SessionVerifier to manage in Tauri state pub struct SessionVerifierWrapper(pub Arc>>); /// Initialize the auth manager (call on app startup) /// Restores session from storage if available #[tauri::command] pub async fn auth_initialize( auth_manager: State<'_, AuthManagerWrapper>, database: State<'_, crate::commands::DatabaseWrapper>, credentials: State<'_, crate::commands::CredentialStoreWrapper>, ) -> Result, String> { // First check if we already have a session in memory if let Some(session) = auth_manager.0.get_session().await { return Ok(Some(session)); } // Try to restore session from storage log::info!("[AuthManager] Restoring session from storage..."); // Use the existing storage_get_active_session function let active_session = match crate::commands::storage::storage_get_active_session(database, credentials).await { Ok(Some(session)) => session, Ok(None) => { log::info!("[AuthManager] No active session in storage"); return Ok(None); } Err(e) => { log::error!("[AuthManager] Failed to get active session: {}", e); return Err(e); } }; // Create session object from active session with normalized URL let normalized_url = crate::auth::AuthManager::normalize_url(&active_session.server_url); let session = Session { user_id: active_session.user_id, username: active_session.username, server_id: active_session.server_id, server_url: normalized_url, server_name: active_session.server_name, access_token: active_session.access_token, verified: false, // Will be verified in background needs_reauth: false, }; // Store in AuthManager auth_manager.0.set_session(Some(session.clone())).await; log::info!("[AuthManager] Session restored for user: {} with normalized URL: {}", session.username, session.server_url); Ok(Some(session)) } /// Connect to a Jellyfin server and get server info #[tauri::command] pub async fn auth_connect_to_server( server_url: String, auth_manager: State<'_, AuthManagerWrapper>, ) -> Result { auth_manager.0.connect_to_server(&server_url).await } /// Login with username and password #[tauri::command] pub async fn auth_login( server_url: String, username: String, password: String, device_id: String, auth_manager: State<'_, AuthManagerWrapper>, ) -> Result { let result = auth_manager.0.login(&server_url, &username, &password, &device_id).await?; // Create session from auth result with normalized URL let normalized_url = crate::auth::AuthManager::normalize_url(&server_url); let session = Session { user_id: result.user.id.clone(), username: result.user.name.clone(), server_id: result.server_id.clone(), server_url: normalized_url, server_name: String::new(), // Will be set by frontend access_token: result.access_token.clone(), verified: true, needs_reauth: false, }; auth_manager.0.set_session(Some(session)).await; Ok(result) } /// Verify current session #[tauri::command] pub async fn auth_verify_session( server_url: String, user_id: String, access_token: String, device_id: String, auth_manager: State<'_, AuthManagerWrapper>, ) -> Result { match auth_manager.0.verify_session(&server_url, &user_id, &access_token, &device_id).await { Ok(_) => Ok(true), Err(e) => { log::warn!("[AuthCommands] Session verification failed: {}", e); Ok(false) } } } /// Logout (clear session and call Jellyfin logout endpoint) #[tauri::command] pub async fn auth_logout( server_url: String, access_token: String, device_id: String, auth_manager: State<'_, AuthManagerWrapper>, session_verifier: State<'_, SessionVerifierWrapper>, ) -> Result<(), String> { // Stop session verification let mut verifier_guard = session_verifier.0.lock().await; if let Some(verifier) = verifier_guard.take() { verifier.stop(); } drop(verifier_guard); // Call Jellyfin logout endpoint auth_manager.0.logout(&server_url, &access_token, &device_id).await?; // Clear session auth_manager.0.set_session(None).await; Ok(()) } /// Get current session #[tauri::command] pub async fn auth_get_session( auth_manager: State<'_, AuthManagerWrapper>, ) -> Result, String> { Ok(auth_manager.0.get_session().await) } /// Set current session (for restoration from storage) #[tauri::command] pub async fn auth_set_session( session: Option, auth_manager: State<'_, AuthManagerWrapper>, ) -> Result<(), String> { // Normalize the server URL if session is provided let normalized_session = session.map(|mut s| { s.server_url = crate::auth::AuthManager::normalize_url(&s.server_url); s }); auth_manager.0.set_session(normalized_session).await; Ok(()) } /// Start background session verification #[tauri::command] pub async fn auth_start_verification( device_id: String, app_handle: tauri::AppHandle, auth_manager: State<'_, AuthManagerWrapper>, session_verifier: State<'_, SessionVerifierWrapper>, ) -> Result<(), String> { let mut verifier_guard = session_verifier.0.lock().await; // Stop existing verifier if any if let Some(verifier) = verifier_guard.take() { verifier.stop(); } // Get AuthManager Arc let manager = auth_manager.0.clone(); // Create new verifier let mut verifier = SessionVerifier::new(manager, device_id); verifier.set_app_handle(app_handle); verifier.start().await; *verifier_guard = Some(verifier); Ok(()) } /// Stop background session verification #[tauri::command] pub async fn auth_stop_verification( session_verifier: State<'_, SessionVerifierWrapper>, ) -> Result<(), String> { let mut verifier_guard = session_verifier.0.lock().await; if let Some(verifier) = verifier_guard.take() { verifier.stop(); } Ok(()) } /// Re-authenticate with password (when session expired) #[tauri::command] pub async fn auth_reauthenticate( password: String, device_id: String, auth_manager: State<'_, AuthManagerWrapper>, ) -> Result { // Get current session to extract server_url and username let session = auth_manager.0.get_session().await .ok_or_else(|| "No active session to re-authenticate".to_string())?; // Re-login with stored credentials let result = auth_manager.0.login(&session.server_url, &session.username, &password, &device_id).await?; // Update session with new token let updated_session = Session { user_id: result.user.id.clone(), username: result.user.name.clone(), server_id: result.server_id.clone(), server_url: session.server_url, server_name: session.server_name, access_token: result.access_token.clone(), verified: true, needs_reauth: false, }; auth_manager.0.set_session(Some(updated_session)).await; Ok(result) } #[cfg(test)] mod tests { use super::*; #[test] fn test_session_serialization() { let session = Session { user_id: "user-123".to_string(), username: "john_doe".to_string(), server_id: "server-456".to_string(), server_url: "https://jellyfin.example.com".to_string(), server_name: "My Jellyfin".to_string(), access_token: "token-789-xyz".to_string(), verified: true, needs_reauth: false, }; // Should serialize successfully let json = serde_json::to_string(&session); assert!(json.is_ok()); let serialized = json.unwrap(); assert!(serialized.contains("user-123")); assert!(serialized.contains("john_doe")); assert!(serialized.contains("server-456")); } #[test] fn test_session_deserialization() { let json = r#"{ "userId": "user-123", "username": "john_doe", "serverId": "server-456", "serverUrl": "https://jellyfin.example.com", "serverName": "My Jellyfin", "accessToken": "token-789", "verified": true, "needsReauth": false }"#; let result: Result = serde_json::from_str(json); assert!(result.is_ok()); let session = result.unwrap(); assert_eq!(session.user_id, "user-123"); assert_eq!(session.username, "john_doe"); assert_eq!(session.server_id, "server-456"); assert!(session.verified); assert!(!session.needs_reauth); } #[test] fn test_session_roundtrip() { let original = Session { user_id: "user-999".to_string(), username: "alice".to_string(), server_id: "server-111".to_string(), server_url: "https://server.local".to_string(), server_name: "Home Server".to_string(), access_token: "very-long-token-string".to_string(), verified: true, needs_reauth: false, }; let json = serde_json::to_string(&original).unwrap(); let deserialized: Session = serde_json::from_str(&json).unwrap(); assert_eq!(original.user_id, deserialized.user_id); assert_eq!(original.username, deserialized.username); assert_eq!(original.server_id, deserialized.server_id); assert_eq!(original.server_url, deserialized.server_url); assert_eq!(original.access_token, deserialized.access_token); assert_eq!(original.verified, deserialized.verified); } #[test] fn test_session_clone() { let session = Session { user_id: "user-clone".to_string(), username: "test_user".to_string(), server_id: "server-clone".to_string(), server_url: "https://clone.example.com".to_string(), server_name: "Clone Server".to_string(), access_token: "clone-token".to_string(), verified: false, needs_reauth: true, }; let cloned = session.clone(); assert_eq!(session.user_id, cloned.user_id); assert_eq!(session.username, cloned.username); assert_eq!(session.verified, cloned.verified); assert_eq!(session.needs_reauth, cloned.needs_reauth); } #[test] fn test_session_unverified() { let session = Session { user_id: "user-unverified".to_string(), username: "newuser".to_string(), server_id: "server-new".to_string(), server_url: "https://new.example.com".to_string(), server_name: "New Server".to_string(), access_token: "new-token".to_string(), verified: false, needs_reauth: true, }; let json = serde_json::to_string(&session).unwrap(); assert!(json.contains("false")); // verified: false assert!(json.contains("true")); // needs_reauth: true let deserialized: Session = serde_json::from_str(&json).unwrap(); assert!(!deserialized.verified); assert!(deserialized.needs_reauth); } #[test] fn test_session_debug() { let session = Session { user_id: "user-debug".to_string(), username: "debug_user".to_string(), server_id: "server-debug".to_string(), server_url: "https://debug.example.com".to_string(), server_name: "Debug Server".to_string(), access_token: "debug-token".to_string(), verified: true, needs_reauth: false, }; let debug_str = format!("{:?}", session); assert!(debug_str.contains("user-debug")); assert!(debug_str.contains("Session")); } #[test] fn test_auth_manager_wrapper_structure() { // Verify wrapper type exists and has correct structure assert_eq!(std::mem::size_of::() > 0, true); } #[test] fn test_session_verifier_wrapper_structure() { // Verify wrapper type exists and has correct structure assert_eq!(std::mem::size_of::() > 0, true); } #[test] fn test_session_with_special_characters() { let session = Session { user_id: "user-special-éñ".to_string(), username: "user@example.com".to_string(), server_id: "server/123".to_string(), server_url: "https://jellyfin.example.com:8096".to_string(), server_name: "My Jellyfin (v10.8.0)".to_string(), access_token: "token+with/special=chars".to_string(), verified: true, needs_reauth: false, }; let json = serde_json::to_string(&session).unwrap(); let deserialized: Session = serde_json::from_str(&json).unwrap(); assert_eq!(session.username, deserialized.username); assert_eq!(session.server_name, deserialized.server_name); assert_eq!(session.access_token, deserialized.access_token); } #[test] fn test_session_field_presence() { let session = Session { user_id: "u1".to_string(), username: "user1".to_string(), server_id: "s1".to_string(), server_url: "url1".to_string(), server_name: "name1".to_string(), access_token: "token1".to_string(), verified: true, needs_reauth: false, }; let json = serde_json::to_string(&session).unwrap(); // Verify camelCase serialization (serde rename_all = "camelCase") assert!(json.contains("userId")); assert!(json.contains("username")); assert!(json.contains("serverId")); assert!(json.contains("serverUrl")); assert!(json.contains("serverName")); assert!(json.contains("accessToken")); assert!(json.contains("verified")); assert!(json.contains("needsReauth")); } }