From 179eb6f72fcbcb8a5fb754203a4440c7a758cde2 Mon Sep 17 00:00:00 2001 From: oleksandr-shysh Date: Mon, 16 Jun 2025 11:49:23 +0300 Subject: [PATCH] Move the jwt logic to an interface, rename the api base url env var --- src/client/HostLobbyModal.ts | 2 +- src/client/JoinPrivateLobbyModal.ts | 2 +- src/client/PublicLobby.ts | 2 +- src/client/jwt.ts | 154 ++++++++++++++++--------- src/core/configuration/ConfigLoader.ts | 2 +- src/server/cors.ts | 5 - webpack.config.js | 7 +- 7 files changed, 105 insertions(+), 69 deletions(-) diff --git a/src/client/HostLobbyModal.ts b/src/client/HostLobbyModal.ts index e0564a92e..6053e5a6c 100644 --- a/src/client/HostLobbyModal.ts +++ b/src/client/HostLobbyModal.ts @@ -592,6 +592,6 @@ export async function buildGameUrl( const config = await getServerConfigFromClient(); const apiPath = `/api/${path}/${gameID}`; - const baseUrl = process.env.API_BASE_URL || ""; + const baseUrl = process.env.APP_BASE_URL || ""; return `${baseUrl}/${config.workerPath(gameID)}${apiPath}`; } diff --git a/src/client/JoinPrivateLobbyModal.ts b/src/client/JoinPrivateLobbyModal.ts index e50d4b0d0..b9b2fa93b 100644 --- a/src/client/JoinPrivateLobbyModal.ts +++ b/src/client/JoinPrivateLobbyModal.ts @@ -173,7 +173,7 @@ export class JoinPrivateLobbyModal extends LitElement { private async checkActiveLobby(lobbyId: string): Promise { const config = await getServerConfigFromClient(); - const url = `${process.env.API_BASE_URL || ""}/${config.workerPath(lobbyId)}/api/game/${lobbyId}/exists`; + const url = `${process.env.APP_BASE_URL || ""}/${config.workerPath(lobbyId)}/api/game/${lobbyId}/exists`; const response = await fetch(url, { method: "GET", diff --git a/src/client/PublicLobby.ts b/src/client/PublicLobby.ts index 1b8408b43..69fae6cb4 100644 --- a/src/client/PublicLobby.ts +++ b/src/client/PublicLobby.ts @@ -57,7 +57,7 @@ export class PublicLobby extends LitElement { async fetchLobbies(): Promise { try { const response = await fetch( - `${process.env.API_BASE_URL}/api/public_lobbies`, + `${process.env.APP_BASE_URL}/api/public_lobbies`, ); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); diff --git a/src/client/jwt.ts b/src/client/jwt.ts index 58cd6126d..b9403c6ec 100644 --- a/src/client/jwt.ts +++ b/src/client/jwt.ts @@ -11,8 +11,74 @@ import { UserMeResponseSchema, } from "../core/ApiSchemas"; -const isNative = Capacitor.getPlatform() !== "web"; -const NATIVE_REDIRECT_URI = `${process.env.API_BASE_URL}/discord-redirect.html`; +interface Platform { + getRedirectUri(): string; + setLocation(url: string): Promise | void; + getApiBaseForLocalhost(): string; + initializeAuthListener(): void; +} + +class BrowserPlatform implements Platform { + getRedirectUri(): string { + return window.location.href.split("#")[0]; + } + + setLocation(url: string): void { + window.location.href = url; + } + + getApiBaseForLocalhost(): string { + return localStorage.getItem("apiHost") ?? "http://localhost:8787"; + } + + initializeAuthListener(): void { + // No-op for web + } +} + +class CapacitorPlatform implements Platform { + getRedirectUri(): string { + return `${process.env.APP_BASE_URL}/discord-redirect.html`; + } + + async setLocation(url: string): Promise { + await Browser.open({ url }); + } + + getApiBaseForLocalhost(): string { + return process.env.APP_BASE_URL + ? process.env.APP_BASE_URL!.replace("9000", "8787") + : "http://localhost:8787"; + } + + initializeAuthListener(): void { + App.addListener("appUrlOpen", async (data) => { + try { + const url = new URL(data.url); + if (handleToken(url, false)) { + __isLoggedIn = undefined; // Force re-evaluation + await Browser.close(); + window.location.assign(window.location.origin || "/"); + return; + } + + const error = url.search; + if (error) { + console.error(`Error from auth provider: ${error}`); + } + await Browser.close(); + } catch (e) { + console.error("Error handling appUrlOpen", e); + await Browser.close(); + } + }); + } +} + +const platform: Platform = + Capacitor.getPlatform() !== "web" + ? new CapacitorPlatform() + : new BrowserPlatform(); function getAudience() { const hostname = @@ -25,24 +91,34 @@ function getAudience() { function getApiBase() { const domainname = getAudience(); return domainname === "localhost" - ? (localStorage.getItem("apiHost") ?? - (isNative && process.env.API_BASE_URL)) - ? process.env.API_BASE_URL!.replace("9000", "8787") - : "http://localhost:8787" + ? platform.getApiBaseForLocalhost() : `https://api.${domainname}`; } -function getToken(): string | null { - 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); - params.delete("token"); - params.toString(); +function handleToken(url: URL, isFromHash: boolean): boolean { + let token: string | null = null; + if (isFromHash) { + if (url.hash.startsWith("#")) { + const params = new URLSearchParams(url.hash.slice(1)); + token = params.get("token"); } + } else { + token = url.searchParams.get("token"); + } + + if (token) { + localStorage.setItem("token", token); + return true; + } + return false; +} + +function getToken(): string | null { + const url = new URL(window.location.href); + if (handleToken(url, true)) { // Clean the URL + const params = new URLSearchParams(url.hash.slice(1)); + params.delete("token"); history.replaceState( null, "", @@ -55,21 +131,11 @@ function getToken(): string | null { } export async function discordLogin() { - let redirectUri: string; - - if (isNative) { - redirectUri = NATIVE_REDIRECT_URI; - } else { - redirectUri = window.location.href.split("#")[0]; - } - - const url = `${getApiBase()}/login/discord?redirect_uri=${encodeURIComponent(redirectUri)}`; - - if (isNative) { - await Browser.open({ url }); - } else { - window.location.href = url; - } + const redirectUri = platform.getRedirectUri(); + const url = `${getApiBase()}/login/discord?redirect_uri=${encodeURIComponent( + redirectUri, + )}`; + await platform.setLocation(url); } export async function logOut(allSessions: boolean = false) { @@ -126,7 +192,7 @@ function _isLoggedIn(): IsLoggedInResponse { const payload = decodeJwt(token); const { iss, aud, exp, iat } = payload; - if (iss !== getApiBase() && !isNative) { + if (iss !== getApiBase()) { // JWT was not issued by the correct server console.error( 'unexpected "iss" claim value', @@ -184,31 +250,7 @@ function _isLoggedIn(): IsLoggedInResponse { } export function initializeAuthListener() { - if (Capacitor.getPlatform() === "web") return; - - App.addListener("appUrlOpen", async (data) => { - try { - const url = new URL(data.url); - const token = url.searchParams.get("token"); - - if (token) { - localStorage.setItem("token", token); - __isLoggedIn = undefined; // Force re-evaluation - await Browser.close(); - window.location.assign(window.location.origin || "/"); - return; - } - - const error = url.search; - if (error) { - console.error(`Error from auth provider: ${error}`); - } - await Browser.close(); - } catch (e) { - console.error("Error handling appUrlOpen", e); - await Browser.close(); - } - }); + platform.initializeAuthListener(); } export async function postRefresh(): Promise { diff --git a/src/core/configuration/ConfigLoader.ts b/src/core/configuration/ConfigLoader.ts index a459020d2..d10d81568 100644 --- a/src/core/configuration/ConfigLoader.ts +++ b/src/core/configuration/ConfigLoader.ts @@ -29,7 +29,7 @@ export async function getServerConfigFromClient(): Promise { if (cachedSC) { return cachedSC; } - const response = await fetch(`${process.env.API_BASE_URL}/api/env`); + const response = await fetch(`${process.env.APP_BASE_URL}/api/env`); if (!response.ok) { throw new Error( diff --git a/src/server/cors.ts b/src/server/cors.ts index 4bb1004c0..e22c0935e 100644 --- a/src/server/cors.ts +++ b/src/server/cors.ts @@ -1,5 +1,4 @@ import cors from "cors"; -import { getLocalIP } from "../../webpack.config"; import { GameEnv } from "../core/configuration/Config"; import { getServerConfigFromServer } from "../core/configuration/ConfigLoader"; @@ -21,10 +20,6 @@ switch (config.env()) { "http://localhost", "http://localhost:8787", ); - const localIp = getLocalIP(); - if (localIp) { - allowedOrigins.push(`http://${localIp}:9000`); - } break; } } diff --git a/webpack.config.js b/webpack.config.js index 2301b83cb..ae5eb29fd 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -30,8 +30,7 @@ const gitCommit = export default async (env, argv) => { const isProduction = argv.mode === "production"; - // Note: when running capacitor apps locally, run `npm run dev` before building the app to avoid env vars being overridden - const apiBaseUrl = process.env.CAPACITOR_BUILD + const appBaseUrl = process.env.CAPACITOR_BUILD ? isProduction && process.env.CAPACITOR_PRODUCTION_HOSTNAME ? `https://${process.env.CAPACITOR_PRODUCTION_HOSTNAME}` : `http://${getLocalIP()}:9000` @@ -159,11 +158,11 @@ export default async (env, argv) => { }), new webpack.DefinePlugin({ "process.env.WEBSOCKET_URL": JSON.stringify( - apiBaseUrl.split("://")[1], // remove protocol + appBaseUrl.split("://")[1], // remove protocol ), "process.env.GAME_ENV": JSON.stringify(isProduction ? "prod" : "dev"), "process.env.GIT_COMMIT": JSON.stringify(gitCommit), - "process.env.API_BASE_URL": JSON.stringify(apiBaseUrl), + "process.env.APP_BASE_URL": JSON.stringify(appBaseUrl), "process.env.CAPACITOR_PRODUCTION_HOSTNAME": JSON.stringify( process.env.CAPACITOR_PRODUCTION_HOSTNAME, ),