diff --git a/src/client/ApiSchemas.ts b/src/client/ApiSchemas.ts new file mode 100644 index 000000000..2aa164048 --- /dev/null +++ b/src/client/ApiSchemas.ts @@ -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; diff --git a/src/client/Main.ts b/src/client/Main.ts index 0d00af662..3769f3b3d 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -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 { + 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}`; +} diff --git a/src/client/index.html b/src/client/index.html index 300505883..2a91c977b 100644 --- a/src/client/index.html +++ b/src/client/index.html @@ -212,6 +212,13 @@
+ +