fix: validate analytics filter ids
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@family-budget/backend",
|
"name": "@family-budget/backend",
|
||||||
"version": "0.10.1",
|
"version": "0.10.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "tsx watch src/app.ts",
|
"dev": "tsx watch src/app.ts",
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
"migrate": "tsx src/db/migrate.ts",
|
"migrate": "tsx src/db/migrate.ts",
|
||||||
"migrate:prod": "node dist/db/migrate.js",
|
"migrate:prod": "node dist/db/migrate.js",
|
||||||
"test:analytics": "tsx src/services/analyticsSemantics.test.ts",
|
"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": "tsx src/services/portfolio.test.ts",
|
||||||
"test:portfolio:db": "NODE_ENV=test tsx src/services/portfolio.integration.test.ts",
|
"test:portfolio:db": "NODE_ENV=test tsx src/services/portfolio.integration.test.ts",
|
||||||
"test:transactions": "tsx src/services/transactions.test.ts",
|
"test:transactions": "tsx src/services/transactions.test.ts",
|
||||||
|
|||||||
12
backend/src/routes/analytics.test.ts
Normal file
12
backend/src/routes/analytics.test.ts
Normal 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');
|
||||||
@@ -5,6 +5,30 @@ import type { Granularity } from '@family-budget/shared';
|
|||||||
|
|
||||||
const router = Router();
|
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(
|
router.get(
|
||||||
'/summary',
|
'/summary',
|
||||||
asyncHandler(async (req, res) => {
|
asyncHandler(async (req, res) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user