Compare commits

...

5 Commits

4 changed files with 44 additions and 1 deletions

View File

@@ -1,5 +1,11 @@
# Changelog
## [Backend 0.10.2] - 2026-08-24
### Fixed
- Analytics endpoints now reject invalid account and category filter IDs with a validation error.
## [Frontend 0.11.1] - 2026-08-21
### Fixed

View File

@@ -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",

View File

@@ -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');

View File

@@ -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) => {