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 let active_session =
35 match crate::commands::storage::storage_get_active_session(database, credentials).await {
36 Ok(Some(session)) => session,
37 Ok(None) => {
38 log::info!("[AuthManager] No active session in storage");
39 return Ok(None);
40 }
41 Err(e) => {
42 log::error!("[AuthManager] Failed to get active session: {}", e);
43 return Err(e);
44 }
45 };
46
47 let normalized_url = crate::auth::AuthManager::normalize_url(&active_session.server_url)?;
49
50 let session = Session {
51 user_id: active_session.user_id,
52 username: active_session.username,
53 server_id: active_session.server_id,
54 server_url: normalized_url,
55 server_name: active_session.server_name,
56 access_token: active_session.access_token,
57 verified: false, needs_reauth: false,
59 };
60
61 auth_manager.0.set_session(Some(session.clone())).await;
63
64 log::info!(
65 "[AuthManager] Session restored for user: {} with normalized URL: {}",
66 session.username,
67 session.server_url
68 );
69 Ok(Some(session))
70}
71
72#[tauri::command]
74#[specta::specta]
75pub async fn auth_connect_to_server(
76 server_url: String,
77 auth_manager: State<'_, AuthManagerWrapper>,
78) -> Result<ServerInfo, String> {
79 auth_manager.0.connect_to_server(&server_url).await
80}
81
82#[tauri::command]
84#[specta::specta]
85pub async fn auth_login(
86 server_url: String,
87 username: String,
88 password: String,
89 device_id: String,
90 auth_manager: State<'_, AuthManagerWrapper>,
91) -> Result<AuthResult, String> {
92 let result = auth_manager
93 .0
94 .login(&server_url, &username, &password, &device_id)
95 .await?;
96
97 let normalized_url = crate::auth::AuthManager::normalize_url(&server_url)?;
99
100 let session = Session {
101 user_id: result.user.id.clone(),
102 username: result.user.name.clone(),
103 server_id: result.server_id.clone(),
104 server_url: normalized_url,
105 server_name: String::new(), access_token: result.access_token.clone(),
107 verified: true,
108 needs_reauth: false,
109 };
110
111 auth_manager.0.set_session(Some(session)).await;
112
113 Ok(result)
114}
115
116#[tauri::command]
118#[specta::specta]
119pub async fn auth_verify_session(
120 server_url: String,
121 user_id: String,
122 access_token: String,
123 device_id: String,
124 auth_manager: State<'_, AuthManagerWrapper>,
125) -> Result<bool, String> {
126 match auth_manager
127 .0
128 .verify_session(&server_url, &user_id, &access_token, &device_id)
129 .await
130 {
131 Ok(_) => Ok(true),
132 Err(e) => {
133 log::warn!("[AuthCommands] Session verification failed: {}", e);
134 Ok(false)
135 }
136 }
137}
138
139#[tauri::command]
141#[specta::specta]
142pub async fn auth_logout(
143 server_url: String,
144 access_token: String,
145 device_id: String,
146 auth_manager: State<'_, AuthManagerWrapper>,
147 session_verifier: State<'_, SessionVerifierWrapper>,
148) -> Result<(), String> {
149 let mut verifier_guard = session_verifier.0.lock().await;
151 if let Some(verifier) = verifier_guard.take() {
152 verifier.stop();
153 }
154 drop(verifier_guard);
155
156 auth_manager
158 .0
159 .logout(&server_url, &access_token, &device_id)
160 .await?;
161
162 auth_manager.0.set_session(None).await;
164
165 Ok(())
166}
167
168#[tauri::command]
170#[specta::specta]
171pub async fn auth_get_session(
172 auth_manager: State<'_, AuthManagerWrapper>,
173) -> Result<Option<Session>, String> {
174 Ok(auth_manager.0.get_session().await)
175}
176
177#[tauri::command]
179#[specta::specta]
180pub async fn auth_set_session(
181 session: Option<Session>,
182 auth_manager: State<'_, AuthManagerWrapper>,
183) -> Result<(), String> {
184 let normalized_session = match session {
186 Some(mut s) => {
187 s.server_url = crate::auth::AuthManager::normalize_url(&s.server_url)?;
188 Some(s)
189 }
190 None => None,
191 };
192
193 auth_manager.0.set_session(normalized_session).await;
194 Ok(())
195}
196
197#[tauri::command]
199#[specta::specta]
200pub async fn auth_start_verification(
201 device_id: String,
202 app_handle: tauri::AppHandle,
203 auth_manager: State<'_, AuthManagerWrapper>,
204 session_verifier: State<'_, SessionVerifierWrapper>,
205) -> Result<(), String> {
206 let mut verifier_guard = session_verifier.0.lock().await;
207
208 if let Some(verifier) = verifier_guard.take() {
210 verifier.stop();
211 }
212
213 let manager = auth_manager.0.clone();
215
216 let mut verifier = SessionVerifier::new(manager, device_id);
218 verifier.set_app_handle(app_handle);
219 verifier.start().await;
220
221 *verifier_guard = Some(verifier);
222
223 Ok(())
224}
225
226#[tauri::command]
228#[specta::specta]
229pub async fn auth_stop_verification(
230 session_verifier: State<'_, SessionVerifierWrapper>,
231) -> Result<(), String> {
232 let mut verifier_guard = session_verifier.0.lock().await;
233
234 if let Some(verifier) = verifier_guard.take() {
235 verifier.stop();
236 }
237
238 Ok(())
239}
240
241#[tauri::command]
243#[specta::specta]
244pub async fn auth_reauthenticate(
245 password: String,
246 device_id: String,
247 auth_manager: State<'_, AuthManagerWrapper>,
248) -> Result<AuthResult, String> {
249 let session = auth_manager
251 .0
252 .get_session()
253 .await
254 .ok_or_else(|| "No active session to re-authenticate".to_string())?;
255
256 let result = auth_manager
258 .0
259 .login(
260 &session.server_url,
261 &session.username,
262 &password,
263 &device_id,
264 )
265 .await?;
266
267 let updated_session = Session {
269 user_id: result.user.id.clone(),
270 username: result.user.name.clone(),
271 server_id: result.server_id.clone(),
272 server_url: session.server_url,
273 server_name: session.server_name,
274 access_token: result.access_token.clone(),
275 verified: true,
276 needs_reauth: false,
277 };
278
279 auth_manager.0.set_session(Some(updated_session)).await;
280
281 Ok(result)
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287
288 #[test]
289 fn test_session_serialization() {
290 let session = Session {
291 user_id: "user-123".to_string(),
292 username: "john_doe".to_string(),
293 server_id: "server-456".to_string(),
294 server_url: "https://jellyfin.example.com".to_string(),
295 server_name: "My Jellyfin".to_string(),
296 access_token: "token-789-xyz".to_string(),
297 verified: true,
298 needs_reauth: false,
299 };
300
301 let json = serde_json::to_string(&session);
303 assert!(json.is_ok());
304 let serialized = json.unwrap();
305 assert!(serialized.contains("user-123"));
306 assert!(serialized.contains("john_doe"));
307 assert!(serialized.contains("server-456"));
308 }
309
310 #[test]
311 fn test_session_deserialization() {
312 let json = r#"{
313 "userId": "user-123",
314 "username": "john_doe",
315 "serverId": "server-456",
316 "serverUrl": "https://jellyfin.example.com",
317 "serverName": "My Jellyfin",
318 "accessToken": "token-789",
319 "verified": true,
320 "needsReauth": false
321 }"#;
322
323 let result: Result<Session, _> = serde_json::from_str(json);
324 assert!(result.is_ok());
325
326 let session = result.unwrap();
327 assert_eq!(session.user_id, "user-123");
328 assert_eq!(session.username, "john_doe");
329 assert_eq!(session.server_id, "server-456");
330 assert!(session.verified);
331 assert!(!session.needs_reauth);
332 }
333
334 #[test]
335 fn test_session_roundtrip() {
336 let original = Session {
337 user_id: "user-999".to_string(),
338 username: "alice".to_string(),
339 server_id: "server-111".to_string(),
340 server_url: "https://server.local".to_string(),
341 server_name: "Home Server".to_string(),
342 access_token: "very-long-token-string".to_string(),
343 verified: true,
344 needs_reauth: false,
345 };
346
347 let json = serde_json::to_string(&original).unwrap();
348 let deserialized: Session = serde_json::from_str(&json).unwrap();
349
350 assert_eq!(original.user_id, deserialized.user_id);
351 assert_eq!(original.username, deserialized.username);
352 assert_eq!(original.server_id, deserialized.server_id);
353 assert_eq!(original.server_url, deserialized.server_url);
354 assert_eq!(original.access_token, deserialized.access_token);
355 assert_eq!(original.verified, deserialized.verified);
356 }
357
358 #[test]
359 fn test_session_clone() {
360 let session = Session {
361 user_id: "user-clone".to_string(),
362 username: "test_user".to_string(),
363 server_id: "server-clone".to_string(),
364 server_url: "https://clone.example.com".to_string(),
365 server_name: "Clone Server".to_string(),
366 access_token: "clone-token".to_string(),
367 verified: false,
368 needs_reauth: true,
369 };
370
371 let cloned = session.clone();
372 assert_eq!(session.user_id, cloned.user_id);
373 assert_eq!(session.username, cloned.username);
374 assert_eq!(session.verified, cloned.verified);
375 assert_eq!(session.needs_reauth, cloned.needs_reauth);
376 }
377
378 #[test]
379 fn test_session_unverified() {
380 let session = Session {
381 user_id: "user-unverified".to_string(),
382 username: "newuser".to_string(),
383 server_id: "server-new".to_string(),
384 server_url: "https://new.example.com".to_string(),
385 server_name: "New Server".to_string(),
386 access_token: "new-token".to_string(),
387 verified: false,
388 needs_reauth: true,
389 };
390
391 let json = serde_json::to_string(&session).unwrap();
392 assert!(json.contains("false")); assert!(json.contains("true")); let deserialized: Session = serde_json::from_str(&json).unwrap();
396 assert!(!deserialized.verified);
397 assert!(deserialized.needs_reauth);
398 }
399
400 #[test]
401 fn test_session_debug() {
402 let session = Session {
403 user_id: "user-debug".to_string(),
404 username: "debug_user".to_string(),
405 server_id: "server-debug".to_string(),
406 server_url: "https://debug.example.com".to_string(),
407 server_name: "Debug Server".to_string(),
408 access_token: "debug-token".to_string(),
409 verified: true,
410 needs_reauth: false,
411 };
412
413 let debug_str = format!("{:?}", session);
414 assert!(debug_str.contains("user-debug"));
415 assert!(debug_str.contains("Session"));
416 }
417
418 #[test]
419 fn test_auth_manager_wrapper_structure() {
420 assert!(std::mem::size_of::<AuthManagerWrapper>() > 0);
422 }
423
424 #[test]
425 fn test_session_verifier_wrapper_structure() {
426 assert!(std::mem::size_of::<SessionVerifierWrapper>() > 0);
428 }
429
430 #[test]
431 fn test_session_with_special_characters() {
432 let session = Session {
433 user_id: "user-special-éñ".to_string(),
434 username: "user@example.com".to_string(),
435 server_id: "server/123".to_string(),
436 server_url: "https://jellyfin.example.com:8096".to_string(),
437 server_name: "My Jellyfin (v10.8.0)".to_string(),
438 access_token: "token+with/special=chars".to_string(),
439 verified: true,
440 needs_reauth: false,
441 };
442
443 let json = serde_json::to_string(&session).unwrap();
444 let deserialized: Session = serde_json::from_str(&json).unwrap();
445
446 assert_eq!(session.username, deserialized.username);
447 assert_eq!(session.server_name, deserialized.server_name);
448 assert_eq!(session.access_token, deserialized.access_token);
449 }
450
451 #[test]
452 fn test_session_field_presence() {
453 let session = Session {
454 user_id: "u1".to_string(),
455 username: "user1".to_string(),
456 server_id: "s1".to_string(),
457 server_url: "url1".to_string(),
458 server_name: "name1".to_string(),
459 access_token: "token1".to_string(),
460 verified: true,
461 needs_reauth: false,
462 };
463
464 let json = serde_json::to_string(&session).unwrap();
465
466 assert!(json.contains("userId"));
468 assert!(json.contains("username"));
469 assert!(json.contains("serverId"));
470 assert!(json.contains("serverUrl"));
471 assert!(json.contains("serverName"));
472 assert!(json.contains("accessToken"));
473 assert!(json.contains("verified"));
474 assert!(json.contains("needsReauth"));
475 }
476}