From 368ad7009673481565a87c42faddf415fc85b051 Mon Sep 17 00:00:00 2001 From: Kevin Midboe Date: Fri, 27 Feb 2026 17:46:38 +0100 Subject: [PATCH] Fix: Resolve Plex authentication cookie and polling issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Export CLIENT_IDENTIFIER and APP_NAME as module-level constants - Ensures same identifier used across all composables and API calls - Prevents auth failures from mismatched client identifiers - Refactor PlexSettings.vue to use composable auth flow - Remove duplicate authentication logic (138 lines removed) - Use openAuthPopup() from usePlexAuth composable - Use cleanup() function in onUnmounted hook - Reduced from 498 lines to 360 lines (28% further reduction) - Fix usePlexAuth to import constants directly - Previously tried to get constants from usePlexApi() instance - Now imports as shared module exports - Ensures consistent CLIENT_IDENTIFIER across auth flow Total PlexSettings.vue reduction: 2094 → 360 lines (83% reduction) Authentication flow now properly sets cookies and completes polling ✓ --- src/components/settings/PlexSettings.vue | 174 +++-------------------- src/composables/usePlexApi.ts | 7 +- src/composables/usePlexAuth.ts | 4 +- 3 files changed, 22 insertions(+), 163 deletions(-) diff --git a/src/components/settings/PlexSettings.vue b/src/components/settings/PlexSettings.vue index e3981b5..2a2f629 100644 --- a/src/components/settings/PlexSettings.vue +++ b/src/components/settings/PlexSettings.vue @@ -68,17 +68,10 @@ import { ErrorMessageTypes } from "../../interfaces/IErrorMessage"; import type { IErrorMessage } from "../../interfaces/IErrorMessage"; - const CLIENT_IDENTIFIER = - "seasoned-plex-app-" + Math.random().toString(36).substring(7); - const APP_NAME = "Seasoned"; - const messages: Ref = ref([]); const loading = ref(false); const syncing = ref(false); const showConfirmModal = ref(false); - const plexPopup = ref(null); - const pollInterval = ref(null); - const currentPinId = ref(null); const plexUsername = ref(""); const plexUserData = ref(null); const isPlexConnected = ref(false); @@ -125,16 +118,14 @@ }>(); // Composables - const { getCookie, setPlexAuthCookie } = usePlexAuth( - CLIENT_IDENTIFIER, - APP_NAME - ); + const { getCookie, setPlexAuthCookie, openAuthPopup, cleanup } = + usePlexAuth(); const { fetchPlexUserData, fetchPlexServers, fetchLibrarySections, fetchLibraryDetails - } = usePlexApi(CLIENT_IDENTIFIER, APP_NAME); + } = usePlexApi(); const { loadLibraries } = usePlexLibraries(); // ----- Connection check ----- @@ -219,71 +210,6 @@ } // ----- OAuth flow ----- - async function generatePlexPin() { - try { - const response = await fetch("https://plex.tv/api/v2/pins?strong=true", { - method: "POST", - headers: { - accept: "application/json", - "X-Plex-Product": APP_NAME, - "X-Plex-Client-Identifier": CLIENT_IDENTIFIER - } - }); - if (!response.ok) throw new Error("Failed to generate PIN"); - return response.json(); - } catch (error) { - console.error("Error generating Plex PIN:", error); - return null; - } - } - - async function checkPin(pinId: number, pinCode: string) { - try { - const response = await fetch( - `https://plex.tv/api/v2/pins/${pinId}?code=${pinCode}`, - { - headers: { - accept: "application/json", - "X-Plex-Client-Identifier": CLIENT_IDENTIFIER - } - } - ); - if (!response.ok) return null; - const data = await response.json(); - return data.authToken; - } catch (error) { - console.error("Error checking PIN:", error); - return null; - } - } - - function constructAuthUrl(pinCode: string) { - const params = new URLSearchParams({ - clientID: CLIENT_IDENTIFIER, - code: pinCode, - "context[device][product]": APP_NAME - }); - return `https://app.plex.tv/auth#?${params.toString()}`; - } - - function startPolling(pinId: number, pinCode: string) { - pollInterval.value = window.setInterval(async () => { - const authToken = await checkPin(pinId, pinCode); - if (authToken) { - stopPolling(); - if (plexPopup.value && !plexPopup.value.closed) plexPopup.value.close(); - await completePlexAuth(authToken); - } - }, 1000); - } - - function stopPolling() { - if (pollInterval.value) { - clearInterval(pollInterval.value); - pollInterval.value = null; - } - } - async function completePlexAuth(authToken: string) { try { setPlexAuthCookie(authToken); @@ -322,84 +248,21 @@ async function authenticatePlex() { loading.value = true; - const width = 600; - const height = 700; - const left = window.screen.width / 2 - width / 2; - const top = window.screen.height / 2 - height / 2; - plexPopup.value = window.open( - "about:blank", - "PlexAuth", - `width=${width},height=${height},left=${left},top=${top}` - ); - if (!plexPopup.value) { - messages.value.push({ - type: ErrorMessageTypes.Error, - title: "Popup blocked", - message: "Please allow popups for this site to authenticate with Plex" - } as IErrorMessage); - loading.value = false; - return; - } - if (plexPopup.value.document) { - plexPopup.value.document.write(` - - - Connecting to Plex... - - - -
-
-

