From 8479fab49d1e3663836fafc4724e9bd0fcd8186c Mon Sep 17 00:00:00 2001 From: Scott Anderson Date: Mon, 5 May 2025 18:48:12 -0400 Subject: [PATCH] Add logout button (#653) ## Description: - Add a logout button, and post to the `/logout` endpoint to end the session server-side. - Add an option to log out all sessions by calling the `/revoke` endpoint. There is button for this yet. - Depends on https://github.com/openfrontio/infra/pull/32 - This is the last part of #559 ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors --------- Co-authored-by: Scott Anderson <662325+scottanderson@users.noreply.github.com> --- resources/lang/en.json | 1 + src/client/Main.ts | 55 ++++++++++++++++++++++++++++++------------ src/client/index.html | 8 ++++++ src/client/jwt.ts | 29 +++++++++++++++++++--- 4 files changed, 74 insertions(+), 19 deletions(-) diff --git a/resources/lang/en.json b/resources/lang/en.json index f38dd2828..62a7153d7 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -10,6 +10,7 @@ "join_discord": "Join the Discord!", "login_discord": "Login with Discord", "logged_in": "Logged in!", + "log_out": "Log out", "create_lobby": "Create Lobby", "join_lobby": "Join Lobby", "single_player": "Single Player", diff --git a/src/client/Main.ts b/src/client/Main.ts index ef6251560..eb6bb3631 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -31,7 +31,7 @@ import { generateCryptoRandomUUID } from "./Utils"; import "./components/baseComponents/Button"; import { OButton } from "./components/baseComponents/Button"; import "./components/baseComponents/Modal"; -import { discordLogin, isLoggedIn } from "./jwt"; +import { discordLogin, getUserMe, isLoggedIn, logOut } from "./jwt"; import "./styles.css"; export interface JoinLobbyEvent { @@ -95,21 +95,9 @@ class Client { const loginDiscordButton = document.getElementById( "login-discord", ) as OButton; - const claims = isLoggedIn(); - if (claims === false) { - // Not logged in - loginDiscordButton.disable = false; - loginDiscordButton.translationKey = "main.login_discord"; - loginDiscordButton.addEventListener("click", discordLogin); - } else { - // Logged in - loginDiscordButton.disable = true; - loginDiscordButton.translationKey = "main.logged_in"; - // const avatarUrl = avatar - // ? `https://cdn.discordapp.com/avatars/${id}/${avatar}.${avatar.startsWith("a_") ? "gif" : "png"}` - // : `https://cdn.discordapp.com/embed/avatars/${Number(discriminator) % 5}.png`; - // TODO: Update the page for logged in user - } + const logoutDiscordButton = document.getElementById( + "logout-discord", + ) as OButton; this.usernameInput = document.querySelector( "username-input", @@ -150,6 +138,41 @@ class Client { hlpModal.open(); }); + const claims = isLoggedIn(); + if (claims === false) { + // Not logged in + loginDiscordButton.disable = false; + loginDiscordButton.translationKey = "main.login_discord"; + loginDiscordButton.addEventListener("click", discordLogin); + logoutDiscordButton.hidden = true; + } else { + // JWT appears to be valid, assume we are logged in + loginDiscordButton.disable = true; + loginDiscordButton.translationKey = "main.logged_in"; + logoutDiscordButton.hidden = false; + logoutDiscordButton.addEventListener("click", () => { + // Log out + logOut(); + loginDiscordButton.disable = false; + loginDiscordButton.translationKey = "main.login_discord"; + loginDiscordButton.addEventListener("click", discordLogin); + logoutDiscordButton.hidden = true; + }); + // Look up the discord user object. + // TODO: Add caching + getUserMe().then((userMeResponse) => { + if (userMeResponse === false) { + // Not logged in + loginDiscordButton.disable = false; + loginDiscordButton.translationKey = "main.login_discord"; + loginDiscordButton.addEventListener("click", discordLogin); + logoutDiscordButton.hidden = true; + return; + } + // TODO: Update the page for logged in user + }); + } + const settingsModal = document.querySelector( "user-setting", ) as UserSettingModal; diff --git a/src/client/index.html b/src/client/index.html index da463d4dd..d1101a0fc 100644 --- a/src/client/index.html +++ b/src/client/index.html @@ -219,6 +219,14 @@ block > + +
diff --git a/src/client/jwt.ts b/src/client/jwt.ts index 5a6d245e7..21f25f8e5 100644 --- a/src/client/jwt.ts +++ b/src/client/jwt.ts @@ -42,6 +42,29 @@ export function discordLogin() { window.location.href = `${getApiBase()}/login/discord?redirect_uri=${window.location.href}`; } +export async function logOut(allSessions: boolean = false) { + const token = localStorage.getItem("token"); + if (token === null) return; + localStorage.removeItem("token"); + __isLoggedIn = false; + + const response = await fetch( + getApiBase() + allSessions ? "/revoke" : "/logout", + { + method: "POST", + headers: { + authorization: `Bearer ${token}`, + }, + }, + ); + + if (response.ok === false) { + console.error("Logout failed", response); + return false; + } + return true; +} + let __isLoggedIn: TokenPayload | false | undefined = undefined; export function isLoggedIn(): TokenPayload | false { if (__isLoggedIn === undefined) { @@ -76,7 +99,7 @@ export function _isLoggedIn(): TokenPayload | false { 'unexpected "iss" claim value', // JSON.stringify(payload, null, 2), ); - localStorage.removeItem("token"); + logOut(); return false; } if (aud !== getAudience()) { @@ -85,7 +108,7 @@ export function _isLoggedIn(): TokenPayload | false { 'unexpected "aud" claim value', // JSON.stringify(payload, null, 2), ); - localStorage.removeItem("token"); + logOut(); return false; } const now = Math.floor(Date.now() / 1000); @@ -95,7 +118,7 @@ export function _isLoggedIn(): TokenPayload | false { 'after "exp" claim value', // JSON.stringify(payload, null, 2), ); - localStorage.removeItem("token"); + logOut(); return false; } const refreshAge: number = 6 * 3600; // 6 hours