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

@@ -1,4 +1,5 @@
import prisma from "../config/database";
import { Prisma } from "@prisma/client";
import bcrypt from "bcryptjs";
import { config } from "../config/env";
@@ -60,7 +61,7 @@ export async function listUsers(params: ListUsersParams) {
const { page, limit, skip, sort, order, search, permission } = params;
const sortField = ALLOWED_SORT_FIELDS.includes(sort) ? sort : "id";
let where: any = search
const where: any = search
? {
OR: [
{ username: { contains: search } },
@@ -113,16 +114,10 @@ export async function createUser(
return { error: "Neplatný formát e-mailu", status: 400 } as const;
}
const existingUsername = await prisma.users.findFirst({
where: { username },
});
if (existingUsername) {
return { error: "Uživatelské jméno již existuje", status: 409 } as const;
}
const existingEmail = await prisma.users.findFirst({ where: { email } });
if (existingEmail) {
return { error: "E-mail již existuje", status: 409 } as const;
// Enforce min password length in-service (consistent with updateUser),
// independent of any schema-level check.
if (data.password.length < 8) {
return { error: "Heslo musí mít alespoň 8 znaků", status: 400 } as const;
}
if (data.role_id) {
@@ -139,19 +134,60 @@ export async function createUser(
config.security.bcryptCost,
);
const user = await prisma.users.create({
data: {
username,
email,
password_hash: passwordHash,
first_name: firstName,
last_name: lastName,
role_id: data.role_id ? Number(data.role_id) : null,
is_active: data.is_active !== false,
},
});
try {
// Run the uniqueness checks INSIDE the transaction with the create so two
// concurrent creates of the same username/email return the intended 409
// instead of letting the DB unique constraint throw an unhandled 500
// (mirrors updateUser).
const user = await prisma.$transaction(async (tx) => {
const existingUsername = await tx.users.findFirst({
where: { username },
});
if (existingUsername) {
throw Object.assign(new Error("Uživatelské jméno již existuje"), {
status: 409,
});
}
return { user } as const;
const existingEmail = await tx.users.findFirst({ where: { email } });
if (existingEmail) {
throw Object.assign(new Error("E-mail již existuje"), { status: 409 });
}
return tx.users.create({
data: {
username,
email,
password_hash: passwordHash,
first_name: firstName,
last_name: lastName,
role_id: data.role_id ? Number(data.role_id) : null,
is_active: data.is_active !== false,
},
});
});
return { user } as const;
} catch (err) {
if (err instanceof Error && "status" in err) {
return {
error: err.message,
status: (err as Error & { status: number }).status,
} as const;
}
// A concurrent insert that beat the in-transaction uniqueness check surfaces
// as a P2002 unique-constraint violation — return the intended 409, not 500.
if (
err instanceof Prisma.PrismaClientKnownRequestError &&
err.code === "P2002"
) {
return {
error: "Uživatelské jméno nebo e-mail již existuje",
status: 409,
} as const;
}
throw err;
}
}
export async function updateUser(