Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
f2e7154
api: Add runtime input validation
bperel Sep 7, 2026
e20a7d4
Merge branch 'master' into api-runtime-input-validation
bperel Sep 16, 2026
056ad85
Add runtime input validation
bperel Sep 16, 2026
59d11d5
Merge branch 'master' into api-runtime-input-validation
bperel Sep 16, 2026
559e76e
Add runtime input validation
bperel Sep 16, 2026
3121a06
Merge branch 'master' into api-runtime-input-validation
bperel Sep 21, 2026
a678ea5
dumili-api: Refactor release date validation logic
bperel Sep 21, 2026
9447fba
dumili-api: Refactor filters validation in getEdges function
bperel Sep 21, 2026
25552ce
Add runtime input validation
bperel Sep 21, 2026
28c5d1a
Add runtime input validation
bperel Sep 21, 2026
553d6df
Add runtime input validation
bperel Sep 22, 2026
cd4f47d
Add runtime input validation
bperel Sep 22, 2026
caa8704
Merge branch 'master' into api-runtime-input-validation
bperel Sep 30, 2026
66b7d03
Regenerate lockfiles
bperel Sep 30, 2026
429ad3e
Add runtime input validation
bperel Sep 30, 2026
9383b23
Add runtime input validation
bperel Sep 30, 2026
fd961e7
Add runtime input validation
bperel Sep 30, 2026
06a647f
duckguessr-api,dumili-api,edgecreator-api,api: Add some validation ch…
bperel Sep 30, 2026
9301476
Add runtime input validation
bperel Sep 30, 2026
8f8b60e
Merge remote-tracking branch 'origin/master' into api-runtime-input-v…
bperel Sep 30, 2026
6f4edf1
Add runtime input validation
bperel Sep 30, 2026
c30a8db
Remove unused types, don't export types unused elsewhere
bperel Sep 30, 2026
09f5d26
Merge remote-tracking branch 'origin/master' into api-runtime-input-v…
bperel Sep 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/duckguessr/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@
"jsonwebtoken": "^9.0.3",
"prisma": "^7.10.0",
"sharp": "^0.33.5",
"socket-call-server": "^0.9.0",
"socket-call-server": "^0.12.0",
"socket.io": "^4.8.3",
"valibot": "^1.5.0",
"~api": "workspace:*",
"~dm-types": "workspace:*",
"~prisma-schemas": "workspace:*"
Expand Down
32 changes: 27 additions & 5 deletions apps/duckguessr/api/pnpm-lock.yaml

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

83 changes: 40 additions & 43 deletions apps/duckguessr/api/services/datasets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { prismaClient as prismaCoa } from "~prisma-schemas/schemas/coa/client";

import prisma from "../prisma/client";
import namespaces from "./namespaces";
import { ev } from "socket-call-server/valibot";
import * as v from "valibot";

