mirror of
https://github.com/goauthentik/authentik
synced 2026-04-25 17:15:26 +02:00
web/flows: prevent leader tab deadlock in continuous login flow (#21583)
* prevent leader tab deadlock in continuous login flow * web: Continuous login tidy. --------- Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
This commit is contained in:
@@ -17,16 +17,16 @@ export interface BroadcastMessage {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export class Broadcast extends BroadcastChannel {
|
||||
export class Broadcast extends BroadcastChannel implements Disposable {
|
||||
static shared = new Broadcast();
|
||||
|
||||
private discoveredTabIds = new Set<string>();
|
||||
exitedTabIds: string[] = [];
|
||||
protected discoveredTabIDs = new Set<string>();
|
||||
public exitedTabIDs: string[] = [];
|
||||
|
||||
#logger: Logger;
|
||||
protected logger: Logger;
|
||||
|
||||
#onMessage = (ev: MessageEvent<BroadcastMessage>) => {
|
||||
this.#logger.debug("broadcast event", ev.data);
|
||||
protected messageListener = (ev: MessageEvent<BroadcastMessage>) => {
|
||||
this.logger.debug("broadcast event", ev.data);
|
||||
switch (ev.data.type) {
|
||||
case BroadcastMessageType.discover:
|
||||
if (ev.data.sender === TabID.shared.current) {
|
||||
@@ -38,40 +38,50 @@ export class Broadcast extends BroadcastChannel {
|
||||
});
|
||||
return;
|
||||
case BroadcastMessageType.discoverReply:
|
||||
this.discoveredTabIds.add(ev.data.sender as string);
|
||||
this.discoveredTabIDs.add(ev.data.sender as string);
|
||||
return;
|
||||
case BroadcastMessageType.exit:
|
||||
this.exitedTabIds.push(ev.data.sender);
|
||||
this.exitedTabIDs.push(ev.data.sender);
|
||||
return;
|
||||
case BroadcastMessageType.continue:
|
||||
if (ev.data.target === TabID.shared.current) {
|
||||
this.#logger.debug("Continuing upon event");
|
||||
this.logger.debug("Continuing upon event");
|
||||
window.dispatchEvent(new CustomEvent("ak-multitab-continue"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
protected pageHideListener = () => {
|
||||
this.akExitTab();
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super(BROADCAST_CHANNEL_NAME);
|
||||
this.addEventListener("message", this.#onMessage);
|
||||
this.#logger = ConsoleLogger.prefix("mtab/broadcast");
|
||||
|
||||
this.addEventListener("message", this.messageListener);
|
||||
window.addEventListener("pagehide", this.pageHideListener);
|
||||
|
||||
this.logger = ConsoleLogger.prefix("mtab/broadcast");
|
||||
}
|
||||
|
||||
[Symbol.dispose]() {
|
||||
this.removeEventListener("message", this.#onMessage);
|
||||
this.removeEventListener("message", this.messageListener);
|
||||
}
|
||||
|
||||
async akTabDiscover(): Promise<Set<string>> {
|
||||
this.discoveredTabIds.clear();
|
||||
this.discoveredTabIDs.clear();
|
||||
|
||||
this.postMessage({
|
||||
type: BroadcastMessageType.discover,
|
||||
sender: TabID.shared.current,
|
||||
});
|
||||
|
||||
await new Promise<void>((r) => {
|
||||
setTimeout(r, 20);
|
||||
});
|
||||
return this.discoveredTabIds;
|
||||
|
||||
return this.discoveredTabIDs;
|
||||
}
|
||||
|
||||
akResumeTab(tabId: string) {
|
||||
|
||||
@@ -8,10 +8,9 @@ import { ConsoleLogger } from "#logger/browser";
|
||||
const lockKey = "authentik-tab-locked";
|
||||
const logger = ConsoleLogger.prefix("mtab/orchestrate");
|
||||
|
||||
const TAB_EXIT_TIMEOUT_MS = 3000;
|
||||
|
||||
export function multiTabOrchestrateLeave() {
|
||||
if (!globalAK().brand.flags.flowsContinuousLogin) {
|
||||
return;
|
||||
}
|
||||
Broadcast.shared.akExitTab();
|
||||
TabID.shared.clear();
|
||||
}
|
||||
@@ -20,35 +19,54 @@ export async function multiTabOrchestrateResume() {
|
||||
if (!globalAK().brand.flags.flowsContinuousLogin) {
|
||||
return;
|
||||
}
|
||||
const lockTabId = localStorage.getItem(lockKey);
|
||||
|
||||
const lockTabID = localStorage.getItem(lockKey);
|
||||
const tabs = await Broadcast.shared.akTabDiscover();
|
||||
|
||||
logger.debug("Got list of tabs", tabs);
|
||||
|
||||
if (lockTabId && tabs.has(lockTabId)) {
|
||||
if (lockTabID && tabs.has(lockTabID)) {
|
||||
logger.debug("Tabs locked, leaving.");
|
||||
multiTabOrchestrateLeave();
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug("Locking tabs");
|
||||
localStorage.setItem(lockKey, TabID.shared.current);
|
||||
|
||||
for (const tab of tabs) {
|
||||
logger.debug("Telling tab to continue", tab);
|
||||
Broadcast.shared.akResumeTab(tab);
|
||||
|
||||
const done = Promise.withResolvers<void>();
|
||||
const checker = setInterval(() => {
|
||||
if (Broadcast.shared.exitedTabIds.includes(tab)) {
|
||||
|
||||
let timeout = -1;
|
||||
|
||||
const checker = requestAnimationFrame(() => {
|
||||
if (Broadcast.shared.exitedTabIDs.includes(tab)) {
|
||||
logger.debug("tab exited", tab);
|
||||
setTimeout(() => {
|
||||
self.clearTimeout(timeout);
|
||||
|
||||
self.setTimeout(() => {
|
||||
logger.debug("continue exited", tab);
|
||||
done.resolve();
|
||||
}, 1000);
|
||||
clearInterval(checker);
|
||||
|
||||
cancelAnimationFrame(checker);
|
||||
}
|
||||
}, 1);
|
||||
});
|
||||
|
||||
timeout = self.setTimeout(() => {
|
||||
logger.warn("Timed out waiting for tab to exit, moving on", tab);
|
||||
cancelAnimationFrame(checker);
|
||||
done.resolve();
|
||||
}, TAB_EXIT_TIMEOUT_MS);
|
||||
|
||||
await done.promise;
|
||||
|
||||
logger.debug("Tab done, continuing", tab);
|
||||
}
|
||||
|
||||
logger.debug("All tabs done.");
|
||||
localStorage.removeItem(lockKey);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user