validate usernames on change, fix public lobby highlight bug, validate

strings with zod for safety
This commit is contained in:
Evan
2025-01-02 09:02:23 -08:00
parent 239f5f11dc
commit f6b739e263
10 changed files with 121 additions and 209 deletions
+15 -89
View File
@@ -12,8 +12,6 @@ import { JoinPrivateLobbyModal } from "./JoinPrivateLobbyModal";
import { generateID } from "../core/Util";
import { generateCryptoRandomUUID } from "./Utils";
import { consolex } from "../core/Consolex";
import {validateUsername} from "../core/validations/username";
import {PublicLobby} from "./PublicLobby";
class Client {
private gameStop: () => void
@@ -26,7 +24,6 @@ class Client {
initialize(): void {
this.usernameInput = document.querySelector('username-input') as UsernameInput;
const usernameValidation = document.getElementById('username-error');
if (!this.usernameInput) {
consolex.warn('Username input element not found');
}
@@ -42,104 +39,38 @@ class Client {
document.addEventListener('leave-lobby', this.handleLeaveLobby.bind(this));
document.addEventListener('single-player', this.handleSinglePlayer.bind(this));
const spModal = document.querySelector('single-player-modal') as SinglePlayerModal;
spModal instanceof SinglePlayerModal
document.getElementById('single-player').addEventListener('click', async () => {
const username = this.usernameInput?.getCurrentUsername();
if (!username) {
usernameValidation.textContent = 'Username is required';
return;
}
const isValid = await this.validateUsername(username);
if (isValid) {
document.getElementById('single-player').addEventListener('click', () => {
if (this.usernameInput.isValid()) {
spModal.open();
} else {
return;
}
});
})
const hostModal = document.querySelector('host-lobby-modal') as HostPrivateLobbyModal;
hostModal instanceof HostPrivateLobbyModal
document.getElementById('host-lobby-button').addEventListener('click', async () => {
const username = this.usernameInput?.getCurrentUsername();
if (!username) {
usernameValidation.textContent = 'Username is required';
return;
}
const isValid = await this.validateUsername(username);
if (isValid) {
document.getElementById('host-lobby-button').addEventListener('click', () => {
if (this.usernameInput.isValid()) {
hostModal.open();
} else {
return;
}
});
})
this.joinModal = document.querySelector('join-private-lobby-modal') as JoinPrivateLobbyModal;
this.joinModal instanceof JoinPrivateLobbyModal
document.getElementById('join-private-lobby-button').addEventListener('click', async () => {
const username = this.usernameInput?.getCurrentUsername();
if (!username) {
usernameValidation.textContent = 'Username is required';
return;
}
const isValid = await this.validateUsername(username);
if (isValid) {
document.getElementById('join-private-lobby-button').addEventListener('click', () => {
if (this.usernameInput.isValid()) {
this.joinModal.open();
}else {
return;
}
});
})
}
private async validateUsername(username: string): Promise<boolean> {
this.usernameInput.validationError = '';
try {
const { isValid, error } = validateUsername(username);
if (!isValid) {
this.usernameInput.validationError = error || 'Failed to validate username.';
return false;
}
return true;
} catch (error) {
consolex.error('Error validating username:', error);
this.usernameInput.validationError = 'An error occurred while validating the username. Please try again.';
return false;
}
}
private async handleJoinLobby(event: CustomEvent) {
const lobby = event.detail.lobby;
consolex.log(`Attempting to join lobby ${lobby.id}`);
// Validate the username
const username = this.usernameInput?.getCurrentUsername();
if (!username) {
this.usernameInput.validationError = 'Username is required';
return;
}
const isValid = await this.validateUsername(username);
if (!isValid) {
return;
}
// Stop existing game
const lobby = event.detail.lobby
consolex.log(`joining lobby ${lobby.id}`)
if (this.gameStop != null) {
consolex.log('Stopping existing game before joining a new lobby');
this.gameStop();
consolex.log('joining lobby, stopping existing game')
this.gameStop()
}
this.gameStop = joinLobby(
{
@@ -154,14 +85,9 @@ class Client {
},
() => this.joinModal.close()
);
const publicLobbyElement = document.querySelector('public-lobby') as PublicLobby;
publicLobbyElement.highlightLobby();
}
private async handleLeaveLobby(event: CustomEvent) {
const publicLobbyElement = document.querySelector('public-lobby') as PublicLobby;
publicLobbyElement.highlightLobby();
if (this.gameStop == null) {
return
}
@@ -214,4 +140,4 @@ export function getPersistentIDFromCookie(): string {
].join(';')
return newID
}
}
+2 -6
View File
@@ -104,13 +104,9 @@ export class PublicLobby extends LitElement {
</button>
`;
}
highlightLobby() {
this.isLobbyHighlighted = !this.isLobbyHighlighted;
this.requestUpdate();
}
private lobbyClicked(lobby: Lobby) {
this.isLobbyHighlighted = !this.isLobbyHighlighted;
if (this.currLobby == null) {
this.currLobby = lobby
this.dispatchEvent(new CustomEvent('join-lobby', {
@@ -132,4 +128,4 @@ export class PublicLobby extends LitElement {
this.currLobby = null
}
}
}
}
+4 -29
View File
@@ -5,10 +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";
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) { }
@@ -185,14 +185,6 @@ export class Transport {
this.socket.onmessage = (event: MessageEvent) => {
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);
@@ -257,23 +249,6 @@ 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",
+37 -24
View File
@@ -1,7 +1,7 @@
import {LitElement, html, css} from 'lit';
import {customElement, property, state} from 'lit/decorators.js';
import {v4 as uuidv4} from 'uuid';
import {MAX_USERNAME_LENGTH} from "../core/Util";
import { LitElement, html, css } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { v4 as uuidv4 } from 'uuid';
import { MAX_USERNAME_LENGTH, validateUsername } from '../core/validations/username';
const usernameKey: string = 'username';
@@ -10,6 +10,8 @@ export class UsernameInput extends LitElement {
@state() private username: string = '';
@property({ type: String }) validationError: string = '';
private _isValid: boolean = true
static styles = css`
input {
width: 100%;
@@ -22,14 +24,12 @@ export class UsernameInput extends LitElement {
line-height: 1.5;
color: #111827;
}
input:focus {
outline: none;
ring: 2px;
ring-color: #3b82f6;
border-color: #3b82f6;
}
.error {
color: #dc2626;
background-color: #fff;
@@ -47,30 +47,38 @@ export class UsernameInput extends LitElement {
connectedCallback() {
super.connectedCallback();
this.username = this.getStoredUsername();
this.dispatchUsernameEvent()
this.dispatchUsernameEvent();
}
render() {
return html`
<input
type="text"
.value=${this.username}
@input=${this.handleInput}
placeholder="Enter your username"
maxlength="${MAX_USERNAME_LENGTH}"
>
${this.validationError
? html`<div class="error">${this.validationError}</div>`
: null}
`;
<input
type="text"
.value=${this.username}
@input=${this.handleChange}
@change=${this.handleChange}
placeholder="Enter your username"
maxlength="${MAX_USERNAME_LENGTH}"
>
${this.validationError
? html`<div class="error">${this.validationError}</div>`
: null}
`;
}
private handleInput(e: Event) {
private handleChange(e: Event) {
const input = e.target as HTMLInputElement;
this.username = input.value.trim();
this.storeUsername(this.username);
this.validationError = '';
this.dispatchUsernameEvent();
const result = validateUsername(this.username)
this._isValid = result.isValid
if (result.isValid) {
this.storeUsername(this.username);
this.validationError = ''
} else {
this.validationError = result.error
}
}
private getStoredUsername(): string {
@@ -89,7 +97,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
}));
@@ -108,4 +116,9 @@ export class UsernameInput extends LitElement {
const threeDigits = decimal % 1000n;
return threeDigits.toString().padStart(3, '0');
}
}
public isValid(): boolean {
return true
return this._isValid
}
}