From 893984346296b89d341dd726187a400430e0534d Mon Sep 17 00:00:00 2001 From: admin Date: Mon, 24 Aug 2026 22:06:24 +0300 Subject: [PATCH] fix: validate analytics filter ids --- backend/package.json | 3 ++- backend/src/routes/analytics.test.ts | 12 ++++++++++++ backend/src/routes/analytics.ts | 24 ++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 backend/src/routes/analytics.test.ts diff --git a/backend/package.json b/backend/package.json index 1660344..9284cc0 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "@family-budget/backend", - "version": "0.10.1", + "version": "0.10.2", "private": true, "scripts": { "dev": "tsx watch src/app.ts", @@ -9,6 +9,7 @@ "migrate": "tsx src/db/migrate.ts", "migrate:prod": "node dist/db/migrate.js", "test:analytics": "tsx src/services/analyticsSemantics.test.ts", + "test:analytics:query": "tsx src/routes/analytics.test.ts", "test:portfolio": "tsx src/services/portfolio.test.ts", "test:portfolio:db": "NODE_ENV=test tsx src/services/portfolio.integration.test.ts", "test:transactions": "tsx src/services/transactions.test.ts", diff --git a/backend/src/routes/analytics.test.ts b/backend/src/routes/analytics.test.ts new file mode 100644 index 0000000..b70a9d5 --- /dev/null +++ b/backend/src/routes/analytics.test.ts @@ -0,0 +1,12 @@ +import assert from 'node:assert/strict'; +import { parseOptionalId } from './analytics'; + +assert.equal(parseOptionalId(undefined, 0), undefined); +assert.equal(parseOptionalId('0', 0), 0); +assert.equal(parseOptionalId('1', 1), 1); +assert.equal(parseOptionalId('0', 1), null); +assert.equal(parseOptionalId('abc', 0), null); +assert.equal(parseOptionalId('1.5', 0), null); +assert.equal(parseOptionalId(['1'], 0), null); + +console.log('analytics query validation: OK'); diff --git a/backend/src/routes/analytics.ts b/backend/src/routes/analytics.ts index 42065bf..98247b3 100644 --- a/backend/src/routes/analytics.ts +++ b/backend/src/routes/analytics.ts @@ -5,6 +5,30 @@ import type { Granularity } from '@family-budget/shared'; const router = Router(); +export function parseOptionalId(value: unknown, minimum: number): number | null | undefined { + if (value === undefined) return undefined; + if (typeof value !== 'string' || !/^\d+$/.test(value)) return null; + + const id = Number(value); + return Number.isSafeInteger(id) && id >= minimum ? id : null; +} + +router.use((req, res, next) => { + const accountId = parseOptionalId(req.query.accountId, 1); + if (accountId === null) { + res.status(400).json({ error: 'BAD_REQUEST', message: 'accountId must be a positive integer' }); + return; + } + + const categoryId = parseOptionalId(req.query.categoryId, 0); + if (categoryId === null) { + res.status(400).json({ error: 'BAD_REQUEST', message: 'categoryId must be a non-negative integer' }); + return; + } + + next(); +}); + router.get( '/summary', asyncHandler(async (req, res) => {