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

@@ -2,7 +2,7 @@ import { FastifyInstance } from "fastify";
import prisma from "../../config/database";
import { requireAuth, requirePermission } from "../../middleware/auth";
import { logAudit } from "../../services/audit";
import { success, error, parseId } from "../../utils/response";
import { success, error, parseId, paginated } from "../../utils/response";
import { parsePagination, buildPaginationMeta } from "../../utils/pagination";
import { parseBody } from "../../schemas/common";
import {
@@ -29,7 +29,7 @@ export default async function attendanceRoutes(
async (request, reply) => {
const authData = request.authData!;
const data = await attendanceService.getStatus(authData.userId);
return reply.send({ success: true, data });
return success(reply, data);
},
);
@@ -105,7 +105,7 @@ export default async function attendanceRoutes(
}
const yr = Number(query.year) || new Date().getFullYear();
const data = await attendanceService.getBalances(yr);
return reply.send({ success: true, data });
return success(reply, data);
}
// --- action=workfund: monthly work fund overview ---
@@ -115,7 +115,7 @@ export default async function attendanceRoutes(
}
const yr = Number(query.year) || new Date().getFullYear();
const data = await attendanceService.getWorkfund(yr);
return reply.send({ success: true, data });
return success(reply, data);
}
// --- action=project_report: monthly project hours ---
@@ -125,7 +125,7 @@ export default async function attendanceRoutes(
}
const yr = Number(query.year) || new Date().getFullYear();
const data = await attendanceService.getProjectReport(yr);
return reply.send({ success: true, data });
return success(reply, data);
}
// --- action=print: attendance print data for admin ---
@@ -137,9 +137,13 @@ export default async function attendanceRoutes(
const monthStr = query.month
? String(query.month)
: localMonthStr(new Date());
const filterUserId = query.user_id ? Number(query.user_id) : null;
const parsedUserId = query.user_id ? Number(query.user_id) : null;
const filterUserId =
parsedUserId !== null && Number.isFinite(parsedUserId)
? parsedUserId
: null;
const data = await attendanceService.getPrintData(monthStr, filterUserId);
return reply.send({ success: true, data });
return success(reply, data);
}
// --- action=attendance_users: users with attendance.record permission ---
@@ -162,21 +166,21 @@ export default async function attendanceRoutes(
select: { id: true, first_name: true, last_name: true, username: true },
orderBy: { last_name: "asc" },
});
return reply.send({
success: true,
data: users.map((u) => ({
return success(
reply,
users.map((u) => ({
id: u.id,
first_name: u.first_name,
last_name: u.last_name,
username: u.username,
})),
});
);
}
// --- action=projects: active projects for attendance project switching ---
if (action === "projects") {
const data = await attendanceService.getActiveProjects();
return reply.send({ success: true, data });
return success(reply, data);
}
// --- action=project_logs: get project logs for a specific attendance record ---
@@ -185,28 +189,43 @@ export default async function attendanceRoutes(
return error(reply, "Nedostatečná oprávnění", 403);
}
const attendanceId = Number(query.attendance_id);
if (!attendanceId) return error(reply, "Chybí attendance_id", 400);
if (!attendanceId || !Number.isFinite(attendanceId))
return error(reply, "Chybí attendance_id", 400);
// Ownership check: a non-admin may only read logs for their own
// attendance record (mirrors the action=location path below).
const parentRecord =
await attendanceService.getLocationRecord(attendanceId);
if (!parentRecord) return error(reply, "Záznam nenalezen", 404);
const isAdmin = authData.permissions.includes("attendance.manage");
if (parentRecord.user_id !== authData.userId && !isAdmin) {
return error(reply, "Nedostatečná oprávnění", 403);
}
const data = await attendanceService.getProjectLogs(attendanceId);
return reply.send({ success: true, data });
return success(reply, data);
}
// --- action=location: single record with GPS data ---
if (action === "location") {
const id = Number(query.id);
if (!id) return error(reply, "Chybí id záznamu", 400);
if (!id || !Number.isFinite(id))
return error(reply, "Chybí id záznamu", 400);
const record = await attendanceService.getLocationRecord(id);
if (!record) return error(reply, "Záznam nenalezen", 404);
const isAdmin = authData.permissions.includes("attendance.manage");
if (record.user_id !== authData.userId && !isAdmin) {
return error(reply, "Nedostatečná oprávnění", 403);
}
return reply.send({ success: true, data: record });
return success(reply, record);
}
// --- Default: paginated records list ---
const { page, limit, skip, order } = parsePagination(query);
const isAdmin = authData.permissions.includes("attendance.manage");
const userId = query.user_id ? Number(query.user_id) : undefined;
const parsedUserId = query.user_id ? Number(query.user_id) : undefined;
const userId =
parsedUserId !== undefined && Number.isFinite(parsedUserId)
? parsedUserId
: undefined;
const result = await attendanceService.listAttendance({
page,
@@ -232,11 +251,11 @@ export default async function attendanceRoutes(
: undefined,
});
return reply.send({
success: true,
data: result.records,
pagination: buildPaginationMeta(result.total, result.page, result.limit),
});
return paginated(
reply,
result.records,
buildPaginationMeta(result.total, result.page, result.limit),
);
});
// POST /api/admin/attendance