add endpoint to kick players from a game given admin token, game id & client id

This commit is contained in:
evan
2025-05-09 13:00:39 -07:00
parent e2c97d8d51
commit c5152f035f
5 changed files with 89 additions and 1 deletions
+1 -1
View File
@@ -82,7 +82,7 @@ export function joinLobby(
if (message.type == "start") {
// Trigger prestart for singleplayer games
onPrestart();
consolex.log(`lobby: game started: ${JSON.stringify(message)}`);
consolex.log(`lobby: game started: ${JSON.stringify(message, null, 2)}`);
onJoin();
// For multiplayer games, GameStartInfo is not known until game starts.
lobbyConfig.gameStartInfo = message.gameStartInfo;
+33
View File
@@ -57,6 +57,8 @@ export class GameServer {
private _hasPrestarted = false;
private kickedClients: Set<ClientID> = new Set();
constructor(
public readonly id: string,
readonly log_: Logger,
@@ -103,6 +105,12 @@ export class GameServer {
}
public addClient(client: Client, lastTurn: number) {
if (this.kickedClients.has(client.clientID)) {
this.log.warn(`cannot add client, already kicked`, {
clientID: client.clientID,
});
return;
}
this.log.info("client (re)joining game", {
clientID: client.clientID,
persistentID: client.persistentID,
@@ -492,6 +500,31 @@ export class GameServer {
return this.gameConfig.gameType == GameType.Public;
}
public kickClient(clientID: ClientID): void {
if (this.kickedClients.has(clientID)) {
this.log.warn(`cannot kick client, already kicked`, {
clientID,
});
return;
}
const client = this.activeClients.find((c) => c.clientID === clientID);
if (client) {
this.log.info("Kicking client from game", {
clientID: client.clientID,
persistentID: client.persistentID,
});
client.ws.close(1000, "Kicked from game");
this.activeClients = this.activeClients.filter(
(c) => c.clientID !== clientID,
);
this.kickedClients.add(clientID);
} else {
this.log.warn(`cannot kick client, not found in game`, {
clientID,
});
}
}
private handleSynchronization() {
if (this.activeClients.length <= 1) {
return;
+33
View File
@@ -160,6 +160,39 @@ app.get(
}),
);
app.post(
"/api/kick_player/:gameID/:clientID",
gatekeeper.httpHandler(LimiterType.Post, async (req, res) => {
if (req.headers[config.adminHeader()] !== config.adminToken()) {
res.status(401).send("Unauthorized");
return;
}
const { gameID, clientID } = req.params;
try {
const response = await fetch(
`http://localhost:${config.workerPort(gameID)}/api/kick_player/${gameID}/${clientID}`,
{
method: "POST",
headers: {
[config.adminHeader()]: config.adminToken(),
},
},
);
if (!response.ok) {
throw new Error(`Failed to kick player: ${response.statusText}`);
}
res.status(200).send("Player kicked successfully");
} catch (error) {
log.error(`Error kicking player from game ${gameID}:`, error);
res.status(500).send("Failed to kick player");
}
}),
);
async function fetchLobbies(): Promise<number> {
const fetchPromises = [];
+21
View File
@@ -250,6 +250,27 @@ export function startWorker() {
}),
);
app.post(
"/api/kick_player/:gameID/:clientID",
gatekeeper.httpHandler(LimiterType.Post, async (req, res) => {
if (req.headers[config.adminHeader()] !== config.adminToken()) {
res.status(401).send("Unauthorized");
return;
}
const { gameID, clientID } = req.params;
const game = gm.game(gameID);
if (!game) {
res.status(404).send("Game not found");
return;
}
game.kickClient(clientID);
res.status(200).send("Player kicked successfully");
}),
);
// WebSocket handling
wss.on("connection", (ws: WebSocket, req) => {
ws.on(
+1
View File
@@ -228,6 +228,7 @@ export default async (env, argv) => {
"/api/archive_singleplayer_game",
"/api/auth/callback",
"/api/auth/discord",
"/api/kick_player",
],
target: "http://localhost:3000",
secure: false,