Skip to content

Commit 672a5e7

Browse files
committed
handle log out
1 parent da64524 commit 672a5e7

File tree

4 files changed

+57
-29
lines changed

4 files changed

+57
-29
lines changed

frontend/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1079,6 +1079,7 @@ export class App extends BtrixElement {
10791079

10801080
private clearUser() {
10811081
this.authService.logout();
1082+
this.authService.finalize();
10821083
this.authService = new AuthService();
10831084
AppStateService.resetUser();
10841085
}

frontend/src/utils/AuthService.ts

Lines changed: 47 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,11 @@
1-
import { nanoid } from "nanoid";
2-
import { z } from "zod";
3-
41
import { APIError } from "./api";
2+
import { STORAGE_KEY_PREFIX } from "./persist";
53
import { urlForName } from "./router";
64
import appState, { AppStateService } from "./state";
75

86
import type { APIUser } from "@/index";
97
import type { Auth } from "@/types/auth";
108

11-
const sessionIdSchema = z.string().nanoid();
12-
type SessionId = z.infer<typeof sessionIdSchema>;
139
type AuthState = Auth | null;
1410
type JWT = {
1511
user_id: string;
@@ -32,21 +28,16 @@ export type LogOutEventDetail = {
3228
redirect?: boolean;
3329
};
3430

35-
type AuthMessage = {
36-
responderId?: SessionId;
37-
requesterId?: SessionId;
38-
};
39-
40-
type AuthRequestEventDetail = AuthMessage & {
31+
type AuthRequestEventDetail = {
4132
name: "requesting_auth";
4233
};
4334

44-
type AuthResponseEventDetail = AuthMessage & {
35+
type AuthResponseEventDetail = {
4536
name: "responding_auth";
4637
auth: AuthState;
4738
};
4839

49-
export type AuthReceivedEventDetail = AuthMessage & {
40+
export type AuthReceivedEventDetail = {
5041
name: "auth_received";
5142
};
5243

@@ -66,8 +57,6 @@ const FRESHNESS_TIMER_INTERVAL = 60 * 1000 * 5;
6657

6758
export default class AuthService {
6859
private timerId?: number;
69-
private readonly sessionId: SessionId = nanoid();
70-
private broadcastChannel?: BroadcastChannel;
7160

7261
static storageKey = "btrix.auth";
7362
static unsupportedAuthErrorCode = "UNSUPPORTED_AUTH_TYPE";
@@ -90,6 +79,9 @@ export default class AuthService {
9079
},
9180
};
9281

82+
private broadcastChannel?: BroadcastChannel;
83+
private readonly finalizeController = new AbortController();
84+
9385
get authState() {
9486
return appState.auth;
9587
}
@@ -221,7 +213,6 @@ export default class AuthService {
221213
const broadcastPromise = new Promise<AuthState>((resolve) => {
222214
// Check if there's any authenticated tabs
223215
this.broadcastChannel?.postMessage({
224-
requesterId: this.sessionId,
225216
name: "requesting_auth",
226217
} satisfies AuthRequestEventDetail);
227218
// Wait for another tab to respond
@@ -231,10 +222,9 @@ export default class AuthService {
231222

232223
// Confirm receipt
233224
this.broadcastChannel?.postMessage({
234-
requesterId: this.sessionId,
235-
responderId: data.responderId,
236225
name: "auth_received",
237226
} satisfies AuthReceivedEventDetail);
227+
238228
resolve(data.auth);
239229
}
240230
};
@@ -265,17 +255,48 @@ export default class AuthService {
265255
}
266256

267257
constructor() {
258+
const { signal } = this.finalizeController;
259+
268260
this.broadcastChannel = new BroadcastChannel(AuthService.storageKey);
269261

270262
// Only have freshness check run in visible tab(s)
271-
document.addEventListener("visibilitychange", () => {
272-
if (!this.authState) return;
273-
if (document.visibilityState === "visible") {
274-
this.startFreshnessCheck();
275-
} else {
276-
this.cancelFreshnessCheck();
277-
}
278-
});
263+
document.addEventListener(
264+
"visibilitychange",
265+
() => {
266+
if (!this.authState) return;
267+
if (document.visibilityState === "visible") {
268+
this.startFreshnessCheck();
269+
} else {
270+
this.cancelFreshnessCheck();
271+
}
272+
},
273+
{ signal },
274+
);
275+
276+
/**
277+
* Assume another tab has logged out if `orgSlug` preference is removed
278+
* See FIXME note in state.ts
279+
*/
280+
window.addEventListener(
281+
"storage",
282+
(e: StorageEvent) => {
283+
if (!this.authState) return;
284+
285+
if (
286+
e.key === `${STORAGE_KEY_PREFIX}.orgSlug` &&
287+
e.oldValue &&
288+
!e.newValue
289+
) {
290+
this.revoke();
291+
}
292+
},
293+
{ signal },
294+
);
295+
}
296+
297+
finalize() {
298+
this.finalizeController.abort();
299+
this.stopSharingSession();
279300
}
280301

281302
saveLogin(auth: Auth) {
@@ -327,7 +348,6 @@ export default class AuthService {
327348
if (auth) {
328349
// A new tab/window opened and is requesting shared auth
329350
this.broadcastChannel?.postMessage({
330-
responderId: this.sessionId,
331351
name: "responding_auth",
332352
auth: AuthService.getCurrentTabAuth(),
333353
} satisfies AuthResponseEventDetail);

frontend/src/utils/persist.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type {
77
StateVar,
88
} from "lit-shared-state";
99

10-
const STORAGE_KEY_PREFIX = "btrix.app";
10+
export const STORAGE_KEY_PREFIX = "btrix.app";
1111

1212
type ExpiringValue = {
1313
value: unknown;

frontend/src/utils/state.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,14 @@ export function makeAppStateService() {
3939
auth: Auth | null = null;
4040

4141
// Store user's org slug preference in local storage in order to redirect
42-
// to the most recently visited org on next log in
42+
// to the most recently visited org on next log in.
43+
//
44+
// FIXME Since the org slug preference is removed on log out, AuthService
45+
// currently checks whether `orgSlug` is being removed in a `storage`
46+
// event to determine whether another tab has logged out.
47+
// It's not the cleanest solution to use `orgSlug` as a cross-tab logout
48+
// event, so we may want to refactor this in the future.
49+
//
4350
// TODO move to `userPreferences`
4451
@options(persist(window.localStorage))
4552
orgSlug: string | null = null;

0 commit comments

Comments
 (0)