mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-18 16:32:40 +00:00
+ add validation server side
This commit is contained in:
@@ -267,4 +267,4 @@ async function createLobby(): Promise<Lobby> {
|
||||
throw error; // Re-throw the error so the caller can handle it
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
+35
-2
@@ -5,6 +5,10 @@ import { AllianceRequest, AllPlayers, Cell, GameType, Player, PlayerID, PlayerTy
|
||||
import { ClientID, ClientIntentMessageSchema, ClientJoinMessageSchema, GameID, Intent, ServerMessage, ServerMessageSchema, ClientPingMessageSchema, GameConfig, ClientLogMessageSchema } from "../core/Schemas"
|
||||
import { LobbyConfig } from "./GameRunner"
|
||||
import { LocalServer } from "./LocalServer"
|
||||
import {UsernameInput} from "./UsernameInput";
|
||||
import {HostLobbyModal as HostPrivateLobbyModal} from "./HostLobbyModal";
|
||||
import {JoinPrivateLobbyModal} from "./JoinPrivateLobbyModal";
|
||||
import {SinglePlayerModal} from "./SinglePlayerModal";
|
||||
|
||||
export class PauseGameEvent implements GameEvent {
|
||||
constructor(public readonly paused: boolean) { }
|
||||
@@ -179,7 +183,20 @@ export class Transport {
|
||||
onconnect()
|
||||
};
|
||||
this.socket.onmessage = (event: MessageEvent) => {
|
||||
onmessage(ServerMessageSchema.parse(JSON.parse(event.data)))
|
||||
try {
|
||||
const serverMsg = ServerMessageSchema.parse(JSON.parse(event.data));
|
||||
|
||||
// Handle validation errors
|
||||
if (serverMsg.type === 'validationError' && serverMsg.input === 'username-input') {
|
||||
this.handleValidationError(serverMsg.message);
|
||||
return;
|
||||
}
|
||||
|
||||
// Process other message types
|
||||
this.onmessage(serverMsg);
|
||||
} catch (error) {
|
||||
console.error('Failed to process server message:', error);
|
||||
}
|
||||
};
|
||||
this.socket.onerror = (err) => {
|
||||
console.error('Socket encountered error: ', err, 'Closing socket');
|
||||
@@ -240,7 +257,23 @@ export class Transport {
|
||||
this.socket.onclose = (event: CloseEvent) => { }
|
||||
}
|
||||
|
||||
private handleValidationError(message: string) {
|
||||
const usernameInput = document.querySelector('username-input') as UsernameInput;
|
||||
const hostModal = document.querySelector('host-lobby-modal') as HostPrivateLobbyModal;
|
||||
const joinModal = document.querySelector('join-private-lobby-modal') as JoinPrivateLobbyModal;
|
||||
const singlePlayerModal = document.querySelector('single-player-modal') as SinglePlayerModal;
|
||||
|
||||
if (usernameInput) {
|
||||
this.closeAllModals([hostModal, joinModal, singlePlayerModal]);
|
||||
usernameInput.validationError = message;
|
||||
}
|
||||
}
|
||||
|
||||
private closeAllModals(modals: (HostPrivateLobbyModal | JoinPrivateLobbyModal | SinglePlayerModal)[]) {
|
||||
modals.forEach(modal => {
|
||||
modal.close();
|
||||
});
|
||||
}
|
||||
private onSendAllianceRequest(event: SendAllianceRequestIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "allianceRequest",
|
||||
@@ -416,4 +449,4 @@ export class Transport {
|
||||
this.socket = null
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,12 +47,11 @@ export class UsernameInput extends LitElement {
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.username = this.getStoredUsername();
|
||||
this.dispatchUsernameEvent();
|
||||
this.dispatchUsernameEvent()
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
.value=${this.username}
|
||||
@@ -63,7 +62,6 @@ export class UsernameInput extends LitElement {
|
||||
${this.validationError
|
||||
? html`<div class="error">${this.validationError}</div>`
|
||||
: null}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -91,7 +89,7 @@ export class UsernameInput extends LitElement {
|
||||
|
||||
private dispatchUsernameEvent() {
|
||||
this.dispatchEvent(new CustomEvent('username-change', {
|
||||
detail: { username: this.username },
|
||||
detail: {username: this.username},
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
|
||||
@@ -227,7 +227,7 @@ export class NameLayer implements Layer {
|
||||
|
||||
|
||||
if (myPlayer != null) {
|
||||
const emojis = render.player.outgoingEmojis().filter(e => e.recipient == AllPlayers || e.recipient == myPlayer);
|
||||
const emojis = render.player.outgoingEmojis().filter(e => e.recipient == AllPlayers || e.recipient == myPlayer)
|
||||
if (emojis.length > 0) {
|
||||
context.font = `${render.fontSize * 4}px ${this.theme.font()}`;
|
||||
context.fillStyle = this.theme.playerInfoColor(render.player.id()).toHex();
|
||||
|
||||
+13
-3
@@ -32,11 +32,12 @@ export type Turn = z.infer<typeof TurnSchema>
|
||||
export type GameConfig = z.infer<typeof GameConfigSchema>
|
||||
|
||||
export type ClientMessage = ClientPingMessage | ClientIntentMessage | ClientJoinMessage | ClientLogMessage
|
||||
export type ServerMessage = ServerSyncMessage | ServerStartGameMessage | ServerPingMessage
|
||||
export type ServerMessage = ServerSyncMessage | ServerStartGameMessage | ServerPingMessage | ServerValidationErrorMessageSchema
|
||||
|
||||
export type ServerSyncMessage = z.infer<typeof ServerTurnMessageSchema>
|
||||
export type ServerStartGameMessage = z.infer<typeof ServerStartGameMessageSchema>
|
||||
export type ServerPingMessage = z.infer<typeof ServerPingMessageSchema>
|
||||
export type ServerValidationErrorMessageSchema = z.infer<typeof ServerValidationErrorMessageSchema>
|
||||
|
||||
export type ClientPingMessage = z.infer<typeof ClientPingMessageSchema>
|
||||
export type ClientIntentMessage = z.infer<typeof ClientIntentMessageSchema>
|
||||
@@ -210,9 +211,18 @@ export const ServerStartGameMessageSchema = ServerBaseMessageSchema.extend({
|
||||
config: GameConfigSchema
|
||||
})
|
||||
|
||||
export const ServerValidationErrorMessageSchema = ServerBaseMessageSchema.extend({
|
||||
type: z.literal('validationError'),
|
||||
input: z.string(),
|
||||
message: z.string()
|
||||
});
|
||||
|
||||
export const ServerMessageSchema = z.union([ServerTurnMessageSchema, ServerStartGameMessageSchema, ServerPingMessageSchema]);
|
||||
|
||||
export const ServerMessageSchema = z.union([
|
||||
ServerTurnMessageSchema,
|
||||
ServerStartGameMessageSchema,
|
||||
ServerPingMessageSchema,
|
||||
ServerValidationErrorMessageSchema,
|
||||
]);
|
||||
|
||||
// Client
|
||||
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ import { customAlphabet, nanoid } from 'nanoid';
|
||||
|
||||
|
||||
export const MIN_USERNAME_LENGTH = 3;
|
||||
export const MAX_USERNAME_LENGTH = 12;
|
||||
export const MAX_USERNAME_LENGTH = 15;
|
||||
|
||||
export function manhattanDist(c1: Cell, c2: Cell): number {
|
||||
return Math.abs(c1.x - c2.x) + Math.abs(c1.y - c2.y);
|
||||
|
||||
@@ -7,49 +7,43 @@ import {BOT_NAME_PREFIXES, BOT_NAME_SUFFIXES} from "./utils/BotNames";
|
||||
|
||||
|
||||
export class BotSpawner {
|
||||
private random: PseudoRandom;
|
||||
private random: PseudoRandom
|
||||
private bots: SpawnIntent[] = [];
|
||||
|
||||
constructor(private gs: Game, gameID: GameID) {
|
||||
this.random = new PseudoRandom(simpleHash(gameID));
|
||||
this.random = new PseudoRandom(simpleHash(gameID))
|
||||
}
|
||||
|
||||
spawnBots(numBots: number): SpawnIntent[] {
|
||||
let tries = 0;
|
||||
let tries = 0
|
||||
while (this.bots.length < numBots) {
|
||||
if (tries > 10000) {
|
||||
consolex.log("too many retries while spawning bots, giving up");
|
||||
return this.bots;
|
||||
consolex.log('too many retries while spawning bots, giving up')
|
||||
return this.bots
|
||||
}
|
||||
const botName = this.randomBotName();
|
||||
const spawn = this.spawnBot(botName);
|
||||
if (spawn != null) {
|
||||
this.bots.push(spawn);
|
||||
} else {
|
||||
tries++;
|
||||
tries++
|
||||
}
|
||||
}
|
||||
return this.bots;
|
||||
}
|
||||
|
||||
private randomBotName(): string {
|
||||
const prefixIndex = this.random.nextInt(0, BOT_NAME_PREFIXES.length);
|
||||
const suffixIndex = this.random.nextInt(0, BOT_NAME_SUFFIXES.length);
|
||||
return `${BOT_NAME_PREFIXES[prefixIndex]} ${BOT_NAME_SUFFIXES[suffixIndex]}`;
|
||||
}
|
||||
|
||||
spawnBot(botName: string): SpawnIntent | null {
|
||||
const tile = this.randTile();
|
||||
const tile = this.randTile()
|
||||
if (!tile.isLand()) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
for (const spawn of this.bots) {
|
||||
if (manhattanDist(new Cell(spawn.x, spawn.y), tile.cell()) < 30) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
}
|
||||
return {
|
||||
type: "spawn",
|
||||
type: 'spawn',
|
||||
playerID: this.random.nextID(),
|
||||
name: botName,
|
||||
playerType: PlayerType.Bot,
|
||||
@@ -58,12 +52,16 @@ export class BotSpawner {
|
||||
};
|
||||
}
|
||||
|
||||
private randomBotName(): string {
|
||||
const prefixIndex = this.random.nextInt(0, BOT_NAME_PREFIXES.length);
|
||||
const suffixIndex = this.random.nextInt(0, BOT_NAME_SUFFIXES.length);
|
||||
return `${BOT_NAME_PREFIXES[prefixIndex]} ${BOT_NAME_SUFFIXES[suffixIndex]}`;
|
||||
}
|
||||
|
||||
private randTile(): Tile {
|
||||
return this.gs.tile(
|
||||
new Cell(
|
||||
this.random.nextInt(0, this.gs.width()),
|
||||
this.random.nextInt(0, this.gs.height())
|
||||
)
|
||||
);
|
||||
return this.gs.tile(new Cell(
|
||||
this.random.nextInt(0, this.gs.width()),
|
||||
this.random.nextInt(0, this.gs.height())
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
export const BOT_NAME_PREFIXES = [
|
||||
"Akkadian", "Babylonian", "Assyrian", "Sumerian", "Hittite", "Phoenician",
|
||||
"Canaanite", "Minoan", "Mycenaean", "Etruscan", "Scythian", "Thracian",
|
||||
"Dacian", "Illyrian", "Median", "Chaldean",
|
||||
"Roman", "Greek", "Byzantine", "Persian", "Parthian", "Seleucid",
|
||||
"Ptolemaic", "Palmyrene", "Macedonian", "Carthaginian",
|
||||
"Ming", "Tang", "Song", "Yuan",
|
||||
"Mauryan", "Kushan", "Rajput", "Mughal", "Satavahana", "Vijayanagara",
|
||||
"Egyptian", "Nubian", "Kushite", "Aksumite", "Ethiopian", "Songhai",
|
||||
"Malian", "Ghanaian", "Benin", "Ashanti", "Zulu", "Tuareg", "Berber",
|
||||
"Kanem-Bornu", "Buganda", "Mossi", "Swahili", "Somali", "Wolof",
|
||||
"Umayyad", "Abbasid", "Ayyubid", "Fatimid", "Mamluk", "Seljuk",
|
||||
"Safavid", "Ottoman", "Almoravid", "Almohad", "Rashidun", "Ziyarid",
|
||||
"Frankish", "Visigothic", "Ostrogothic", "Viking", "Norman", "Saxon",
|
||||
"Anglo-Saxon", "Celtic", "Gaulish", "Carolingian", "Merovingian",
|
||||
"Capetian", "Plantagenet", "Tudor", "Stuart", "Habsburg", "Romanov",
|
||||
"Lancaster", "York", "Bourbon", "Napoleonic",
|
||||
"British", "French", "Spanish", "Portuguese", "Dutch", "Russian",
|
||||
"German", "Italian", "Swedish", "Norwegian", "Danish", "Polish",
|
||||
"Hungarian", "Austrian", "Swiss", "Czech", "Slovak", "Serbian",
|
||||
"Croatian", "Bosnian", "Montenegrin", "Bulgarian", "Romanian",
|
||||
"Apache", "Sioux", "Cherokee", "Navajo", "Iroquois", "Inuit", "Arawak",
|
||||
"Carib", "Taino", "Aztec", "Mayan", "Incan", "Mapuche", "Guarani",
|
||||
"Tupi", "Yanomami", "Zuni", "Hopi", "Kiowa", "Comanche", "Shoshone",
|
||||
"Japanese", "Ryukyu", "Ainu", "Cham", "Khmer", "Thai", "Vietnamese",
|
||||
"Burmese", "Balinese", "Malay", "Filipino", "Mongolian",
|
||||
"Korean", "Tibetan", "Manchu", "Uyghur", "Hmong", "Karen", "Pyu",
|
||||
"Hawaiian", "Fijian", "Tongan", "Samoan", "Maori", "Micronesian",
|
||||
"Hebrew", "Armenian", "Georgian", "Phoenician", "Assyrian", "Chaldean",
|
||||
"Kurdish", "Turkic", "Kazakh", "Uzbek", "Kyrgyz", "Tajik", "Uighur",
|
||||
"Pashtun", "Baloch", "Afghan", "Persian",
|
||||
]
|
||||
export const BOT_NAME_SUFFIXES = [
|
||||
"Empire", "Dynasty", "Kingdom", "Sultanate", "Confederation", "Union",
|
||||
"Republic", "Caliphate", "Dominion", "Realm", "State",
|
||||
"Federation", "Territory", "Commonwealth", "League", "Duchy", "Province",
|
||||
"Protectorate", "Colony", "Mandate", "Free State","Canton", "Region", "Nation",
|
||||
"Assembly", "Hierarchy", "Archduchy", "Grand Duchy","Metropolis", "Cluster",
|
||||
"Alliance", "Tribunal", "Council", "Confederacy", "Order", "Regime",
|
||||
"Dominion", "Syndicate","Guild", "Corporation", "Patriarchy",
|
||||
"Matriarchy","Legion", "Horde", "Clan", "Brotherhood", "Sisterhood","Ascendancy", "Supremacy",
|
||||
"Province","Kingdoms", "Tribes", "Dominion", "Assembly", "Republics"
|
||||
];
|
||||
@@ -0,0 +1,33 @@
|
||||
import { MAX_USERNAME_LENGTH, MIN_USERNAME_LENGTH } from "../Util";
|
||||
|
||||
export function validateUsername(username: string): { isValid: boolean; error?: string } {
|
||||
const validPattern = /^[a-zA-Z0-9_]+$/; // Alphanumeric and underscores
|
||||
|
||||
if (typeof username !== 'string') {
|
||||
return { isValid: false, error: "Username must be a string." };
|
||||
}
|
||||
|
||||
if (username.length < MIN_USERNAME_LENGTH) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: `Username must be at least ${MIN_USERNAME_LENGTH} characters long.`,
|
||||
};
|
||||
}
|
||||
|
||||
if (username.length > MAX_USERNAME_LENGTH) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: `Username must not exceed ${MAX_USERNAME_LENGTH} characters.`,
|
||||
};
|
||||
}
|
||||
|
||||
if (!validPattern.test(username)) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: "Username can only contain letters, numbers, and underscores.",
|
||||
};
|
||||
}
|
||||
|
||||
// All checks passed
|
||||
return { isValid: true };
|
||||
}
|
||||
+17
-2
@@ -12,6 +12,7 @@ import { GamePhase, GameServer } from './GameServer';
|
||||
import { archive } from './Archive';
|
||||
import { DiscordBot } from './DiscordBot';
|
||||
import {MAX_USERNAME_LENGTH, MIN_USERNAME_LENGTH} from "../core/Util";
|
||||
import {validateUsername} from "../core/validations/username";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
@@ -148,12 +149,26 @@ wss.on('connection', (ws, req) => {
|
||||
if (Array.isArray(ip)) {
|
||||
ip = ip[0]
|
||||
}
|
||||
gm.addClient(
|
||||
const username = clientMsg.username;
|
||||
const { isValid, error } = validateUsername(username);
|
||||
if (!isValid) {
|
||||
const errorMsg = error || "Invalid username.";
|
||||
// Send error back to the client
|
||||
ws.send(JSON.stringify({
|
||||
type: 'error',
|
||||
input: 'username-input',
|
||||
message: errorMsg,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// If username is valid, add the client
|
||||
gm.addClient(
|
||||
new Client(
|
||||
clientMsg.clientID,
|
||||
clientMsg.persistentID,
|
||||
ip,
|
||||
clientMsg.username,
|
||||
username,
|
||||
ws
|
||||
),
|
||||
clientMsg.gameID,
|
||||
|
||||
Reference in New Issue
Block a user