feat: sign out all operator sessions (#287)
CI / lint (pull_request) Successful in 28s
CI / build-frontend (pull_request) Successful in 5s

This commit is contained in:
timmy committed 2026-08-08 09:55:34 +00:00
1 parent 94cbde3cf9
commit 37d55e9b20
9 files changed
+195 -9

No files matched your search

+2
View File
@@ -14,6 +14,7 @@ html, body { height: 100%; margin: 0; background: var(--bg); color: var(--text);
header { position: sticky; top: 0; z-index: 20; padding: 12px 16px; display:flex; gap:16px; align-items:center; justify-content:space-between; background: linear-gradient(180deg, rgba(11,21,38,.95), rgba(11,21,38,.55), transparent); backdrop-filter: blur(4px); border-bottom: 1px solid #1b2d45; }
.toolbar { display:flex; gap:10px; align-items:center; flex-wrap:wrap; }
button { background: linear-gradient(180deg,#1f3a5f,#15324d); border:1px solid #2a496e; color:#e5e7eb; padding:8px 12px; border-radius:10px; cursor:pointer; }
#sign-out-all { min-height:44px; }
button:hover { filter: brightness(1.15); }
.panel { border: 1px solid #1b2d45; border-radius: 14px; padding: 12px; background: rgba(11,21,38,.92); }
.panel > summary { cursor: pointer; list-style-position: inside; }
@@ -298,6 +299,7 @@ textarea { resize: vertical; min-height: 120px; }
<div class="status"><span class="dot"></span><span class="small" id="status">Live</span></div>
<button id="refresh">Refresh</button>
<button id="sign-out" type="button">Sign out &amp; clear this device</button>
<button id="sign-out-all" type="button">Sign out all devices</button>
<button id="open-palette">Command</button>
<span class="small muted" id="clock"></span>
</div>
+15 -2
View File
@@ -14,12 +14,15 @@
caches: root.caches,
serviceWorker: root.navigator?.serviceWorker,
location: root.location,
confirmAction: message => root.confirm(message),
onExpired: () => root.dispatchEvent(new CustomEvent('stackchain:session-expired')),
});
root.fetch = boundary.fetch;
const attach = () => {
const button = root.document.getElementById('sign-out');
if (button) button.addEventListener('click', () => boundary.signOut());
const allDevicesButton = root.document.getElementById('sign-out-all');
if (allDevicesButton) allDevicesButton.addEventListener('click', () => boundary.signOutAllDevices());
boundary.resumeQueuedWork();
};
if (root.document.readyState === 'loading') root.document.addEventListener('DOMContentLoaded', attach);
@@ -27,7 +30,7 @@
root.stackchainSession = boundary;
}
})(typeof window !== 'undefined' ? window : this, function createSessionBoundary({
cookie, origin, base, fetchImpl, localStorage, sessionStorage, indexedDB, caches, serviceWorker, location,
cookie, origin, base, fetchImpl, localStorage, sessionStorage, indexedDB, caches, serviceWorker, location, confirmAction,
onExpired = () => {},
}) {
const SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']);
@@ -90,6 +93,16 @@
}
}
async function signOutAllDevices() {
const confirmed = confirmAction?.('Sign out every device? You will need to sign in again everywhere.');
if (!confirmed) return false;
const response = await sessionFetch(base + 'api/v1/sessions', { method: 'DELETE' });
if (!response.ok) throw new Error('Could not sign out all devices');
await clearPrivateDeviceData();
location.assign(base + 'login');
return true;
}
async function resumeQueuedWork() {
try {
const registration = await serviceWorker?.ready;
@@ -97,5 +110,5 @@
} catch (_error) { /* Background Sync is optional; foreground delivery remains available. */ }
}
return { fetch: sessionFetch, signOut, clearPrivateDeviceData, resumeQueuedWork };
return { fetch: sessionFetch, signOut, signOutAllDevices, clearPrivateDeviceData, resumeQueuedWork };
});