@@ -36,7 +36,7 @@ class UserModule {
3636 async loadUserData ( ) {
3737 try {
3838 // Load user info
39- const userResponse = await fetch ( './api/user/info' ) ;
39+ const userResponse = await fetch ( './api/user/info' , { credentials : 'include' } ) ;
4040 if ( ! userResponse . ok ) throw new Error ( 'Failed to fetch user data' ) ;
4141
4242 const userData = await userResponse . json ( ) ;
@@ -49,7 +49,7 @@ class UserModule {
4949
5050 // Load Plex status
5151 try {
52- const plexResponse = await fetch ( './api/auth/plex/status' ) ;
52+ const plexResponse = await fetch ( './api/auth/plex/status' , { credentials : 'include' } ) ;
5353 if ( plexResponse . ok ) {
5454 const plexData = await plexResponse . json ( ) ;
5555 if ( plexData . success ) {
@@ -65,6 +65,9 @@ class UserModule {
6565 this . updatePlexStatus ( null ) ;
6666 }
6767
68+ // Check if we're returning from Plex authentication
69+ this . checkPlexReturn ( ) ;
70+
6871 } catch ( error ) {
6972 console . error ( 'Error loading user data:' , error ) ;
7073 }
@@ -84,6 +87,7 @@ class UserModule {
8487 const response = await fetch ( './api/user/change-username' , {
8588 method : 'POST' ,
8689 headers : { 'Content-Type' : 'application/json' } ,
90+ credentials : 'include' ,
8791 body : JSON . stringify ( {
8892 username : newUsername ,
8993 password : currentPassword
@@ -130,6 +134,7 @@ class UserModule {
130134 const response = await fetch ( './api/user/change-password' , {
131135 method : 'POST' ,
132136 headers : { 'Content-Type' : 'application/json' } ,
137+ credentials : 'include' ,
133138 body : JSON . stringify ( {
134139 current_password : currentPassword ,
135140 new_password : newPassword
@@ -153,7 +158,10 @@ class UserModule {
153158
154159 async enableTwoFactor ( ) {
155160 try {
156- const response = await fetch ( './api/user/2fa/setup' , { method : 'POST' } ) ;
161+ const response = await fetch ( './api/user/2fa/setup' , {
162+ method : 'POST' ,
163+ credentials : 'include'
164+ } ) ;
157165 const result = await response . json ( ) ;
158166
159167 if ( response . ok ) {
@@ -183,6 +191,7 @@ class UserModule {
183191 const response = await fetch ( './api/user/2fa/verify' , {
184192 method : 'POST' ,
185193 headers : { 'Content-Type' : 'application/json' } ,
194+ credentials : 'include' ,
186195 body : JSON . stringify ( { code } )
187196 } ) ;
188197
@@ -216,6 +225,7 @@ class UserModule {
216225 const response = await fetch ( './api/user/2fa/disable' , {
217226 method : 'POST' ,
218227 headers : { 'Content-Type' : 'application/json' } ,
228+ credentials : 'include' ,
219229 body : JSON . stringify ( {
220230 password : password ,
221231 code : otpCode
@@ -329,101 +339,214 @@ class UserModule {
329339 }
330340
331341 async linkPlexAccount ( ) {
342+ const modal = document . getElementById ( 'plexLinkModal' ) ;
343+ const pinCode = document . getElementById ( 'plexLinkPinCode' ) ;
344+
345+ modal . style . display = 'block' ;
346+ pinCode . textContent = '' ;
347+ this . setPlexLinkStatus ( 'waiting' , '<i class="fas fa-spinner spinner"></i> Preparing Plex authentication...' ) ;
348+
332349 try {
333- const response = await fetch ( './api/auth/plex/pin' , {
350+ // Create Plex PIN with user_mode flag
351+ const response = await fetch ( './api/auth/plex/pin' , {
334352 method : 'POST' ,
335- headers : {
336- 'Content-Type' : 'application/json'
337- } ,
338- body : JSON . stringify ( {
339- user_mode : true
340- } )
353+ headers : { 'Content-Type' : 'application/json' } ,
354+ body : JSON . stringify ( { user_mode : true } )
341355 } ) ;
342- const result = await response . json ( ) ;
343-
344- if ( response . ok ) {
345- // Open Plex auth URL in new window
346- window . open ( result . auth_url , '_blank' ) ;
356+
357+ const data = await response . json ( ) ;
358+
359+ if ( data . success ) {
360+ this . currentPlexPinId = data . pin_id ;
347361
348- document . getElementById ( 'plexLinkStatus' ) . textContent = 'Waiting for authentication...' ;
349- document . getElementById ( 'plexLinkModal' ) . style . display = 'flex' ;
362+ // Extract PIN code from auth URL
363+ const hashPart = data . auth_url . split ( '#' ) [ 1 ] ;
364+ if ( hashPart ) {
365+ const urlParams = new URLSearchParams ( hashPart . substring ( 1 ) ) ;
366+ const pinCodeValue = urlParams . get ( 'code' ) ;
367+ pinCode . textContent = pinCodeValue || 'PIN-' + this . currentPlexPinId ;
368+ } else {
369+ pinCode . textContent = 'PIN-' + this . currentPlexPinId ;
370+ }
371+
372+ this . setPlexLinkStatus ( 'waiting' , '<i class="fas fa-external-link-alt"></i> You will be redirected to Plex to sign in. After authentication, you will be brought back here automatically.' ) ;
350373
351- this . plexPinId = result . pin_id ;
352- this . startPlexPolling ( ) ;
374+ // Store PIN ID and flags for when we return from Plex
375+ localStorage . setItem ( 'huntarr-plex-pin-id' , this . currentPlexPinId ) ;
376+ localStorage . setItem ( 'huntarr-plex-linking' , 'true' ) ;
377+ localStorage . setItem ( 'huntarr-plex-user-mode' , 'true' ) ;
378+
379+ // Redirect to Plex authentication
380+ setTimeout ( ( ) => {
381+ this . setPlexLinkStatus ( 'waiting' , '<i class="fas fa-spinner spinner"></i> Redirecting to Plex...' ) ;
382+ setTimeout ( ( ) => {
383+ window . location . href = data . auth_url ;
384+ } , 1000 ) ;
385+ } , 2000 ) ;
353386 } else {
354- const statusElement = document . getElementById ( 'plexMainPageStatus' ) ;
355- this . showStatus ( statusElement , result . error || 'Failed to get Plex PIN' , 'error' ) ;
387+ this . setPlexLinkStatus ( 'error' , '<i class="fas fa-exclamation-triangle"></i> Failed to create Plex PIN: ' + ( data . error || 'Unknown error. Please try again.' ) ) ;
356388 }
357389 } catch ( error ) {
358- const statusElement = document . getElementById ( 'plexMainPageStatus' ) ;
359- this . showStatus ( statusElement , 'Error connecting to Plex' , 'error ') ;
390+ console . error ( 'Error creating Plex PIN:' , error ) ;
391+ this . setPlexLinkStatus ( 'error' , '<i class="fas fa-exclamation-triangle"></i> Network error: Unable to connect to Plex. Please check your internet connection and try again. ' ) ;
360392 }
361393 }
362394
363- startPlexPolling ( ) {
364- this . plexPollingInterval = setInterval ( async ( ) => {
365- try {
366- const response = await fetch ( `./api/auth/plex/check/${ this . plexPinId } ` , { method : 'GET' } ) ;
367- const result = await response . json ( ) ;
395+ setPlexLinkStatus ( type , message ) {
396+ const plexLinkStatus = document . getElementById ( 'plexLinkStatus' ) ;
397+
398+ if ( plexLinkStatus ) {
399+ plexLinkStatus . className = `plex-status ${ type } ` ;
400+ plexLinkStatus . innerHTML = message ;
401+ plexLinkStatus . style . display = 'block' ;
402+ }
403+ }
368404
369- if ( response . ok && result . success && result . claimed ) {
370- // PIN has been claimed, now link the account
371- document . getElementById ( 'plexLinkStatus' ) . textContent = 'Linking account...' ;
372-
373- try {
374- // Use the same approach as setup - let backend get username from database
375- const linkResponse = await fetch ( './api/auth/plex/link' , {
376- method : 'POST' ,
377- headers : {
378- 'Content-Type' : 'application/json'
379- } ,
380- body : JSON . stringify ( {
381- token : result . token ,
382- setup_mode : true // Use setup mode like the working implementation
383- } )
384- } ) ;
385-
386- const linkResult = await linkResponse . json ( ) ;
387-
388- if ( linkResponse . ok && linkResult . success ) {
389- document . getElementById ( 'plexLinkStatus' ) . textContent = 'Successfully linked!' ;
390- document . getElementById ( 'plexLinkStatus' ) . className = 'plex-status success' ;
391-
392- setTimeout ( ( ) => {
393- this . cancelPlexLink ( ) ;
394- this . loadUserData ( ) ; // Refresh user data to show linked account
395- } , 2000 ) ;
396- } else {
397- document . getElementById ( 'plexLinkStatus' ) . textContent = linkResult . error || 'Failed to link account' ;
398- document . getElementById ( 'plexLinkStatus' ) . className = 'plex-status error' ;
399- }
400-
401- clearInterval ( this . plexPollingInterval ) ;
402- } catch ( linkError ) {
403- document . getElementById ( 'plexLinkStatus' ) . textContent = 'Error linking account' ;
404- document . getElementById ( 'plexLinkStatus' ) . className = 'plex-status error' ;
405- clearInterval ( this . plexPollingInterval ) ;
405+ startPlexPolling ( ) {
406+ console . log ( 'startPlexPinChecking called with PIN ID:' , this . currentPlexPinId ) ;
407+
408+ // Clear any existing interval
409+ if ( this . plexPollingInterval ) {
410+ console . log ( 'Clearing existing interval' ) ;
411+ clearInterval ( this . plexPollingInterval ) ;
412+ this . plexPollingInterval = null ;
413+ }
414+
415+ if ( ! this . currentPlexPinId ) {
416+ console . error ( 'No PIN ID available for checking' ) ;
417+ this . setPlexLinkStatus ( 'error' , '<i class="fas fa-exclamation-triangle"></i> No PIN ID available. Please try again.' ) ;
418+ return ;
419+ }
420+
421+ this . setPlexLinkStatus ( 'waiting' , '<i class="fas fa-hourglass-half"></i> Checking authentication status...' ) ;
422+
423+ this . plexPollingInterval = setInterval ( ( ) => {
424+ console . log ( 'Checking PIN status for:' , this . currentPlexPinId ) ;
425+
426+ fetch ( `./api/auth/plex/check/${ this . currentPlexPinId } ` )
427+ . then ( response => {
428+ console . log ( 'PIN check response status:' , response . status ) ;
429+ return response . json ( ) ;
430+ } )
431+ . then ( data => {
432+ console . log ( 'PIN check data:' , data ) ;
433+ if ( data . success && data . claimed ) {
434+ console . log ( 'PIN claimed, linking account' ) ;
435+ this . setPlexLinkStatus ( 'success' , '<i class="fas fa-link"></i> Plex account successfully linked!' ) ;
436+ this . stopPlexLinking ( ) ; // Stop checking immediately
437+ this . linkWithPlexToken ( data . token ) ; // This will also call stopPlexLinking in finally
438+ } else if ( data . success && ! data . claimed ) {
439+ console . log ( 'PIN not yet claimed, continuing to check' ) ;
440+ this . setPlexLinkStatus ( 'waiting' , '<i class="fas fa-hourglass-half"></i> Waiting for Plex authentication to complete...' ) ;
441+ } else {
442+ console . error ( 'PIN check failed:' , data ) ;
443+ this . setPlexLinkStatus ( 'error' , '<i class="fas fa-exclamation-triangle"></i> Authentication check failed: ' + ( data . error || 'Please try again.' ) ) ;
444+ this . stopPlexLinking ( ) ;
406445 }
407- } else if ( result . error && result . error !== 'PIN not authorized yet' ) {
408- document . getElementById ( 'plexLinkStatus' ) . textContent = result . error ;
409- document . getElementById ( 'plexLinkStatus' ) . className = 'plex-status error' ;
410- clearInterval ( this . plexPollingInterval ) ;
411- }
412- } catch ( error ) {
413- document . getElementById ( 'plexLinkStatus' ) . textContent = 'Error checking authorization' ;
414- document . getElementById ( 'plexLinkStatus' ) . className = 'plex-status error' ;
415- clearInterval ( this . plexPollingInterval ) ;
416- }
446+ } )
447+ . catch ( error => {
448+ console . error ( 'Error checking PIN:' , error ) ;
449+ this . setPlexLinkStatus ( 'error' , '<i class="fas fa-exclamation-triangle"></i> Network error: Unable to verify authentication status. Please try again.' ) ;
450+ this . stopPlexLinking ( ) ;
451+ } ) ;
417452 } , 2000 ) ;
453+
454+ // Stop checking after 10 minutes
455+ setTimeout ( ( ) => {
456+ if ( this . plexPollingInterval ) {
457+ console . log ( 'PIN check timeout reached' ) ;
458+ this . stopPlexLinking ( ) ;
459+ this . setPlexLinkStatus ( 'error' , '<i class="fas fa-clock"></i> Authentication timeout: PIN expired after 10 minutes. Please try linking your account again.' ) ;
460+ }
461+ } , 600000 ) ;
418462 }
419-
420- cancelPlexLink ( ) {
463+
464+ async linkWithPlexToken ( token ) {
465+ console . log ( 'Linking with Plex token' ) ;
466+ this . setPlexLinkStatus ( 'waiting' , '<i class="fas fa-spinner spinner"></i> Finalizing account link...' ) ;
467+
468+ try {
469+ // Use the same approach as setup - let backend get username from database
470+ const linkResponse = await fetch ( './api/auth/plex/link' , {
471+ method : 'POST' ,
472+ headers : {
473+ 'Content-Type' : 'application/json' ,
474+ } ,
475+ credentials : 'include' ,
476+ body : JSON . stringify ( {
477+ token : token ,
478+ setup_mode : true // Use setup mode like the working implementation
479+ } )
480+ } ) ;
481+
482+ const linkResult = await linkResponse . json ( ) ;
483+
484+ if ( linkResponse . ok && linkResult . success ) {
485+ this . setPlexLinkStatus ( 'success' , '<i class="fas fa-check-circle"></i> Plex account successfully linked!' ) ;
486+ setTimeout ( ( ) => {
487+ const modal = document . getElementById ( 'plexLinkModal' ) ;
488+ if ( modal ) modal . style . display = 'none' ;
489+
490+ // Reload user data to show updated Plex status
491+ this . loadUserData ( ) ;
492+ } , 2000 ) ;
493+ } else {
494+ this . setPlexLinkStatus ( 'error' , '<i class="fas fa-exclamation-triangle"></i> Account linking failed: ' + ( linkResult . error || 'Unknown error occurred. Please try again.' ) ) ;
495+ }
496+ } catch ( error ) {
497+ console . error ( 'Error linking Plex account:' , error ) ;
498+ this . setPlexLinkStatus ( 'error' , '<i class="fas fa-exclamation-triangle"></i> Network error: Unable to complete account linking. Please check your connection and try again.' ) ;
499+ } finally {
500+ // Always stop the PIN checking interval when linking completes (success or failure)
501+ console . log ( 'linkWithPlexToken completed, stopping PIN checking' ) ;
502+ this . stopPlexLinking ( ) ;
503+ }
504+ }
505+
506+ stopPlexLinking ( ) {
507+ console . log ( 'stopPlexLinking called' ) ;
421508 if ( this . plexPollingInterval ) {
422509 clearInterval ( this . plexPollingInterval ) ;
510+ this . plexPollingInterval = null ;
511+ console . log ( 'Cleared PIN check interval' ) ;
423512 }
513+ this . currentPlexPinId = null ;
514+ }
515+
516+ // Add method to check for return from Plex authentication
517+ checkPlexReturn ( ) {
518+ const plexLinking = localStorage . getItem ( 'huntarr-plex-linking' ) ;
519+ const plexPinId = localStorage . getItem ( 'huntarr-plex-pin-id' ) ;
520+ const userMode = localStorage . getItem ( 'huntarr-plex-user-mode' ) ;
521+
522+ if ( plexLinking === 'true' && plexPinId && userMode === 'true' ) {
523+ console . log ( 'Detected return from Plex authentication, PIN ID:' , plexPinId ) ;
524+
525+ // Clear the flags
526+ localStorage . removeItem ( 'huntarr-plex-linking' ) ;
527+ localStorage . removeItem ( 'huntarr-plex-pin-id' ) ;
528+ localStorage . removeItem ( 'huntarr-plex-user-mode' ) ;
529+
530+ // Show modal and start checking
531+ document . getElementById ( 'plexLinkModal' ) . style . display = 'block' ;
532+
533+ // Extract PIN code for display
534+ const pinCodeValue = plexPinId . substring ( 0 , 4 ) + '-' + plexPinId . substring ( 4 ) ;
535+ document . getElementById ( 'plexLinkPinCode' ) . textContent = pinCodeValue ;
536+
537+ // Set global PIN ID and start checking
538+ this . currentPlexPinId = plexPinId ;
539+ this . setPlexLinkStatus ( 'waiting' , '<i class="fas fa-spinner spinner"></i> Completing Plex authentication and linking your account...' ) ;
540+
541+ console . log ( 'Starting PIN checking for returned user' ) ;
542+ this . startPlexPolling ( ) ;
543+ }
544+ }
545+
546+ cancelPlexLink ( ) {
547+ this . stopPlexLinking ( ) ;
424548 document . getElementById ( 'plexLinkModal' ) . style . display = 'none' ;
425- document . getElementById ( 'plexLinkStatus' ) . className = 'plex-status waiting' ;
426- document . getElementById ( 'plexLinkStatus' ) . textContent = 'Waiting for authentication...' ;
549+ this . setPlexLinkStatus ( 'waiting' , '<i class="fas fa-hourglass-half"></i> Initializing Plex authentication...' ) ;
427550 }
428551
429552 async unlinkPlexAccount ( ) {
@@ -434,7 +557,10 @@ class UserModule {
434557 }
435558
436559 try {
437- const response = await fetch ( './api/auth/plex/unlink' , { method : 'POST' } ) ;
560+ const response = await fetch ( './api/auth/plex/unlink' , {
561+ method : 'POST' ,
562+ credentials : 'include'
563+ } ) ;
438564 const result = await response . json ( ) ;
439565
440566 if ( response . ok ) {
0 commit comments