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

@@ -9,6 +9,49 @@ import {
} from "../services/numbering.service";
import prisma from "../config/database";
const YEAR = new Date().getFullYear();
/**
* Assert two consecutive document numbers (a) keep an identical format / year
* prefix and (b) increment by exactly one — without hard-coding which
* `company_settings` pattern is configured.
*
* Some patterns are entirely digits ({YY}{CODE}{NNNN}), so a regex split of
* "trailing digit run" is ambiguous. Instead we find the longest common prefix
* of the two strings (the unchanged format/year portion) and parse the
* remaining differing suffixes as integers — these are the rendered sequences.
*/
function assertStrictIncrement(num1: string, num2: string) {
expect(num1).not.toBe(num2);
expect(num2.length).toBe(num1.length); // same width ⇒ same format
let i = 0;
while (i < num1.length && num1[i] === num2[i]) i++;
const commonPrefix = num1.slice(0, i);
// The format/year portion is shared and non-empty (the numbers don't differ
// from the very first character).
expect(commonPrefix.length).toBeGreaterThan(0);
const seq1 = Number(num1.slice(i));
const seq2 = Number(num2.slice(i));
expect(Number.isNaN(seq1)).toBe(false);
expect(Number.isNaN(seq2)).toBe(false);
expect(seq2).toBe(seq1 + 1); // strict +1, not merely !==
}
/**
* Seed the (type, year) sequence row to a high `last_number` so the next
* generated numbers land far above any real order/quotation in the test DB —
* the generator's skip-taken retry then never fires and the strict +1
* assertion is deterministic.
*/
async function seedSequence(type: string, lastNumber: number) {
await prisma.number_sequences.deleteMany({ where: { type } });
await prisma.number_sequences.create({
data: { type, year: YEAR, last_number: lastNumber },
});
}
describe("generateSharedNumber", () => {
beforeEach(async () => {
await prisma.number_sequences.deleteMany({ where: { type: "shared" } });
@@ -24,10 +67,13 @@ describe("generateSharedNumber", () => {
expect(num.length).toBeGreaterThan(0);
});
it("increments on consecutive calls", async () => {
it("increments the sequence by exactly 1 and preserves the year/format", async () => {
// Seed high so neither generated number collides with a real order/project
// → the skip-taken retry never fires and the increment is strictly +1.
await seedSequence("shared", 900000);
const num1 = await generateSharedNumber();
const num2 = await generateSharedNumber();
expect(num1).not.toBe(num2);
assertStrictIncrement(num1, num2);
});
});
@@ -46,10 +92,11 @@ describe("generateOfferNumber", () => {
expect(num.length).toBeGreaterThan(0);
});
it("increments on consecutive calls", async () => {
it("increments the sequence by exactly 1 and preserves the year/format", async () => {
await seedSequence("offer", 900000);
const num1 = await generateOfferNumber();
const num2 = await generateOfferNumber();
expect(num1).not.toBe(num2);
assertStrictIncrement(num1, num2);
});
});
@@ -77,9 +124,17 @@ describe("previewSharedNumber", () => {
});
it("skips a number already taken when the sequence counter lags behind", async () => {
// Fresh counter → preview points at the first sequence number. With the
// skip-taken logic this is guaranteed free, so we can safely claim it.
// Seed the counter HIGH so the preview lands far above any real
// order/project in the test DB. Without this, a real row could already
// hold the first sequence number — making the create collide on the unique
// column, or making `second !== first` pass for the wrong reason (because
// the counter happened to already point past `first`).
await seedSequence("shared", 900000);
const first = await previewSharedNumber();
// Preconditions: the seeded preview really is free (so the create below is
// safe) and is exactly what the generator would assign next.
expect(await isSharedNumberTaken(first)).toBe(false);
const project = await prisma.projects.create({
data: { project_number: first, name: "test-preview-skip-taken" },
});
@@ -117,9 +172,14 @@ describe("previewOfferNumber", () => {
});
it("skips a number already taken when the sequence counter lags behind", async () => {
// Fresh counter → preview points at the first sequence number; with the
// skip-taken logic it's guaranteed free, so we can claim it.
// Seed HIGH so the preview lands above any real quotation in the test DB
// (see the shared-number test for the rationale): the create can't collide
// and the skip-advance is genuinely exercised rather than passing because a
// real row already advanced the preview.
await seedSequence("offer", 900000);
const first = await previewOfferNumber();
expect(await isOfferNumberTaken(first)).toBe(false);
const quotation = await prisma.quotations.create({
data: { quotation_number: first },
});