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>
This commit is contained in:
Scott Anderson
2025-05-05 18:48:12 -04:00
committed by GitHub
parent c97eca0ed2
commit 8479fab49d
4 changed files with 74 additions and 19 deletions
+1
View File
@@ -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",
+39 -16
View File
@@ -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;
+8
View File
@@ -219,6 +219,14 @@
block
></o-button>
<o-button
id="logout-discord"
title="Log out"
translationKey="main.log_out"
visible="false"
block
></o-button>
<div class="container__row">
<flag-input class="w-[20%] md:w-[15%]"></flag-input>
<username-input class="w-full"></username-input>
+26 -3
View File
@@ -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