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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user