mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-24 13:14:45 +00:00
Handle Discord Login in capacitor apps
This commit is contained in:
Generated
+38
@@ -8,6 +8,9 @@
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-redshift-data": "^3.758.0",
|
||||
"@aws-sdk/client-s3": "^3.758.0",
|
||||
"@capacitor/app": "^7.0.1",
|
||||
"@capacitor/browser": "^7.0.1",
|
||||
"@capacitor/core": "^7.3.0",
|
||||
"@datastructures-js/priority-queue": "^6.3.1",
|
||||
"@google-cloud/secret-manager": "^5.6.0",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
@@ -101,6 +104,7 @@
|
||||
"jest-environment-jsdom": "^30.0.0",
|
||||
"lint-staged": "^16.1.2",
|
||||
"mrmime": "^2.0.0",
|
||||
"os-browserify": "^0.3.0",
|
||||
"postcss": "^8.5.1",
|
||||
"postcss-loader": "^8.1.1",
|
||||
"prettier": "^3.5.3",
|
||||
@@ -2879,6 +2883,33 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@capacitor/app": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/app/-/app-7.0.1.tgz",
|
||||
"integrity": "sha512-ArlVZAAla4MwQoKh26x2AaTDOBh5Vhp1VhMKR3RwqZSsZnazKTFGNrPbr9Ez5r1knnEDfApyjwp1uZnXK1WTYQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@capacitor/core": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@capacitor/browser": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/browser/-/browser-7.0.1.tgz",
|
||||
"integrity": "sha512-N6KEVLw2enTnourQzYJLvAkSds2Ed21zqsvHnSImrVDenzX8fUj032kMt4EdewmxfxiEwRa911BT1VOPBi0fEA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@capacitor/core": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@capacitor/core": {
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-7.3.0.tgz",
|
||||
"integrity": "sha512-t/DdTyBchQ2eAZuCmAARlqQsrEm0WyeNwh5zeRuv+cR6gnAsw+86/EWvJ/em5dTnZyaqEy8vlmOMdWarrUbnuQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@colors/colors": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz",
|
||||
@@ -19059,6 +19090,13 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/os-browserify": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz",
|
||||
"integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/p-limit": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"jest-environment-jsdom": "^30.0.0",
|
||||
"lint-staged": "^16.1.2",
|
||||
"mrmime": "^2.0.0",
|
||||
"os-browserify": "^0.3.0",
|
||||
"postcss": "^8.5.1",
|
||||
"postcss-loader": "^8.1.1",
|
||||
"prettier": "^3.5.3",
|
||||
@@ -82,6 +83,9 @@
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-redshift-data": "^3.758.0",
|
||||
"@aws-sdk/client-s3": "^3.758.0",
|
||||
"@capacitor/app": "^7.0.1",
|
||||
"@capacitor/browser": "^7.0.1",
|
||||
"@capacitor/core": "^7.3.0",
|
||||
"@datastructures-js/priority-queue": "^6.3.1",
|
||||
"@google-cloud/secret-manager": "^5.6.0",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
|
||||
@@ -592,6 +592,6 @@ export async function buildGameUrl(
|
||||
const config = await getServerConfigFromClient();
|
||||
|
||||
const apiPath = `/api/${path}/${gameID}`;
|
||||
const baseUrl = process.env.API_BASE_URL || "/";
|
||||
return `${baseUrl}${config.workerPath(gameID)}${apiPath}`;
|
||||
const baseUrl = process.env.API_BASE_URL || "";
|
||||
return `${baseUrl}/${config.workerPath(gameID)}${apiPath}`;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, query, state } from "lit/decorators.js";
|
||||
import { translateText } from "../client/Utils";
|
||||
import { getServerConfigFromClient } from "../core/configuration/ConfigLoader";
|
||||
import { GameInfo, GameRecord } from "../core/Schemas";
|
||||
import { generateID } from "../core/Util";
|
||||
import "./components/baseComponents/Button";
|
||||
@@ -171,7 +172,8 @@ export class JoinPrivateLobbyModal extends LitElement {
|
||||
}
|
||||
|
||||
private async checkActiveLobby(lobbyId: string): Promise<boolean> {
|
||||
const url = await buildGameUrl(lobbyId, "game/exists");
|
||||
const config = await getServerConfigFromClient();
|
||||
const url = `${process.env.API_BASE_URL || ""}/${config.workerPath(lobbyId)}/api/game/${lobbyId}/exists`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
|
||||
+9
-1
@@ -30,7 +30,13 @@ import { NewsButton } from "./components/NewsButton";
|
||||
import "./components/baseComponents/Button";
|
||||
import { OButton } from "./components/baseComponents/Button";
|
||||
import "./components/baseComponents/Modal";
|
||||
import { discordLogin, getUserMe, isLoggedIn, logOut } from "./jwt";
|
||||
import {
|
||||
discordLogin,
|
||||
getUserMe,
|
||||
initializeAuthListener,
|
||||
isLoggedIn,
|
||||
logOut,
|
||||
} from "./jwt";
|
||||
import "./styles.css";
|
||||
|
||||
declare global {
|
||||
@@ -77,6 +83,8 @@ class Client {
|
||||
constructor() {}
|
||||
|
||||
initialize(): void {
|
||||
initializeAuthListener();
|
||||
|
||||
const gameVersion = document.getElementById(
|
||||
"game-version",
|
||||
) as HTMLDivElement;
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Redirecting...</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Please wait while we redirect you back to the application.</p>
|
||||
<script>
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const code = params.get("code");
|
||||
const error = params.get("error");
|
||||
const state = params.get("state");
|
||||
|
||||
if (code) {
|
||||
let url = `com.openfront.app://auth?code=${code}`;
|
||||
if (state) {
|
||||
url += `&state=${state}`;
|
||||
}
|
||||
window.location = url;
|
||||
} else if (error) {
|
||||
let url = `com.openfront.app://auth?error=${error}`;
|
||||
if (state) {
|
||||
url += `&state=${state}`;
|
||||
}
|
||||
window.location = url;
|
||||
} else {
|
||||
document.body.innerText =
|
||||
"No code or error found. You can close this window.";
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+84
-5
@@ -1,3 +1,6 @@
|
||||
import { App } from "@capacitor/app";
|
||||
import { Browser } from "@capacitor/browser";
|
||||
import { Capacitor } from "@capacitor/core";
|
||||
import { decodeJwt } from "jose";
|
||||
import { z } from "zod/v4";
|
||||
import {
|
||||
@@ -8,8 +11,12 @@ import {
|
||||
UserMeResponseSchema,
|
||||
} from "../core/ApiSchemas";
|
||||
|
||||
const isNative = Capacitor.getPlatform() !== "web";
|
||||
|
||||
function getAudience() {
|
||||
const { hostname } = new URL(window.location.href);
|
||||
const hostname =
|
||||
process.env.CAPACITOR_PRODUCTION_HOSTNAME ||
|
||||
new URL(window.location.href).hostname;
|
||||
const domainname = hostname.split(".").slice(-2).join(".");
|
||||
return domainname;
|
||||
}
|
||||
@@ -17,7 +24,10 @@ function getAudience() {
|
||||
function getApiBase() {
|
||||
const domainname = getAudience();
|
||||
return domainname === "localhost"
|
||||
? (localStorage.getItem("apiHost") ?? "http://localhost:8787")
|
||||
? (localStorage.getItem("apiHost") ??
|
||||
(isNative && process.env.API_BASE_URL))
|
||||
? process.env.API_BASE_URL!.replace("9000", "8787")
|
||||
: "http://localhost:8787"
|
||||
: `https://api.${domainname}`;
|
||||
}
|
||||
|
||||
@@ -43,8 +53,22 @@ function getToken(): string | null {
|
||||
return localStorage.getItem("token");
|
||||
}
|
||||
|
||||
export function discordLogin() {
|
||||
window.location.href = `${getApiBase()}/login/discord?redirect_uri=${window.location.href}`;
|
||||
export async function discordLogin() {
|
||||
let redirectUri: string;
|
||||
|
||||
if (isNative) {
|
||||
redirectUri = `${process.env.API_BASE_URL}/discord-redirect.html`;
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
||||
export async function logOut(allSessions: boolean = false) {
|
||||
@@ -101,7 +125,7 @@ function _isLoggedIn(): IsLoggedInResponse {
|
||||
const payload = decodeJwt(token);
|
||||
const { iss, aud, exp, iat } = payload;
|
||||
|
||||
if (iss !== getApiBase()) {
|
||||
if (iss !== getApiBase() && !isNative) {
|
||||
// JWT was not issued by the correct server
|
||||
console.error(
|
||||
'unexpected "iss" claim value',
|
||||
@@ -158,6 +182,61 @@ function _isLoggedIn(): IsLoggedInResponse {
|
||||
}
|
||||
}
|
||||
|
||||
export function initializeAuthListener() {
|
||||
if (Capacitor.getPlatform() === "web") return;
|
||||
|
||||
App.addListener("appUrlOpen", async (data) => {
|
||||
try {
|
||||
const url = new URL(data.url);
|
||||
const code = url.searchParams.get("code");
|
||||
const state = url.searchParams.get("state");
|
||||
const error = url.searchParams.get("error");
|
||||
|
||||
if (error) {
|
||||
console.error("Error from auth provider:", error);
|
||||
await Browser.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (code && state) {
|
||||
const redirectUri = `${process.env.API_BASE_URL}/discord-redirect.html`;
|
||||
const response = await fetch(`${getApiBase()}/mobile/callback`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
code,
|
||||
state,
|
||||
redirect_uri: redirectUri,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to exchange code for token: ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
const { token } = await response.json();
|
||||
|
||||
if (token) {
|
||||
localStorage.setItem("token", token);
|
||||
__isLoggedIn = undefined;
|
||||
await Browser.close();
|
||||
window.location.assign(window.location.origin || "/");
|
||||
} else {
|
||||
console.error("No token found in response");
|
||||
await Browser.close();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error handling appUrlOpen", e);
|
||||
await Browser.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function postRefresh(): Promise<boolean> {
|
||||
try {
|
||||
const token = getToken();
|
||||
|
||||
+19
-9
@@ -6,16 +6,26 @@ import { getServerConfigFromServer } from "../core/configuration/ConfigLoader";
|
||||
const config = getServerConfigFromServer();
|
||||
const origin = config.origin();
|
||||
|
||||
const allowedOrigins = [
|
||||
origin,
|
||||
"capacitor://openfront.io",
|
||||
"https://openfront.io",
|
||||
];
|
||||
const allowedOrigins: string[] = [origin];
|
||||
|
||||
if (config.env() === GameEnv.Dev) {
|
||||
const localIp = getLocalIP();
|
||||
if (localIp) {
|
||||
allowedOrigins.push(`http://${localIp}:9000`);
|
||||
switch (config.env()) {
|
||||
case GameEnv.Prod:
|
||||
allowedOrigins.push("capacitor://openfront.io", "https://openfront.io");
|
||||
break;
|
||||
case GameEnv.Preprod:
|
||||
allowedOrigins.push("capacitor://openfront.dev", "https://openfront.dev");
|
||||
break;
|
||||
case GameEnv.Dev: {
|
||||
allowedOrigins.push(
|
||||
"capacitor://localhost",
|
||||
"http://localhost",
|
||||
"http://localhost:8787",
|
||||
);
|
||||
const localIp = getLocalIP();
|
||||
if (localIp) {
|
||||
allowedOrigins.push(`http://${localIp}:9000`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -124,6 +124,9 @@ export default async (env, argv) => {
|
||||
"node_modules/protobufjs/minimal.js",
|
||||
),
|
||||
},
|
||||
fallback: {
|
||||
os: path.resolve(__dirname, "node_modules/os-browserify/browser.js"),
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
@@ -141,6 +144,21 @@ export default async (env, argv) => {
|
||||
}
|
||||
: false,
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: "./src/client/discord-redirect.html",
|
||||
filename: "discord-redirect.html",
|
||||
chunks: [],
|
||||
minify: isProduction
|
||||
? {
|
||||
collapseWhitespace: true,
|
||||
removeComments: true,
|
||||
removeRedundantAttributes: true,
|
||||
removeScriptTypeAttributes: true,
|
||||
removeStyleLinkTypeAttributes: true,
|
||||
useShortDoctype: true,
|
||||
}
|
||||
: false,
|
||||
}),
|
||||
new webpack.DefinePlugin({
|
||||
"process.env.WEBSOCKET_URL": JSON.stringify(
|
||||
isProduction ? "" : "localhost:3000",
|
||||
|
||||
Reference in New Issue
Block a user