mirror of
https://github.com/KevinMidboe/seasoned.git
synced 2026-03-11 20:05:39 +00:00
Refactor: Extract Plex composables and smaller components
Split large PlexSettings component into reusable pieces: Composables: - usePlexApi.ts: API functions for user data, servers, libraries - usePlexAuth.ts: OAuth authentication flow, PIN generation, polling Components: - PlexAuthButton.vue: Sign-in button with OAuth popup - PlexProfileCard.vue: User profile with badges (Pass, 2FA, Labs, years) Benefits: - Better code organization and maintainability - Reusable authentication logic - Cleaner separation of concerns - Easier testing and debugging
This commit is contained in:
199
src/composables/usePlexAuth.ts
Normal file
199
src/composables/usePlexAuth.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import { ref } from "vue";
|
||||
import { usePlexApi } from "./usePlexApi";
|
||||
|
||||
export function usePlexAuth() {
|
||||
const { CLIENT_IDENTIFIER, APP_NAME } = usePlexApi();
|
||||
|
||||
const loading = ref(false);
|
||||
const plexPopup = ref<Window | null>(null);
|
||||
const pollInterval = ref<number | null>(null);
|
||||
|
||||
// Generate a PIN for Plex OAuth
|
||||
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");
|
||||
const data = await response.json();
|
||||
return { id: data.id, code: data.code };
|
||||
} catch (error) {
|
||||
console.error("[PlexAuth] Error generating PIN:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Check PIN status
|
||||
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("[PlexAuth] Error checking PIN:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Construct auth URL
|
||||
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()}`;
|
||||
}
|
||||
|
||||
// Start polling for PIN
|
||||
function startPolling(
|
||||
pinId: number,
|
||||
pinCode: string,
|
||||
onSuccess: (token: string) => void
|
||||
) {
|
||||
pollInterval.value = window.setInterval(async () => {
|
||||
const authToken = await checkPin(pinId, pinCode);
|
||||
if (authToken) {
|
||||
stopPolling();
|
||||
if (plexPopup.value && !plexPopup.value.closed) {
|
||||
plexPopup.value.close();
|
||||
}
|
||||
onSuccess(authToken);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Stop polling
|
||||
function stopPolling() {
|
||||
if (pollInterval.value) {
|
||||
clearInterval(pollInterval.value);
|
||||
pollInterval.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Set cookie
|
||||
function setPlexAuthCookie(authToken: string) {
|
||||
const expires = new Date();
|
||||
expires.setDate(expires.getDate() + 30);
|
||||
document.cookie = `plex_auth_token=${authToken}; path=/; expires=${expires.toUTCString()}; SameSite=Strict`;
|
||||
}
|
||||
|
||||
// Get cookie
|
||||
function getCookie(name: string): string | null {
|
||||
const value = `; ${document.cookie}`;
|
||||
const parts = value.split(`; ${name}=`);
|
||||
if (parts.length === 2) {
|
||||
return parts.pop()?.split(";").shift() || null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Open authentication popup
|
||||
async function openAuthPopup(
|
||||
onSuccess: (token: string) => void,
|
||||
onError: (msg: string) => void
|
||||
) {
|
||||
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) {
|
||||
onError("Please allow popups for this site to authenticate with Plex");
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Add loading screen
|
||||
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; }
|
||||
.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();
|
||||
onError("Could not generate Plex authentication PIN");
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const authUrl = constructAuthUrl(pin.code);
|
||||
if (plexPopup.value && !plexPopup.value.closed) {
|
||||
plexPopup.value.location.href = authUrl;
|
||||
} else {
|
||||
onError("Authentication window was closed");
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
startPolling(pin.id, pin.code, onSuccess);
|
||||
|
||||
// Check if popup closed
|
||||
const popupChecker = setInterval(() => {
|
||||
if (plexPopup.value && plexPopup.value.closed) {
|
||||
clearInterval(popupChecker);
|
||||
stopPolling();
|
||||
if (loading.value) {
|
||||
loading.value = false;
|
||||
onError("Plex authentication window was closed");
|
||||
}
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
function cleanup() {
|
||||
stopPolling();
|
||||
if (plexPopup.value && !plexPopup.value.closed) {
|
||||
plexPopup.value.close();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
loading,
|
||||
setPlexAuthCookie,
|
||||
getCookie,
|
||||
openAuthPopup,
|
||||
cleanup
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user