Skip to content

Commit f61db7f

Browse files
committed
Enhance Plex account linking and authentication flow
- Updated user.js to include credentials in fetch requests for user data and Plex status, ensuring proper session handling. - Improved the Plex linking process with better error handling and user feedback during authentication. - Modified index.html to streamline redirection logic based on Plex linking state. - Enhanced user_section.html for improved modal styling and user experience during Plex account linking. - Refactored plex_auth_routes.py to simplify the callback handling and improve user redirection after Plex authentication.
1 parent 4e5f654 commit f61db7f

File tree

5 files changed

+228
-134
lines changed

5 files changed

+228
-134
lines changed

frontend/static/js/user.js

Lines changed: 206 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)