fix: audit fix pass #1 — all 19 verified HIGH findings + critical dep cleanup

Fixes every CRITICAL/HIGH finding from the 2026-06-09 full-codebase audit
(REVIEW_FINDINGS.md); each fix went through independent spec + code-quality
review. Plan and per-task log: docs/superpowers/plans/2026-06-10-audit-high-fix-pass.md

- attendance: schemas accept the combined local datetimes the forms/service
  use (new dateTimeString helpers in schemas/common.ts), breaks persist on
  create, AttendanceCreate submit rebuilt — every submit 400'd since 519edce
- 2fa: backup codes wired to /totp/backup-verify (+ remember-me parity),
  enrollment QR generated locally via qrcode (CSP-blocked external service
  also leaked the secret), dashboard shows per-user enrollment, not policy
- invoices/orders: per-line VAT survives re-saves (numberOr 0-respecting
  coercion in formatters.ts), billing_text persists on update, issued-order
  status transitions update UI gates
- trips: real pagination on all 3 pages, GET /trips/stats server aggregate
  (shared buildTripsWhere + legacy distance coalesce), vehicle_id applies on
  PUT with both-vehicle odometer recompute, print rebuilt (sync window.open,
  escaped template, server totals)
- orders api: attachment_data PDF blob excluded from all non-binary reads
- warehouse: unit field is a Select over UnitEnum, receipt attachments
  downloadable via new authenticated GET route
- downloads: shared RFC 5987 contentDisposition helper — Czech filenames no
  longer 500 (warehouse, received-invoices, orders endpoints)
- misc: block-env hook actually blocks (exit 2 + stderr), project create
  works with empty dates, NaN filter guards on trips endpoints
- deps: remove unused concurrently (clears both critical advisories), pin
  @hono/node-server >=1.19.13 via overrides (clears the 3 moderates without
  the Prisma 6 downgrade), drop deprecated @types stubs

Gates: tsc -b clean - vitest 30 files / 342 tests (31 new) - eslint 0 errors
- build OK

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-06-10 09:59:47 +02:00
parent d08e55a41a
commit 1826fc7976
44 changed files with 2812 additions and 622 deletions

View File

