Skip to content

Commit

Permalink
* replace joi with zod so that types can be inferred
Browse files Browse the repository at this point in the history
 * replace options types with inferred zod types
 * ensure all socket listeners safeParse and reassign incoming variables to avoid accidentally using unsafe variables
 * add temporary API result type to avoid changing the client
  • Loading branch information
ConorMurphy21 committed Dec 26, 2023
1 parent 60640d4 commit d370e76
Show file tree
Hide file tree
Showing 9 changed files with 148 additions and 188 deletions.
103 changes: 15 additions & 88 deletions server/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
"cors": "^2.8.5",
"debug": "^4.3.4",
"express": "^4.18.2",
"joi": "^17.9.1",
"klaw-sync": "^6.0.0",
"locale": "^0.1.0",
"morgan": "~1.9.1",
Expand All @@ -28,7 +27,8 @@
"spellchecker": "^3.7.1",
"sprintf-js": "^1.1.3",
"winston": "^3.11.0",
"winston-cloudwatch": "^6.2.0"
"winston-cloudwatch": "^6.2.0",
"zod": "^3.22.4"
},
"devDependencies": {
"@types/chai": "^4.3.11",
Expand Down
53 changes: 36 additions & 17 deletions server/src/routes/gameHandlers.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,32 @@
import { getRoomById, Room } from '../state/rooms';
import { GameState, Stage } from '../state/gameState';
import Joi from 'joi';
import { GameState, Responses, Stage } from '../state/gameState';
import { z } from 'zod';
import logger from '../logger/logger';
import { Server, Socket } from 'socket.io';

/*** handler validation schemas ***/
import setOptionsSchema from '../types/optionsSchema';
import { isErr, isOk, isSuccess } from '../types/result';
import { ApiResult, isErr, isOk, isSuccess } from '../types/result';
import { ConfigurableOptions, getConfigurableOptionsSchema } from '../state/options';

const registerGameHandlers = (io: Server, socket: Socket) => {
/*** GAME STATE ENDPOINTS ***/
socket.on('setOptions', (options, callback) => {
socket.on('setOptions', (options: ConfigurableOptions, callback?: (p: { success: boolean }) => void) => {
const room = roomIfLeader(socket.id);
if (!room) {
logger.error('(gameHandlers) Set options attempted with no room');
return;
}
const result = setOptionsSchema.validate(options, { stripUnknown: true });
if (result.error) {
const validationResult = z
.object({ options: getConfigurableOptionsSchema(), callback: z.function().optional() })
.safeParse({ options, callback });
if (!validationResult.success) {
logger.error('(gameHandlers) Invalid options schema used');
return;
}
options = result.value;
({ options, callback } = validationResult.data);
room.state!.options = { ...room.state!.options, ...options };
io.to(room.name).emit('setOptions', room.state!.getOptions());

if (callback) callback({ success: true });
});

Expand All @@ -44,10 +47,12 @@ const registerGameHandlers = (io: Server, socket: Socket) => {
});

socket.on('promptResponse', (response: string) => {
if (Joi.string().max(60).min(1).required().validate(response).error) {
const validationResult = z.string().max(60).min(1).safeParse(response);
if (!validationResult.success) {
logger.error('(gameHandlers) Prompt Response too large');
return;
}
response = validationResult.data;
const room = getRoomById(socket.id);
if (!room) {
logger.error('(gameHandlers) PromptResponse attempted with no room');
Expand All @@ -64,10 +69,12 @@ const registerGameHandlers = (io: Server, socket: Socket) => {

// true to vote to skip, false to unvote to skip
socket.on('pollVote', (pollName: string) => {
if (Joi.string().required().validate(pollName).error) {
const validationResult = z.string().safeParse(pollName);
if (!validationResult.success) {
logger.error('(gameHandlers) PollVote invalid format');
return;
}
pollName = validationResult.data;
const room = getRoomById(socket.id);
if (!room) {
logger.error('(gameHandlers) PollVote attempted with no room');
Expand All @@ -87,10 +94,12 @@ const registerGameHandlers = (io: Server, socket: Socket) => {
});

socket.on('selectSelectionType', (isStrike: boolean) => {
if (Joi.boolean().required().validate(isStrike).error) {
const validationResult = z.boolean().safeParse(isStrike);
if (!validationResult.success) {
logger.error('(gameHandlers) isStrike invalid format');
return;
}
isStrike = validationResult.data;
const room = getRoomById(socket.id);
if (!room) {
logger.error('(gameHandlers) selectSelectionType attempted with no room');
Expand All @@ -106,10 +115,13 @@ const registerGameHandlers = (io: Server, socket: Socket) => {
});

socket.on('selectResponse', (response: string) => {
if (Joi.string().max(60).min(1).required().validate(response).error) {
const validationResult = z.string().max(60).min(1).safeParse(response);
if (!validationResult.success) {
logger.error('(gameHandlers) selectResponse attempted with invalid match');
return;
}
response = validationResult.data;

const room = getRoomById(socket.id);
if (!room) {
logger.error('(gameHandlers) selectResponse attempted with no room');
Expand All @@ -125,10 +137,12 @@ const registerGameHandlers = (io: Server, socket: Socket) => {
});

socket.on('selectMatch', (match: string) => {
if (Joi.string().max(60).allow('').required().validate(match).error) {
const validationResult = z.string().max(60).safeParse(match);
if (!validationResult.success) {
logger.error('(gameHandlers) selectMatch attempted with invalid match');
return;
}
match = validationResult.data;
const room = getRoomById(socket.id);
if (!room) {
logger.error('(gameHandlers) selectMatch attempted with no room');
Expand Down Expand Up @@ -158,11 +172,14 @@ const registerGameHandlers = (io: Server, socket: Socket) => {
}
});

socket.on('getResponses', (id, callback) => {
if (!callback || !Joi.string().required().validate(id)) {
logger.error('(gameHandlers) getResponse attempted with invalid arguments');
// todo: change client side to accept Result type instead of this dumb ApiResult
socket.on('getResponses', (id, callback: (result: ApiResult<Responses>) => void) => {
const validationResult = z.object({ id: z.string(), callback: z.function() }).safeParse({ id, callback });
if (!validationResult.success) {
logger.error('(gameHandlers) getResponses attempted with invalid arguments');
return;
}
({ id, callback } = validationResult.data);
const room = getRoomById(socket.id);
if (!room) {
logger.error('(gameHandlers) getResponses attempted with no room');
Expand All @@ -172,8 +189,10 @@ const registerGameHandlers = (io: Server, socket: Socket) => {
const result = state.getResponses(id);
if (isErr(result)) {
logger.log(result.wrap('(gameHandlers) getResponses failed due to %1$s'));
callback({ success: true, ...result });
} else {
callback({ success: true, ...result });
}
if (callback) callback(result);
});
};

Expand Down
29 changes: 16 additions & 13 deletions server/src/routes/roomHandlers.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import Joi from 'joi';
import { createRoom, disconnectPlayer, getRoomById, getRoomByName, joinRoom } from '../state/rooms';
import logger from '../logger/logger';
import { midgameJoin } from './gameHandlers';
import { Server, Socket } from 'socket.io';
import { isErr } from '../types/result';
import { Stage } from '../state/gameState';
import { z } from 'zod';

/*** handler validation schemas ***/
const roomSchema = Joi.object({
name: Joi.string().allow(''),
roomName: Joi.string().allow(''),
langs: Joi.array().items(Joi.string().min(2).max(5))
}).required();

const roomSchema = z.object({
name: z.string(),
roomName: z.string(),
langs: z.array(z.string().min(2).max(5)).optional()
});
export function registerRoomHandlers(io: Server, socket: Socket): void {
socket.onAny(() => {
// update activity
Expand All @@ -23,13 +22,16 @@ export function registerRoomHandlers(io: Server, socket: Socket): void {
});

/*** CONNECTION AND ROOM CREATION ***/
socket.on('createRoom', (name: string, roomName: string, langs: string) => {
socket.on('createRoom', (name: string, roomName: string, langs?: string[]) => {
// disconnect for cleanup
disconnect(socket);

const validateResult = roomSchema.validate({ name, roomName, langs });
if (validateResult.error) return;

const validateResult = roomSchema.safeParse({ name, roomName, langs });
if (!validateResult.success) {
logger.warn(`(roomHandlers) Missing handling for room creation: ${validateResult.error.message}`);
return;
}
({ name, roomName, langs } = validateResult.data);
const result = createRoom(socket.id, name, roomName, langs);
// store name in session variable
if (isErr(result)) {
Expand All @@ -49,8 +51,9 @@ export function registerRoomHandlers(io: Server, socket: Socket): void {
// disconnect for cleanup
disconnect(socket);

const validateResult = roomSchema.validate({ name, roomName, langs: [] });
if (validateResult.error) return;
const validateResult = roomSchema.safeParse({ name, roomName, langs: [] });
if (!validateResult.success) return;
({ name, roomName } = validateResult.data);

const result = joinRoom(socket.id, name, roomName);
if (isErr(result)) {
Expand Down
Loading

0 comments on commit d370e76

Please sign in to comment.