fix: 2026-06-09 full-codebase audit hardening

Validation: shared NaN-guarded Zod coercion helpers in schemas/common.ts replace
the raw number|string transform idiom across every schema (the root-cause NaN bug
class); emailOrEmpty + lenient isoDateString/timeString.

Security: roles privilege-escalation closed; refresh-token family revocation on
reuse; TOTP uses config params; read endpoints permission-guarded; received-invoices
gross VAT on all paths; orders-pdf custom-items authz.

Concurrency: $queryRaw SELECT...FOR UPDATE locks in ascending-id order (warehouse
confirm/cancel, attendance lockUserRow); uniqueness checks moved into create
transactions (TOCTOU -> 409); deterministic id tiebreak on second-precision
timestamp ordering (plan resolveCell/resolveGrid, warehouse FIFO).

Frontend: Rules-of-Hooks fixed across ~14 pages + PlanCellModal; UTC-date persisted
fields; dashboard invalidation gaps; stale-closure confirm bugs.

Tooling/tests: ESLint flat config (react-hooks/rules-of-hooks = error) + Prettier;
tsconfig.test.json so tsc -b type-checks the tests; removed 3 dead deps; npm audit
fix (8 -> 3). Suite 195 -> 247 (happy-path auth, FIFO oldest-first, flakiness fixes),
isolated on app_test via .env.test with a hard-throw setup guard.

Gates: tsc 0 | build 0 | vitest 247/247 | eslint 0 errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-06-09 06:45:26 +02:00
parent c454d1a3fc
commit 519edce373
179 changed files with 7179 additions and 2844 deletions

View File

@@ -7,6 +7,7 @@ import prisma from "../config/database";
import { config } from "../config/env";
import { securityHeaders } from "../middleware/security";
import planRoutes from "../routes/admin/plan";
import { localDateStr } from "../utils/date";
import {
resolveCell,
resolveGrid,
@@ -31,6 +32,14 @@ import {
const N = "wh_plan_";
let adminUserId: number;
// A DEDICATED, distinct non-admin user for the service-level employee-scoping
// tests (and as the second employee in the multi-user bulk test). Creating our
// own user — instead of grabbing "the second user in the DB" — guarantees it is
// never accidentally the admin (which would make `toEqual([])` pass for the
// wrong reason on a single-user DB), and that it is genuinely distinct so the
// 2-employee aggregate assertion can't collapse to 1.
let scopeUserId: number;
let scopeRoleId: number;
let noPermUserId: number;
beforeAll(async () => {
@@ -43,11 +52,44 @@ beforeAll(async () => {
if (!admin) throw new Error("Test setup: admin user not found");
adminUserId = admin.id;
// For list-scoping tests: any non-admin user that is NOT the admin we use
// for create tests. Just pick the second user.
const allUsers = await prisma.users.findMany({ take: 2 });
noPermUserId =
allUsers.find((u) => u.id !== adminUserId)?.id ?? allUsers[0].id;
// Dedicated non-admin user + role for scoping/multi-user tests.
const stamp = Date.now();
const role = await prisma.roles.create({
data: {
name: `scope_plan_${stamp}`,
display_name: "Plan Scope Test",
},
});
scopeRoleId = role.id;
const user = await prisma.users.create({
data: {
username: `scope_plan_${stamp}`,
first_name: "Scope",
last_name: "User",
email: `scope_plan_${stamp}@test.local`,
password_hash:
"$2a$10$invalidinvalidinvalidinvalidinvalidinvalidinvalidinvali",
role_id: role.id,
is_active: true,
},
});
scopeUserId = user.id;
if (scopeUserId === adminUserId)
throw new Error("scope user must be distinct from admin");
});
afterAll(async () => {
// Clean up the dedicated scope user/role created above. Wrapped in catch so a
// leftover FK (none expected — its rows are note-prefixed and removed) can't
// mask real failures.
if (scopeUserId)
await prisma.users
.deleteMany({ where: { id: scopeUserId } })
.catch(() => {});
if (scopeRoleId)
await prisma.roles
.deleteMany({ where: { id: scopeRoleId } })
.catch(() => {});
});
beforeEach(async () => {
@@ -355,6 +397,24 @@ describe("plan.service.assertNotPastDate", () => {
const result = assertNotPastDate("2000-01-01", true);
expect(result).toBeNull();
});
it("treats TODAY (local Prague date) as allowed — the boundary is inclusive", () => {
// Deterministic without mocking the clock: compute "today" exactly the way
// the service does (local-time YYYY-MM-DD; TZ is pinned to Europe/Prague in
// config/env.ts). The boundary is the bug-prone path — an off-by-one here
// would wrongly reject editing today's plan.
const today = localDateStr(new Date());
expect(assertNotPastDate(today, false)).toBeNull();
});
it("rejects YESTERDAY (local Prague date) as a past date", () => {
const d = new Date();
d.setDate(d.getDate() - 1); // local-time yesterday
const yesterday = localDateStr(d);
const result = assertNotPastDate(yesterday, false);
expect(result).not.toBeNull();
expect(result?.status).toBe(403);
});
});
// ---------------------------------------------------------------------------
@@ -521,7 +581,7 @@ describe("plan.service.updateEntry", () => {
);
expect("data" in updated).toBe(true);
if ("data" in updated) {
expect(updated.data.note).toBe(`${N}updated`);
expect((updated.data as any).note).toBe(`${N}updated`);
expect((updated.oldData as any).note).toBe(`${N}original`);
}
});
@@ -686,7 +746,7 @@ describe("plan.service.updateOverride", () => {
);
expect("data" in updated).toBe(true);
if ("data" in updated) {
expect(updated.data.note).toBe(`${N}updated`);
expect((updated.data as any).note).toBe(`${N}updated`);
expect((updated.oldData as any).note).toBe(`${N}original`);
}
});
@@ -761,9 +821,13 @@ describe("plan.service.listEntries (employee scoping)", () => {
);
const rows = await listEntries(
{ user_id: adminUserId, date_from: "2098-02-01", date_to: "2098-02-28" },
noPermUserId,
scopeUserId, // dedicated distinct non-admin actor (never the admin)
false, // not admin
);
// A non-admin actor querying ANOTHER user's data is scoped to its own
// user_id, so it sees none of the admin's entries. This can only be
// [] for the right reason because scopeUserId !== adminUserId (asserted
// in beforeAll).
expect(rows).toEqual([]);
});
});
@@ -782,7 +846,7 @@ describe("plan.service.listOverrides (employee scoping)", () => {
);
const rows = await listOverrides(
{ user_id: adminUserId, date_from: "2098-03-01", date_to: "2098-03-31" },
noPermUserId,
scopeUserId, // dedicated distinct non-admin actor (never the admin)
false,
);
expect(rows).toEqual([]);
@@ -875,9 +939,11 @@ describe("plan.service.bulkCreateEntries", () => {
});
it("aggregates across multiple employees", async () => {
// Two genuinely distinct employees (admin + the dedicated scope user) so
// `users` can't collapse to 1 if the DB happened to have a single user.
const res = await bulkCreateEntries(
{
user_ids: [adminUserId, noPermUserId],
user_ids: [adminUserId, scopeUserId],
date_from: "2096-08-03", // Friday
date_to: "2096-08-03",
include_weekends: true,