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.
Cette révision appartient à :
Franciskid
2026-07-26 20:30:00 +02:00
Parent 35631eae46
révision 85a80b1c5b
+34 -3
Voir le fichier
@@ -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