Private Lobbies: Add kick player functionality (#1436)

## Description:
Added player management features so lobby hosts can kick players from
private games. This includes both UI changes and backend work.

### What's new:
- Hosts can now kick players from private lobbies with a simple button
- Added host badges and remove buttons to the UI
- Made sure only hosts can kick people, and hosts can't kick themselves

### How it works:
- When someone creates a private game, they automatically become the
host
- Kicking happens through WebSocket "kick-player" events
- Server checks that you're actually the host before letting you kick
anyone

<img width="1291" height="871" alt="Screenshot 2025-07-15 002114"
src="https://github.com/user-attachments/assets/ea575f83-a0f4-45d1-9cfe-7521d373f3d5"
/>



### Known Issues:
- Kicked player gets general message (same when kicked for multi tab)

### Other Issues:
- Host abandoment still existent (host clicks on x; or is closing tab)

## 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
- [x] I have read and accepted the CLA agreement (only required once).

## Please put your Discord username so you can be contacted if a bug or
regression is found:

[UN]nvm

---------

Co-authored-by: floriankilian <floriankilian@users.noreply.github.com>
This commit is contained in:
floriankilian
2025-08-01 07:01:10 +02:00
committed by GitHub
parent ee459b7410
commit bd59cd61cb
10 changed files with 211 additions and 54 deletions
+11 -2
View File
@@ -15,6 +15,7 @@ import {
ClientMessageSchema,
GameRecord,
GameRecordSchema,
ID,
ServerErrorMessage,
} from "../core/Schemas";
import { CreateGameInputSchema, GameInputSchema } from "../core/WorkerSchemas";
@@ -90,6 +91,13 @@ export function startWorker() {
"/api/create_game/:id",
gatekeeper.httpHandler(LimiterType.Post, async (req, res) => {
const id = req.params.id;
const creatorClientID = (() => {
if (typeof req.query.creatorClientID !== "string") return undefined;
const trimmed = req.query.creatorClientID.trim();
return ID.safeParse(trimmed).success ? trimmed : undefined;
})();
if (!id) {
log.warn(`cannot create game, id not found`);
return res.status(400).json({ error: "Game ID is required" });
@@ -122,10 +130,11 @@ export function startWorker() {
return res.status(400).json({ error: "Worker, game id mismatch" });
}
const game = gm.createGame(id, gc);
// Pass creatorClientID to createGame
const game = gm.createGame(id, gc, creatorClientID);
log.info(
`Worker ${workerId}: IP ${ipAnonymize(clientIP)} creating game ${game.isPublic() ? "Public" : "Private"} with id ${id}`,
`Worker ${workerId}: IP ${ipAnonymize(clientIP)} creating ${game.isPublic() ? "Public" : "Private"}${gc?.gameMode ? ` ${gc.gameMode}` : ""} game with id ${id}${creatorClientID ? `, creator: ${creatorClientID}` : ""}`,
);
res.json(game.gameInfo());
}),