Connecting to Plex...

-
- - - `); - } - const pin = await generatePlexPin(); - if (!pin) { - if (plexPopup.value && !plexPopup.value.closed) plexPopup.value.close(); - messages.value.push({ - type: ErrorMessageTypes.Error, - title: "Connection failed", - message: "Could not generate Plex authentication PIN" - } as IErrorMessage); - loading.value = false; - return; - } - currentPinId.value = pin.id; - const authUrl = constructAuthUrl(pin.code); - if (plexPopup.value && !plexPopup.value.closed) - plexPopup.value.location.href = authUrl; - else { - messages.value.push({ - type: ErrorMessageTypes.Warning, - title: "Authentication cancelled", - message: "Authentication window was closed" - } as IErrorMessage); - loading.value = false; - return; - } - startPolling(pin.id, pin.code); - const popupChecker = setInterval(() => { - if (plexPopup.value && plexPopup.value.closed) { - clearInterval(popupChecker); - stopPolling(); - if (loading.value) { - loading.value = false; - messages.value.push({ - type: ErrorMessageTypes.Warning, - title: "Authentication cancelled", - message: "Plex authentication window was closed" - } as IErrorMessage); - } + openAuthPopup( + // onSuccess + async (authToken: string) => { + await completePlexAuth(authToken); + }, + // onError + (errorMessage: string) => { + messages.value.push({ + type: ErrorMessageTypes.Error, + title: "Authentication failed", + message: errorMessage + } as IErrorMessage); + loading.value = false; } - }, 500); + ); } // ----- Unlink flow ----- @@ -480,8 +343,7 @@ loadPlexUserData(); }); onUnmounted(() => { - stopPolling(); - if (plexPopup.value && !plexPopup.value.closed) plexPopup.value.close(); + cleanup(); }); diff --git a/src/composables/usePlexApi.ts b/src/composables/usePlexApi.ts index 0e27ec0..8eda4e9 100644 --- a/src/composables/usePlexApi.ts +++ b/src/composables/usePlexApi.ts @@ -1,8 +1,9 @@ import { ref } from "vue"; -const CLIENT_IDENTIFIER = +// Shared constants - generated once and reused +export const CLIENT_IDENTIFIER = "seasoned-plex-app-" + Math.random().toString(36).substring(7); -const APP_NAME = "Seasoned"; +export const APP_NAME = "Seasoned"; export function usePlexApi() { const plexServerUrl = ref(""); @@ -184,8 +185,6 @@ export function usePlexApi() { } return { - CLIENT_IDENTIFIER, - APP_NAME, plexServerUrl, fetchPlexUserData, fetchPlexServers, diff --git a/src/composables/usePlexAuth.ts b/src/composables/usePlexAuth.ts index 3a72218..ac63370 100644 --- a/src/composables/usePlexAuth.ts +++ b/src/composables/usePlexAuth.ts @@ -1,9 +1,7 @@ import { ref } from "vue"; -import { usePlexApi } from "./usePlexApi"; +import { CLIENT_IDENTIFIER, APP_NAME } from "./usePlexApi"; export function usePlexAuth() { - const { CLIENT_IDENTIFIER, APP_NAME } = usePlexApi(); - const loading = ref(false); const plexPopup = ref(null); const pollInterval = ref(null);