From 85a80b1c5b974274887b3d656bf3b3aff71d7318 Mon Sep 17 00:00:00 2001 From: Franciskid Date: Sun, 26 Jul 2026 20:30:00 +0200 Subject: [PATCH] re-check who the current user is instead of trusting the first answer The result of Users/Me was cached in a module level promise for the lifetime of the page. The web client is a single page app, so signing out or switching accounts never reloads the document and the script kept whichever user it saw first. Open the page as an admin, switch to a normal user in the same tab, and isAdministrator() still said yes: the admin-only ShareLink action was injected into that user's menus. The cached user and guest state are now keyed to ApiClient.getCurrentUserId(), so a switch invalidates them, and a non-admin verdict actively removes any action left in the DOM by the previous session instead of just declining to add one. Nothing was reachable through this. The create endpoint checks the Administrator role server side and answers 403 for a non-admin (confirmed against a real non-admin session), so the button was there but did nothing. --- Jellyfin.Plugin.ShareLinks/Web/sharelinks.js | 37 ++++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Plugin.ShareLinks/Web/sharelinks.js b/Jellyfin.Plugin.ShareLinks/Web/sharelinks.js index ab60134..182449f 100644 --- a/Jellyfin.Plugin.ShareLinks/Web/sharelinks.js +++ b/Jellyfin.Plugin.ShareLinks/Web/sharelinks.js @@ -2,14 +2,16 @@ var pluginId = '68540b76-ee74-436d-85ff-2abc884bbea6'; var copyLabel = 'Copy Stream URL'; var actionLabel = 'ShareLink'; - var clientVersion = '1.0.2-ui-1'; + var clientVersion = '1.0.2-ui-3'; var allowedItemStorageKey = 'sharelinks.allowedItemId'; var guestClassName = 'sharelinks-guest'; var hiddenAttr = 'data-sharelinks-hidden'; var injectedAttr = 'data-sharelinks-injected'; var configPromise = null; var userPromise = null; + var userPromiseUserId = null; var guestStatePromise = null; + var guestStatePromiseUserId = null; var booted = false; var scanQueued = false; var bootRetry = null; @@ -222,21 +224,41 @@ return configPromise; } + /** + * The web client is a single page app: signing out or switching accounts does + * not reload the document, so anything cached for "the current user" has to be + * keyed to the session it came from. Caching it for the lifetime of the page + * let an admin's verdict survive into the next user's session. + */ function getCurrentUser() { - if (!userPromise) { + var userId = currentApiUserId(); + if (!userPromise || userPromiseUserId !== userId) { + userPromiseUserId = userId; userPromise = apiGet('Users/Me').catch(function () { return null; }); } + return userPromise; } + function currentApiUserId() { + try { + return (window.ApiClient && ApiClient.getCurrentUserId && ApiClient.getCurrentUserId()) || ''; + } catch (error) { + return ''; + } + } + function getGuestState() { - if (!guestStatePromise) { + var userId = currentApiUserId(); + if (!guestStatePromise || guestStatePromiseUserId !== userId) { + guestStatePromiseUserId = userId; guestStatePromise = apiGet('ShareLinks/GuestState').catch(function () { return null; }); } + return guestStatePromise; } @@ -436,6 +458,9 @@ async function scanForMoreMenuActions() { var user = await getCurrentUser(); if (!isAdministrator(user)) { + // Take back anything injected for a previous session rather than only + // skipping: a switch inside the SPA leaves the old DOM in place. + removeInjectedActions(); return; } @@ -455,6 +480,12 @@ appendActionSection(container, itemId); } + function removeInjectedActions() { + Array.prototype.forEach.call(document.querySelectorAll('[' + injectedAttr + '="1"]'), function (node) { + node.remove(); + }); + } + /** * Records which item's "more" menu is about to open. The action sheet is a * detached, body-level element with no link back to the card or row it was