Prevent multiple clients from using the same account (#706)

## Description:

This change blocks a persistent ID from being reused by more than one
client in the same game. This setting is only enabled in staging and
prod.

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
- [x] I understand that submitting code with bugs that could have been
caught through manual testing blocks releases and new features for all
contributors

---------

Co-authored-by: Scott Anderson <662325+scottanderson@users.noreply.github.com>
This commit is contained in:
Scott Anderson
2025-05-18 00:03:05 -04:00
committed by GitHub
parent e4c8b17b48
commit b558bd722d
2 changed files with 27 additions and 12 deletions
+4 -8
View File
@@ -31,7 +31,6 @@ export class SinglePlayerModal extends LitElement {
@state() private selectedMap: GameMapType = GameMapType.World;
@state() private selectedDifficulty: Difficulty = Difficulty.Medium;
@state() private disableNPCs: boolean = false;
@state() private disableNukes: boolean = false;
@state() private bots: number = 400;
@state() private infiniteGold: boolean = false;
@state() private infiniteTroops: boolean = false;
@@ -390,10 +389,6 @@ export class SinglePlayerModal extends LitElement {
this.disableNPCs = Boolean((e.target as HTMLInputElement).checked);
}
private handleDisableNukesChange(e: Event) {
this.disableNukes = Boolean((e.target as HTMLInputElement).checked);
}
private handleGameModeSelection(value: GameMode) {
this.gameMode = value;
}
@@ -456,15 +451,16 @@ export class SinglePlayerModal extends LitElement {
playerTeams: this.teamCount,
difficulty: this.selectedDifficulty,
disableNPCs: this.disableNPCs,
disableNukes: this.disableNukes,
bots: this.bots,
infiniteGold: this.infiniteGold,
infiniteTroops: this.infiniteTroops,
instantBuild: this.instantBuild,
disabledUnits: this.disabledUnits,
disabledUnits: this.disabledUnits
.map((u) => Object.values(UnitType).find((ut) => ut === u))
.filter((ut): ut is UnitType => ut !== undefined),
},
},
} as JoinLobbyEvent,
} satisfies JoinLobbyEvent,
bubbles: true,
composed: true,
}),
+23 -4
View File
@@ -20,7 +20,7 @@ import {
Turn,
} from "../core/Schemas";
import { createGameRecord } from "../core/Util";
import { ServerConfig } from "../core/configuration/Config";
import { GameEnv, ServerConfig } from "../core/configuration/Config";
import { GameType } from "../core/game/Game";
import { archive } from "./Archive";
import { Client } from "./Client";
@@ -132,6 +132,25 @@ export class GameServer {
return;
}
if (this.config.env() === GameEnv.Prod) {
// Prevent multiple clients from using the same account in prod
const conflicting = this.activeClients.find(
(c) =>
c.persistentID === client.persistentID &&
c.clientID !== client.clientID,
);
if (conflicting !== undefined) {
this.log.error("client ids do not match", {
clientID: client.clientID,
clientIP: ipAnonymize(client.ip),
clientPersistentID: client.persistentID,
existingIP: ipAnonymize(conflicting.ip),
existingPersistentID: conflicting.persistentID,
});
return;
}
}
// Remove stale client if this is a reconnect
const existing = this.activeClients.find(
(c) => c.clientID === client.clientID,
@@ -148,10 +167,10 @@ export class GameServer {
return;
}
existing.ws.removeAllListeners("message");
this.activeClients = this.activeClients.filter(
(c) => c.clientID !== client.clientID,
);
this.activeClients = this.activeClients.filter((c) => c !== existing);
}
// Client connection accepted
this.activeClients.push(client);
client.lastPing = Date.now();