const listenEvents = () => ({
getDatasets: async () => prisma.$queryRaw<
Expand All @@ -26,36 +28,34 @@ const listenEvents = () => ({
AND decision = 'ok'
GROUP BY dataset.name`,

previewDataset: async ({
personNationalityFilter,
oldestDateFilterMin: _oldestDateFilterMin,
oldestDateFilterMax: _oldestDateFilterMax,
}: {
personNationalityFilter: string[] | undefined;
oldestDateFilterMin: number | undefined;
oldestDateFilterMax: number | undefined;
}) => {
const errors: string[] = [];
if (personNationalityFilter && !personNationalityFilter.length) {
errors.push(
"At least one nationality is required when using the nationality filter",
);
}
if (errors.length) {
return {
errors,
datasetSize: 0,
} as const;
}
return prismaCoa.$queryRaw<
[
{
datasetSize: number;
samples: string;
authors: string;
},
]
>`
previewDataset: ev(
v.pipe(
v.object({
personNationalityFilter: v.optional(v.array(v.string())),
oldestDateFilterMin: v.optional(v.number()),
oldestDateFilterMax: v.optional(v.number()),
}),
v.check(
({ personNationalityFilter }) =>
!personNationalityFilter || personNationalityFilter.length > 0,
"At least one nationality is required when using the nationality filter" as const,
),
),
)(
async ({
personNationalityFilter,
oldestDateFilterMin: _oldestDateFilterMin,
oldestDateFilterMax: _oldestDateFilterMax,
}) =>
prismaCoa.$queryRaw<
[
{
datasetSize: number;
samples: string;
authors: string;
},
]
>`
with dataset as (select sitecode, url, REPLACE(artsummary, ',', '') as personcode
from inducks_entryurl
inner join inducks_entry using (entrycode)
Expand Down Expand Up @@ -89,19 +89,16 @@ const listenEvents = () => ({
)
select datasetSize, samples, authors
from dataset_stats, authors_list
`.then(
([result]) =>
({
datasetSize: result.datasetSize,
authors: JSON.parse(result.authors),
samples: JSON.parse(result.samples),
}) as {
datasetSize: number;
samples: { url: string; personcode: string }[];
authors: Record<string, number>;
},
);
},
`.then<{
datasetSize: number;
samples: { url: string; personcode: string }[];
authors: Record<string, number>;
}>(([result]) => ({
datasetSize: result.datasetSize,
authors: JSON.parse(result.authors),
samples: JSON.parse(result.samples),
})),
),
});

const { client, server } = useSocketEvents<
Expand Down
10 changes: 6 additions & 4 deletions apps/duckguessr/api/services/game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import namespaces from "./namespaces";
import { prismaClient as prismaCoa } from "~prisma-schemas/schemas/coa/client";

import { io } from "..";
import { ev } from "socket-call-server/valibot";
import * as v from "valibot";

export type ClientListenEvents = {
playerJoined: (player: player) => void;
Expand Down Expand Up @@ -342,7 +344,7 @@ const listenEvents = (gameServices: GameServices) => {
_socket.broadcast.emit("matchStarts");
gameServices.matchStarts();
},
guess: async (personcode: string | null) => {
guess: ev(v.nullable(v.string()))(async (personcode) => {
const haveAllPlayersGuessed = await onGuess(
gameServices,
_socket.data.user,
Expand All @@ -351,8 +353,8 @@ const listenEvents = (gameServices: GameServices) => {
if (haveAllPlayersGuessed) {
return haveAllPlayersGuessed;
}
},
disconnect: async (reason: string) => {
}),
disconnect: ev(v.string())(async (reason) => {
if (reason !== "client namespace disconnect") {
if (
_socket &&
Expand All @@ -367,7 +369,7 @@ const listenEvents = (gameServices: GameServices) => {
);
}
}
},
}),
};
};

Expand Down
6 changes: 4 additions & 2 deletions apps/duckguessr/api/services/home.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { useSocketEvents } from "socket-call-server";

import prisma from "../prisma/client";
import namespaces from "./namespaces";
import { ev } from "socket-call-server/valibot";
import * as v from "valibot";

const convertUrlToBase64 = async (url: string): Promise<string | null> => {
try {
Expand All @@ -22,7 +24,7 @@ const convertUrlToBase64 = async (url: string): Promise<string | null> => {
};

const listenEvents = () => ({
getGameRounds: async (gameId: number) => {
getGameRounds: ev(v.number())(async (gameId) => {
const round = await prisma.round.findFirst({
include: {
roundScores: true,
Expand Down Expand Up @@ -66,7 +68,7 @@ const listenEvents = () => ({
roundNumber: round.roundNumber,
base64,
};
},
}),
});

export const { client, server } = useSocketEvents<
Expand Down
32 changes: 20 additions & 12 deletions apps/duckguessr/api/services/maintenance.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { useSocketEvents } from "socket-call-server";
import { ev } from "socket-call-server/valibot";
import * as v from "valibot";

import prisma from "../prisma/client";
import { type entryurlDetailsDecision } from "../prisma/client_duckguessr/browser";
import { entryurlDetailsDecision } from "../prisma/client_duckguessr/browser";
import namespaces from "./namespaces";
import { RequiredPlayerMiddleware } from "../middlewares/required-player";

Expand All @@ -16,13 +18,13 @@ const listenEvents = () => ({
group by dataset_id, decision
`,

getMaintenanceDataForDataset: async (
datasetName: string,
decisions: (entryurlDetailsDecision | "null")[],
offset: number,
) => {
if (!decisions) {
throw new Error("No decisions provided");
getMaintenanceDataForDataset: ev(
v.string(),
v.array(v.enum({ ...entryurlDetailsDecision, null: "null" })),
v.number(),
)(async (datasetName, decisions, offset) => {
if (!decisions.length) {
return [];
}
const dataset = await prisma.dataset.findUnique({
where: {
Expand Down Expand Up @@ -53,10 +55,15 @@ const listenEvents = () => ({
sitecodeUrl: "asc",
},
});
},
updateMaintenanceData: async (
data: { sitecodeUrl: string; decision: entryurlDetailsDecision }[],
) =>
}),
updateMaintenanceData: ev(
v.array(
v.object({
sitecodeUrl: v.string(),
decision: v.enum(entryurlDetailsDecision),
}),
),
)(async (data) =>
prisma.$transaction(
data.map(({ sitecodeUrl, decision }) =>
prisma.entryurlDetails.update({
Expand All @@ -70,6 +77,7 @@ const listenEvents = () => ({
}),
),
),
),
});

const { client, server } = useSocketEvents<
Expand Down
6 changes: 4 additions & 2 deletions apps/duckguessr/api/services/match.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import type { SessionUser } from "../types/SessionUser";
import namespaces from "./namespaces";
import { RequiredPlayerMiddleware } from "../middlewares/required-player";
import { createGameSocket } from "./game";
import { ev } from "socket-call-server/valibot";
import * as v from "valibot";

type MatchServices = NamespaceProxyTarget<
Socket<
Expand All @@ -20,12 +22,12 @@ type MatchServices = NamespaceProxyTarget<
>;

const listenEvents = ({ _socket }: MatchServices) => ({
createMatch: async (dataset: string) => {
createMatch: ev(v.string())(async (dataset) => {
console.log(`${_socket.data.user.username} is creating a match`);
const newGame = (await game.create(dataset))!;
await createGameSocket(newGame.id);
return newGame.id;
},
}),
});

const { client, server } = useSocketEvents<
Expand Down
18 changes: 10 additions & 8 deletions apps/duckguessr/api/services/player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import { type player } from "../prisma/client_duckguessr/browser";
import namespaces from "./namespaces";
import { RequiredPlayerMiddleware } from "../middlewares/required-player";

import { ev } from "socket-call-server/valibot";
import * as v from "valibot";

export type ClientListenEvents = {
logged: (player: player) => void;
loginFailed: () => void;
Expand All @@ -25,14 +28,13 @@ type PlayerServices = NamespaceProxyTarget<
>;

const listenEvents = ({ _socket }: PlayerServices) => ({
getPlayer: () => {
return Promise.resolve(_socket.data.user);
},
getPlayer: () => Promise.resolve(_socket.data.user),

updateUser: async ({ avatar }: player) =>
updateUser: ev(v.object({ avatar: v.string() }))(async ({ avatar }) =>
updatePlayer(_socket.data.user.id, { avatar }),
),

getStats: async (gameId?: number) => {
getStats: ev(v.optional(v.number()))(async (gameId) => {
const playerIdsToQuery = [_socket.data.user.id];
if (gameId) {
playerIdsToQuery.push(
Expand All @@ -46,9 +48,9 @@ const listenEvents = ({ _socket }: PlayerServices) => ({
);
}
return await getPlayerStatistics(playerIdsToQuery);
},
}),

getGameStats: async (gameId: number) => {
getGameStats: ev(v.number())(async (gameId: number) => {
const playerIdsToQuery = [_socket.data.user.id];
if (gameId) {
playerIdsToQuery.push(
Expand All @@ -63,7 +65,7 @@ const listenEvents = ({ _socket }: PlayerServices) => ({
}
const stats = await getPlayerStatistics(playerIdsToQuery);
return { gameId, stats };
},
}),
});

const { client, server } = useSocketEvents<
Expand Down
Loading
Loading