@@ -0,0 +1,309 @@
import { describe, it, expect, beforeAll, afterAll, beforeEach } from "vitest";
import Fastify from "fastify";
import cookie from "@fastify/cookie";
import rateLimit from "@fastify/rate-limit";
import jwt from "jsonwebtoken";
import prisma from "../config/database";
import { config } from "../config/env";
import { securityHeaders } from "../middleware/security";
import attendanceRoutes from "../routes/admin/attendance";
// ---------------------------------------------------------------------------
// Regression tests for the attendance create/edit wire format.
//
// The admin create/edit forms send COMBINED local datetimes
// ("YYYY-MM-DDTHH:MM:00", built by combineDatetime from a date + time input)
// for arrival_time / departure_time / break_start / break_end, and the
// service parses them with `new Date(...)`. Commit 519edce validated these
// fields with `timeString` (bare HH:MM), which rejected every create/edit
// containing a time, and CreateAttendanceSchema lacked break_start/break_end
// entirely, so breaks were silently stripped on create. These tests pin the
// combined-datetime contract at the HTTP route level.
// ---------------------------------------------------------------------------
// All fixtures live on far-future dates so they can never collide with real
// rows in the throwaway test DB; cleanup deletes exactly this range.
const RANGE_START = new Date("2098-01-01T00:00:00");
const RANGE_END = new Date("2099-01-01T00:00:00");
let app: Awaited<ReturnType<typeof buildApp>>;
let adminUserId: number;
let adminToken: string;
async function buildApp() {
const a = Fastify({ logger: false });
await a.register(cookie);
await a.register(rateLimit, { max: 1000, timeWindow: "1 minute" });
a.addHook("onRequest", securityHeaders);
await a.register(attendanceRoutes, { prefix: "/api/admin/attendance" });
return a;
}
/**
* Generate a JWT access token. `requireAuth` re-loads auth data from the DB
* via `loadAuthData(payload.sub)`, so only the `sub` (user id) matters here.
*/
function generateToken(user: {
id: number;
username: string;
roleName: string | null;
}): string {
return jwt.sign(
{ sub: user.id, username: user.username, role: user.roleName },
config.jwt.secret,
{ expiresIn: "15m" },
);
}
async function authPost(path: string, token: string, body: unknown) {
return app.inject({
method: "POST",
url: path,
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
payload: body as object,
});
}
async function authPut(path: string, token: string, body: unknown) {
return app.inject({
method: "PUT",
url: path,
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
payload: body as object,
});
}
async function cleanupFixtureRows() {
await prisma.attendance.deleteMany({
where: {
user_id: adminUserId,
shift_date: { gte: RANGE_START, lt: RANGE_END },
},
});
}
beforeAll(async () => {
app = await buildApp();
const admin = await prisma.users.findFirst({
where: { roles: { name: "admin" } },
include: { roles: true },
});
if (!admin) throw new Error("Test setup: admin user not found");
adminUserId = admin.id;
adminToken = generateToken({
id: admin.id,
username: admin.username,
roleName: admin.roles?.name ?? null,
});
await cleanupFixtureRows();
});
beforeEach(async () => {
await cleanupFixtureRows();
});
afterAll(async () => {
await cleanupFixtureRows();
if (app) await app.close();
});
describe("POST /api/admin/attendance (combined datetimes)", () => {
it("creates a work record with arrival+departure datetimes and persists them", async () => {
const arrival = "2098-03-10T08:00:00";
const departure = "2098-03-10T16:30:00";
const res = await authPost("/api/admin/attendance", adminToken, {
user_id: adminUserId,
shift_date: "2098-03-10",
leave_type: "work",
arrival_time: arrival,
departure_time: departure,
notes: "attendance_wire_test work",
});
expect(res.statusCode).toBe(201);
const body = res.json();
expect(body.success).toBe(true);
const rec = await prisma.attendance.findUnique({
where: { id: body.data.id },
});
expect(rec).toBeTruthy();
expect(rec!.leave_type).toBe("work");
expect(rec!.arrival_time?.getTime()).toBe(new Date(arrival).getTime());
expect(rec!.departure_time?.getTime()).toBe(new Date(departure).getTime());
});
it("persists break_start/break_end on create (previously silently stripped)", async () => {
const res = await authPost("/api/admin/attendance", adminToken, {
user_id: adminUserId,
shift_date: "2098-03-11",
leave_type: "work",
arrival_time: "2098-03-11T08:00:00",
departure_time: "2098-03-11T16:30:00",
break_start: "2098-03-11T12:00:00",
break_end: "2098-03-11T12:30:00",
notes: "attendance_wire_test break",
});
expect(res.statusCode).toBe(201);
const body = res.json();
expect(body.success).toBe(true);
const rec = await prisma.attendance.findUnique({
where: { id: body.data.id },
});
expect(rec!.break_start?.getTime()).toBe(
new Date("2098-03-11T12:00:00").getTime(),
);
expect(rec!.break_end?.getTime()).toBe(
new Date("2098-03-11T12:30:00").getTime(),
);
});
it("creates a leave record with NO time fields", async () => {
const res = await authPost("/api/admin/attendance", adminToken, {
user_id: adminUserId,
shift_date: "2098-03-12",
leave_type: "vacation",
leave_hours: 8,
notes: "attendance_wire_test leave",
});
expect(res.statusCode).toBe(201);
const body = res.json();
expect(body.success).toBe(true);
const rec = await prisma.attendance.findUnique({
where: { id: body.data.id },
});
expect(rec!.leave_type).toBe("vacation");
expect(Number(rec!.leave_hours)).toBe(8);
expect(rec!.arrival_time).toBeNull();
expect(rec!.departure_time).toBeNull();
});
it('tolerates "" for unset datetime fields (old form payloads) → null', async () => {
// The AttendanceCreate page historically always sent empty strings for
// the unfilled time fields — even for leave records — which 400'd EVERY
// submit after 519edce. The schema must treat "" as "not set".
const res = await authPost("/api/admin/attendance", adminToken, {
user_id: adminUserId,
shift_date: "2098-03-13",
leave_type: "sick",
leave_hours: 8,
arrival_time: "",
departure_time: "",
break_start: "",
break_end: "",
notes: "attendance_wire_test empty strings",
});
expect(res.statusCode).toBe(201);
const body = res.json();
expect(body.success).toBe(true);
const rec = await prisma.attendance.findUnique({
where: { id: body.data.id },
});
expect(rec!.leave_type).toBe("sick");
expect(rec!.arrival_time).toBeNull();
expect(rec!.departure_time).toBeNull();
expect(rec!.break_start).toBeNull();
expect(rec!.break_end).toBeNull();
});
});
describe("PUT /api/admin/attendance/:id (combined datetimes)", () => {
it("updates a record with combined datetimes (incl. overnight departure)", async () => {
const created = await prisma.attendance.create({
data: {
user_id: adminUserId,
shift_date: new Date(2098, 2, 14, 12, 0, 0),
leave_type: "work",
arrival_time: new Date("2098-03-14T08:00:00"),
departure_time: new Date("2098-03-14T16:30:00"),
notes: "attendance_wire_test update",
},
});
const res = await authPut(
`/api/admin/attendance/${created.id}`,
adminToken,
{
leave_type: "work",
arrival_time: "2098-03-14T22:00:00",
departure_time: "2098-03-15T06:00:00",
break_start: "2098-03-15T02:00:00",
break_end: "2098-03-15T02:30:00",
notes: "attendance_wire_test updated",
},
);
expect(res.statusCode).toBe(200);
const body = res.json();
expect(body.success).toBe(true);
const rec = await prisma.attendance.findUnique({
where: { id: created.id },
});
expect(rec!.arrival_time?.getTime()).toBe(
new Date("2098-03-14T22:00:00").getTime(),
);
expect(rec!.departure_time?.getTime()).toBe(
new Date("2098-03-15T06:00:00").getTime(),
);
expect(rec!.break_start?.getTime()).toBe(
new Date("2098-03-15T02:00:00").getTime(),
);
expect(rec!.break_end?.getTime()).toBe(
new Date("2098-03-15T02:30:00").getTime(),
);
expect(rec!.notes).toBe("attendance_wire_test updated");
});
it("clears times when null is sent (leave-type edit)", async () => {
const created = await prisma.attendance.create({
data: {
user_id: adminUserId,
shift_date: new Date(2098, 2, 16, 12, 0, 0),
leave_type: "work",
arrival_time: new Date("2098-03-16T08:00:00"),
departure_time: new Date("2098-03-16T16:30:00"),
notes: "attendance_wire_test to-leave",
},
});
const res = await authPut(
`/api/admin/attendance/${created.id}`,
adminToken,
{
leave_type: "vacation",
leave_hours: 8,
arrival_time: null,
departure_time: null,
break_start: null,
break_end: null,
notes: "attendance_wire_test to-leave",
},
);
expect(res.statusCode).toBe(200);
expect(res.json().success).toBe(true);
const rec = await prisma.attendance.findUnique({
where: { id: created.id },
});
expect(rec!.leave_type).toBe("vacation");
expect(rec!.arrival_time).toBeNull();
expect(rec!.departure_time).toBeNull();
});
});