Improve ingame moderation for admins (#3678)

## Description:

Players with the `admin` flare can now kick players from any game
(including public lobbies), not just the lobby creator in private
lobbies.

## 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:

w.o.n
This commit is contained in:
Ryan
2026-04-20 19:09:04 +01:00
committed by GitHub
parent 52033597ef
commit c3d7d0373e
12 changed files with 308 additions and 25 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ export class Client {
public readonly clientID: ClientID,
public readonly persistentID: string,
public readonly claims: TokenPayload | null,
public readonly roles: string[] | undefined,
public readonly role: string | null,
public readonly flares: string[] | undefined,
public readonly ip: string,
public username: string,
+23 -12
View File
@@ -2,6 +2,7 @@ import ipAnonymize from "ip-anonymize";
import { Logger } from "winston";
import WebSocket from "ws";
import { z } from "zod";
import { isAdminRole } from "../core/ApiSchemas";
import { GameEnv, ServerConfig } from "../core/configuration/Config";
import { GameType } from "../core/game/Game";
import {
@@ -35,6 +36,7 @@ export enum GamePhase {
const KICK_REASON_DUPLICATE_SESSION = "kick_reason.duplicate_session";
const KICK_REASON_LOBBY_CREATOR = "kick_reason.lobby_creator";
const KICK_REASON_ADMIN = "kick_reason.admin";
const KICK_REASON_HOST_LEFT = "kick_reason.host_left";
const KICK_REASON_TOO_MUCH_DATA = "kick_reason.too_much_data";
const KICK_REASON_INVALID_MESSAGE = "kick_reason.invalid_message";
@@ -394,18 +396,24 @@ export class GameServer {
// Handle kick_player intent via WebSocket
case "kick_player": {
// Check if the authenticated client is the lobby creator
if (client.clientID !== this.lobbyCreatorID) {
this.log.warn(`Only lobby creator can kick players`, {
clientID: client.clientID,
creatorID: this.lobbyCreatorID,
target: stampedIntent.target,
gameID: this.id,
});
const isLobbyCreator = client.clientID === this.lobbyCreatorID;
const isAdmin = isAdminRole(client.role);
// Check if the authenticated client is the lobby creator or admin
if (!isLobbyCreator && !isAdmin) {
this.log.warn(
`Only lobby creator or admin can kick players`,
{
clientID: client.clientID,
creatorID: this.lobbyCreatorID,
target: stampedIntent.target,
gameID: this.id,
},
);
return;
}
// Don't allow lobby creator to kick themselves
// Don't allow kicking yourself
if (client.clientID === stampedIntent.target) {
this.log.warn(`Cannot kick yourself`, {
clientID: client.clientID,
@@ -414,8 +422,9 @@ export class GameServer {
}
// Log and execute the kick
this.log.info(`Lobby creator initiated kick of player`, {
creatorID: client.clientID,
this.log.info(`Player initiated kick`, {
kickerID: client.clientID,
isAdmin,
target: stampedIntent.target,
gameID: this.id,
kickMethod: "websocket",
@@ -423,7 +432,9 @@ export class GameServer {
this.kickClient(
stampedIntent.target,
KICK_REASON_LOBBY_CREATOR,
isAdmin && !isLobbyCreator
? KICK_REASON_ADMIN
: KICK_REASON_LOBBY_CREATOR,
);
return;
}
+1 -3
View File
@@ -401,7 +401,6 @@ export async function startWorker() {
return;
}
let roles: string[] | undefined;
let flares: string[] | undefined;
const allowedFlares = config.allowedFlares();
@@ -422,7 +421,6 @@ export async function startWorker() {
ws.close(1002, "Unauthorized: user me fetch failed");
return;
}
roles = result.response.player.roles;
flares = result.response.player.flares;
if (allowedFlares !== undefined) {
@@ -484,7 +482,7 @@ export async function startWorker() {
generateID(),
persistentId,
claims,
roles,
claims?.role ?? null,
flares,
ip,
censoredUsername,