mirror of
https://github.com/KevinMidboe/seasoned.git
synced 2026-03-11 11:55:38 +00:00
Fix: Resolve Plex authentication cookie and polling issues
- 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 ✓
This commit is contained in:
@@ -68,17 +68,10 @@
|
|||||||
import { ErrorMessageTypes } from "../../interfaces/IErrorMessage";
|
import { ErrorMessageTypes } from "../../interfaces/IErrorMessage";
|
||||||
import type { IErrorMessage } 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<IErrorMessage[]> = ref([]);
|
const messages: Ref<IErrorMessage[]> = ref([]);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const syncing = ref(false);
|
const syncing = ref(false);
|
||||||
const showConfirmModal = ref(false);
|
const showConfirmModal = ref(false);
|
||||||
const plexPopup = ref<Window | null>(null);
|
|
||||||
const pollInterval = ref<number | null>(null);
|
|
||||||
const currentPinId = ref<number | null>(null);
|
|
||||||
const plexUsername = ref<string>("");
|
const plexUsername = ref<string>("");
|
||||||
const plexUserData = ref<any>(null);
|
const plexUserData = ref<any>(null);
|
||||||
const isPlexConnected = ref<boolean>(false);
|
const isPlexConnected = ref<boolean>(false);
|
||||||
@@ -125,16 +118,14 @@
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
// Composables
|
// Composables
|
||||||
const { getCookie, setPlexAuthCookie } = usePlexAuth(
|
const { getCookie, setPlexAuthCookie, openAuthPopup, cleanup } =
|
||||||
CLIENT_IDENTIFIER,
|
usePlexAuth();
|
||||||
APP_NAME
|
|
||||||
);
|
|
||||||
const {
|
const {
|
||||||
fetchPlexUserData,
|
fetchPlexUserData,
|
||||||
fetchPlexServers,
|
fetchPlexServers,
|
||||||
fetchLibrarySections,
|
fetchLibrarySections,
|
||||||
fetchLibraryDetails
|
fetchLibraryDetails
|
||||||
} = usePlexApi(CLIENT_IDENTIFIER, APP_NAME);
|
} = usePlexApi();
|
||||||
const { loadLibraries } = usePlexLibraries();
|
const { loadLibraries } = usePlexLibraries();
|
||||||
|
|
||||||
// ----- Connection check -----
|
// ----- Connection check -----
|
||||||
@@ -219,71 +210,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ----- OAuth flow -----
|
// ----- 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) {
|
async function completePlexAuth(authToken: string) {
|
||||||
try {
|
try {
|
||||||
setPlexAuthCookie(authToken);
|
setPlexAuthCookie(authToken);
|
||||||
@@ -322,84 +248,21 @@
|
|||||||
|
|
||||||
async function authenticatePlex() {
|
async function authenticatePlex() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
const width = 600;
|
openAuthPopup(
|
||||||
const height = 700;
|
// onSuccess
|
||||||
const left = window.screen.width / 2 - width / 2;
|
async (authToken: string) => {
|
||||||
const top = window.screen.height / 2 - height / 2;
|
await completePlexAuth(authToken);
|
||||||
plexPopup.value = window.open(
|
},
|
||||||
"about:blank",
|
// onError
|
||||||
"PlexAuth",
|
(errorMessage: string) => {
|
||||||
`width=${width},height=${height},left=${left},top=${top}`
|
messages.value.push({
|
||||||
);
|
type: ErrorMessageTypes.Error,
|
||||||
if (!plexPopup.value) {
|
title: "Authentication failed",
|
||||||
messages.value.push({
|
message: errorMessage
|
||||||
type: ErrorMessageTypes.Error,
|
} as IErrorMessage);
|
||||||
title: "Popup blocked",
|
loading.value = false;
|
||||||
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(`
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Connecting to Plex...</title>
|
|
||||||
<style>
|
|
||||||
body{margin:0;padding:0;display:flex;justify-content:center;align-items:center;height:100vh;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#1c3a13;color:#fcfcf7;}
|
|
||||||
.loader{text-align:center;}
|
|
||||||
.spinner{border:4px solid rgba(252,252,247,0.3);border-top:4px solid #fcfcf7;border-radius:50%;width:40px;height:40px;animation:spin 1s linear infinite;margin:0 auto 20px;}
|
|
||||||
@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="loader">
|
|
||||||
<div class="spinner"></div>
|
|
||||||
<p>Connecting to Plex...</p>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, 500);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----- Unlink flow -----
|
// ----- Unlink flow -----
|
||||||
@@ -480,8 +343,7 @@
|
|||||||
loadPlexUserData();
|
loadPlexUserData();
|
||||||
});
|
});
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
stopPolling();
|
cleanup();
|
||||||
if (plexPopup.value && !plexPopup.value.closed) plexPopup.value.close();
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { ref } from "vue";
|
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);
|
"seasoned-plex-app-" + Math.random().toString(36).substring(7);
|
||||||
const APP_NAME = "Seasoned";
|
export const APP_NAME = "Seasoned";
|
||||||
|
|
||||||
export function usePlexApi() {
|
export function usePlexApi() {
|
||||||
const plexServerUrl = ref("");
|
const plexServerUrl = ref("");
|
||||||
@@ -184,8 +185,6 @@ export function usePlexApi() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
CLIENT_IDENTIFIER,
|
|
||||||
APP_NAME,
|
|
||||||
plexServerUrl,
|
plexServerUrl,
|
||||||
fetchPlexUserData,
|
fetchPlexUserData,
|
||||||
fetchPlexServers,
|
fetchPlexServers,
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { usePlexApi } from "./usePlexApi";
|
import { CLIENT_IDENTIFIER, APP_NAME } from "./usePlexApi";
|
||||||
|
|
||||||
export function usePlexAuth() {
|
export function usePlexAuth() {
|
||||||
const { CLIENT_IDENTIFIER, APP_NAME } = usePlexApi();
|
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const plexPopup = ref<Window | null>(null);
|
const plexPopup = ref<Window | null>(null);
|
||||||
const pollInterval = ref<number | null>(null);
|
const pollInterval = ref<number | null>(null);
|
||||||
|
|||||||
Reference in New Issue
Block a user