Discord login (#611)

## Description:

## Please complete the following:

- [ ] I have added screenshots for all UI updates
- [ ] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
- [ ] I understand that submitting code with bugs that could have been
caught through manual testing blocks releases and new features for all
contributors

## Please put your Discord username so you can be contacted if a bug or
regression is found:

<DISCORD USERNAME>

---------

Co-authored-by: Scott Anderson <662325+scottanderson@users.noreply.github.com>
This commit is contained in:
Scott Anderson
2025-04-28 21:31:42 -04:00
committed by GitHub
parent a36fdcaaed
commit cb9a97083a
3 changed files with 90 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
import { z } from "zod";
export const UserMeResponseSchema = z.object({
user: z.object({
id: z.string(),
avatar: z.string(),
username: z.string(),
global_name: z.string(),
discriminator: z.string(),
locale: z.string(),
}),
});
export type UserMeResponse = z.infer<typeof UserMeResponseSchema>;
+70
View File
@@ -5,6 +5,7 @@ import { GameRecord, GameStartInfo } from "../core/Schemas";
import { getServerConfigFromClient } from "../core/configuration/ConfigLoader";
import { GameType } from "../core/game/Game";
import { UserSettings } from "../core/game/UserSettings";
import { UserMeResponse, UserMeResponseSchema } from "./ApiSchemas";
import { joinLobby } from "./ClientGameRunner";
import "./DarkModeButton";
import { DarkModeButton } from "./DarkModeButton";
@@ -58,6 +59,21 @@ class Client {
constructor() {}
initialize(): void {
const { hash } = window.location;
if (hash.startsWith("#")) {
const params = new URLSearchParams(hash.slice(1));
const token = params.get("token");
if (token) {
localStorage.setItem("token", token);
}
// Clean the URL
history.replaceState(
null,
"",
window.location.pathname + window.location.search,
);
}
const langSelector = document.querySelector(
"lang-selector",
) as LangSelector;
@@ -90,6 +106,22 @@ class Client {
consolex.warn("Random name button element not found");
}
const loginDiscordButton = document.getElementById("login-discord");
isLoggedIn().then((loggedIn) => {
if (loggedIn !== false) {
console.log("Logged in", JSON.stringify(loggedIn, null, 2));
const { user } = loggedIn;
const { id, avatar, username, global_name, discriminator } = user;
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
} else {
localStorage.removeItem("token");
loginDiscordButton.addEventListener("click", discordLogin);
}
});
this.usernameInput = document.querySelector(
"username-input",
) as UsernameInput;
@@ -283,6 +315,32 @@ document.addEventListener("DOMContentLoaded", () => {
new Client().initialize();
});
async function isLoggedIn(): Promise<UserMeResponse | false> {
try {
const token = localStorage.getItem("token");
if (!token) return false;
const response = await fetch(getApiBase() + "/users/@me", {
headers: {
authorization: `Bearer ${token}`,
},
});
if (response.status !== 200) return false;
const body = await response.json();
const result = UserMeResponseSchema.safeParse(body);
if (!result.success) {
console.error(
"Invalid response",
JSON.stringify(body),
JSON.stringify(result.error),
);
return false;
}
return result.data;
} catch (e) {
return false;
}
}
function setFavicon(): void {
const link = document.createElement("link");
link.type = "image/x-icon";
@@ -316,3 +374,15 @@ export function getPersistentIDFromCookie(): string {
return newID;
}
function getApiBase() {
const { hostname } = new URL(window.location.href);
const domainname = hostname.split(".").slice(-2).join(".");
return domainname === "localhost"
? "http://localhost:8787"
: `https://api.${domainname}`;
}
function discordLogin() {
window.location.href = `${getApiBase()}/login/discord?redirect_uri=${window.location.href}`;
}
+7
View File
@@ -212,6 +212,13 @@
<!-- Main container with responsive padding -->
<main class="flex justify-center flex-grow">
<div class="container pt-12">
<o-button
id="login-discord"
title="Login"
translationKey="main.login_discord"
block
></o-button>
<div class="container__row">
<flag-input class="w-[20%] md:w-[15%]"></flag-input>
<username-input class="w-full"></username-input>