mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 12:00:44 +00:00
Fetch archived games from api, allow development against production & staging (#2045)
## Description: Instead of going through the game server to fetch archived games, have the client fetch from api directly. Also loosen up cors restrictions & domain checks so localhost:9000 can talk to staging or production servers related to #1571 ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: evan
This commit is contained in:
@@ -87,6 +87,22 @@ To run just the server with development settings:
|
||||
npm run start:server-dev
|
||||
```
|
||||
|
||||
### Connecting to staging or production backends
|
||||
|
||||
Sometimes it's useful to connect to production servers when replaying a game, testing user profiles, purchases, or login flow.
|
||||
|
||||
To connect to staging api servers:
|
||||
|
||||
```bash
|
||||
npm run dev:staging
|
||||
```
|
||||
|
||||
To connect to production api servers:
|
||||
|
||||
```bash
|
||||
npm run dev:prod
|
||||
```
|
||||
|
||||
## 🛠️ Development Tools
|
||||
|
||||
- **Format code**:
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
"start:server": "node --loader ts-node/esm --experimental-specifier-resolution=node src/server/Server.ts",
|
||||
"start:server-dev": "cross-env GAME_ENV=dev node --loader ts-node/esm --experimental-specifier-resolution=node src/server/Server.ts",
|
||||
"dev": "cross-env GAME_ENV=dev concurrently \"npm run start:client\" \"npm run start:server-dev\"",
|
||||
"dev:staging": "cross-env GAME_ENV=dev API_DOMAIN=api.openfront.dev concurrently \"npm run start:client\" \"npm run start:server-dev\"",
|
||||
"dev:prod": "cross-env GAME_ENV=dev API_DOMAIN=api.openfront.io concurrently \"npm run start:client\" \"npm run start:server-dev\"",
|
||||
"tunnel": "npm run build-prod && npm run start:server",
|
||||
"test": "jest",
|
||||
"perf": "npx tsx tests/perf/*.ts",
|
||||
|
||||
@@ -193,8 +193,9 @@
|
||||
"join_lobby": "Join Lobby",
|
||||
"checking": "Checking lobby...",
|
||||
"not_found": "Lobby not found. Please check the ID and try again.",
|
||||
"error": "An error occurred. Please try again.",
|
||||
"joined_waiting": "Joined successfully! Waiting for game to start..."
|
||||
"error": "An error occurred. Please try again or contact support.",
|
||||
"joined_waiting": "Joined successfully! Waiting for game to start...",
|
||||
"version_mismatch": "This game was created with a different version. Cannot join."
|
||||
},
|
||||
"public_lobby": {
|
||||
"join": "Join next Game",
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, query, state } from "lit/decorators.js";
|
||||
import { translateText } from "../client/Utils";
|
||||
import { GameInfo, GameRecord } from "../core/Schemas";
|
||||
import { GameInfo, GameRecordSchema } from "../core/Schemas";
|
||||
import { generateID } from "../core/Util";
|
||||
import { getServerConfigFromClient } from "../core/configuration/ConfigLoader";
|
||||
import { JoinLobbyEvent } from "./Main";
|
||||
import "./components/baseComponents/Button";
|
||||
import "./components/baseComponents/Modal";
|
||||
import { getApiBase } from "./jwt";
|
||||
@customElement("join-private-lobby-modal")
|
||||
export class JoinPrivateLobbyModal extends LitElement {
|
||||
@query("o-modal") private modalEl!: HTMLElement & {
|
||||
@@ -179,10 +180,19 @@ export class JoinPrivateLobbyModal extends LitElement {
|
||||
if (gameExists) return;
|
||||
|
||||
// If not active, check archived games
|
||||
const archivedGame = await this.checkArchivedGame(lobbyId);
|
||||
if (archivedGame) return;
|
||||
|
||||
this.message = `${translateText("private_lobby.not_found")}`;
|
||||
switch (await this.checkArchivedGame(lobbyId)) {
|
||||
case "success":
|
||||
return;
|
||||
case "not_found":
|
||||
this.message = `${translateText("private_lobby.not_found")}`;
|
||||
return;
|
||||
case "version_mismatch":
|
||||
this.message = `${translateText("private_lobby.version_mismatch")}`;
|
||||
return;
|
||||
case "error":
|
||||
this.message = `${translateText("private_lobby.error")}`;
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error checking lobby existence:", error);
|
||||
this.message = `${translateText("private_lobby.error")}`;
|
||||
@@ -222,49 +232,70 @@ export class JoinPrivateLobbyModal extends LitElement {
|
||||
return false;
|
||||
}
|
||||
|
||||
private async checkArchivedGame(lobbyId: string): Promise<boolean> {
|
||||
const config = await getServerConfigFromClient();
|
||||
const archiveUrl = `/${config.workerPath(lobbyId)}/api/archived_game/${lobbyId}`;
|
||||
|
||||
const archiveResponse = await fetch(archiveUrl, {
|
||||
private async checkArchivedGame(
|
||||
lobbyId: string,
|
||||
): Promise<"success" | "not_found" | "version_mismatch" | "error"> {
|
||||
const archivePromise = fetch(`${getApiBase()}/game/${lobbyId}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const gitCommitPromise = fetch(`/commit.txt`, {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
|
||||
const archiveData = await archiveResponse.json();
|
||||
const [archiveResponse, gitCommitResponse] = await Promise.all([
|
||||
archivePromise,
|
||||
gitCommitPromise,
|
||||
]);
|
||||
|
||||
if (
|
||||
archiveData.success === false &&
|
||||
archiveData.error === "Version mismatch"
|
||||
) {
|
||||
if (archiveResponse.status === 404) {
|
||||
return "not_found";
|
||||
}
|
||||
if (archiveResponse.status !== 200) {
|
||||
return "error";
|
||||
}
|
||||
|
||||
const archiveData = await archiveResponse.json();
|
||||
const parsed = GameRecordSchema.safeParse(archiveData);
|
||||
if (!parsed.success) {
|
||||
return "version_mismatch";
|
||||
}
|
||||
|
||||
let myGitCommit = "";
|
||||
if (gitCommitResponse.status === 404) {
|
||||
// commit.txt is not found when running locally
|
||||
myGitCommit = "DEV";
|
||||
} else if (gitCommitResponse.status === 200) {
|
||||
myGitCommit = await gitCommitResponse.text();
|
||||
} else {
|
||||
console.error("Error getting git commit:", gitCommitResponse.status);
|
||||
return "error";
|
||||
}
|
||||
|
||||
// Allow DEV to join games created with a different version for debugging.
|
||||
if (myGitCommit !== "DEV" && parsed.data.gitCommit !== myGitCommit) {
|
||||
console.warn(
|
||||
`Git commit hash mismatch for game ${lobbyId}`,
|
||||
archiveData.details,
|
||||
);
|
||||
this.message =
|
||||
"This game was created with a different version. Cannot join.";
|
||||
return true;
|
||||
return "version_mismatch";
|
||||
}
|
||||
|
||||
if (archiveData.exists) {
|
||||
const gameRecord = archiveData.gameRecord as GameRecord;
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("join-lobby", {
|
||||
detail: {
|
||||
gameID: lobbyId,
|
||||
gameRecord: gameRecord,
|
||||
clientID: generateID(),
|
||||
} as JoinLobbyEvent,
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("join-lobby", {
|
||||
detail: {
|
||||
gameID: lobbyId,
|
||||
gameRecord: parsed.data,
|
||||
clientID: generateID(),
|
||||
} as JoinLobbyEvent,
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
return "success";
|
||||
}
|
||||
|
||||
private async pollPlayers() {
|
||||
|
||||
+12
-4
@@ -17,9 +17,16 @@ function getAudience() {
|
||||
|
||||
export function getApiBase() {
|
||||
const domainname = getAudience();
|
||||
return domainname === "localhost"
|
||||
? (localStorage.getItem("apiHost") ?? "http://localhost:8787")
|
||||
: `https://api.${domainname}`;
|
||||
|
||||
if (domainname === "localhost") {
|
||||
const apiDomain = process?.env?.API_DOMAIN;
|
||||
if (apiDomain) {
|
||||
return `https://${apiDomain}`;
|
||||
}
|
||||
return localStorage.getItem("apiHost") ?? "http://localhost:8787";
|
||||
}
|
||||
|
||||
return `https://api.${domainname}`;
|
||||
}
|
||||
|
||||
function getToken(): string | null {
|
||||
@@ -159,7 +166,8 @@ function _isLoggedIn(): IsLoggedInResponse {
|
||||
logOut();
|
||||
return false;
|
||||
}
|
||||
if (aud !== getAudience()) {
|
||||
const myAud = getAudience();
|
||||
if (myAud !== "localhost" && aud !== myAud) {
|
||||
// JWT was not issued for this website
|
||||
console.error(
|
||||
'unexpected "aud" claim value',
|
||||
|
||||
+1
-38
@@ -7,7 +7,6 @@ import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { WebSocket, WebSocketServer } from "ws";
|
||||
import { z } from "zod";
|
||||
import { GameEnv } from "../core/configuration/Config";
|
||||
import { getServerConfigFromServer } from "../core/configuration/ConfigLoader";
|
||||
import { GameType } from "../core/game/Game";
|
||||
import {
|
||||
@@ -18,7 +17,7 @@ import {
|
||||
} from "../core/Schemas";
|
||||
import { replacer } from "../core/Util";
|
||||
import { CreateGameInputSchema, GameInputSchema } from "../core/WorkerSchemas";
|
||||
import { archive, finalizeGameRecord, readGameRecord } from "./Archive";
|
||||
import { archive, finalizeGameRecord } from "./Archive";
|
||||
import { Client } from "./Client";
|
||||
import { GameManager } from "./GameManager";
|
||||
import { getUserMe, verifyClientToken } from "./jwt";
|
||||
@@ -212,42 +211,6 @@ export async function startWorker() {
|
||||
res.json(game.gameInfo());
|
||||
});
|
||||
|
||||
app.get("/api/archived_game/:id", async (req, res) => {
|
||||
const gameRecord = await readGameRecord(req.params.id);
|
||||
|
||||
if (!gameRecord) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: "Game not found",
|
||||
exists: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
config.env() !== GameEnv.Dev &&
|
||||
gameRecord.gitCommit !== config.gitCommit()
|
||||
) {
|
||||
log.warn(
|
||||
`git commit mismatch for game ${req.params.id}, expected ${config.gitCommit()}, got ${gameRecord.gitCommit}`,
|
||||
);
|
||||
return res.status(409).json({
|
||||
success: false,
|
||||
error: "Version mismatch",
|
||||
exists: true,
|
||||
details: {
|
||||
expectedCommit: config.gitCommit(),
|
||||
actualCommit: gameRecord.gitCommit,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
exists: true,
|
||||
gameRecord: gameRecord,
|
||||
});
|
||||
});
|
||||
|
||||
app.post("/api/archive_singleplayer_game", async (req, res) => {
|
||||
try {
|
||||
const record = req.body;
|
||||
|
||||
@@ -129,6 +129,7 @@ export default async (env, argv) => {
|
||||
"process.env.STRIPE_PUBLISHABLE_KEY": JSON.stringify(
|
||||
process.env.STRIPE_PUBLISHABLE_KEY,
|
||||
),
|
||||
"process.env.API_DOMAIN": JSON.stringify(process.env.API_DOMAIN),
|
||||
}),
|
||||
new CopyPlugin({
|
||||
patterns: [
|
||||
|
||||
Reference in New Issue
Block a user