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:
@@ -1,6 +1,17 @@
|
||||
import { describe, it, expect, afterEach } from "vitest";
|
||||
import { describe, it, expect, afterEach, beforeAll, afterAll } 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 { createOrder, listOrders } from "../services/orders.service";
|
||||
import { config } from "../config/env";
|
||||
import ordersRoutes from "../routes/admin/orders";
|
||||
import {
|
||||
createOrder,
|
||||
listOrders,
|
||||
getOrder,
|
||||
getOrderAttachment,
|
||||
} from "../services/orders.service";
|
||||
|
||||
const createdOrderIds: number[] = [];
|
||||
const createdCustomerIds: number[] = [];
|
||||
@@ -80,6 +91,166 @@ describe("listOrders search filter", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("order attachment payload hygiene", () => {
|
||||
// The PO attachment blob must be served ONLY by GET /:id/attachment —
|
||||
// list/detail payloads carry attachment_name as the existence signal.
|
||||
const PDF_BYTES = Buffer.from("%PDF-1.4 orders-list-test-attachment-bytes");
|
||||
const TEST_ADMIN = "orders_list_test_admin";
|
||||
|
||||
let app: Awaited<ReturnType<typeof buildOrdersApp>>;
|
||||
let adminToken: string;
|
||||
let adminUserId: number;
|
||||
|
||||
async function buildOrdersApp() {
|
||||
const fastify = Fastify({ logger: false });
|
||||
await fastify.register(cookie);
|
||||
await fastify.register(rateLimit, { max: 1000, timeWindow: "1 minute" });
|
||||
// multipart is registered inside ordersRoutes itself
|
||||
await fastify.register(ordersRoutes, { prefix: "/api/admin/orders" });
|
||||
return fastify;
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await buildOrdersApp();
|
||||
|
||||
// Clean up any leftover test user from a previous failed run
|
||||
await prisma.users.deleteMany({ where: { username: TEST_ADMIN } });
|
||||
|
||||
const adminRole = await prisma.roles.findFirst({
|
||||
where: { name: "admin" },
|
||||
});
|
||||
if (!adminRole)
|
||||
throw new Error("Admin role not found — seed the database first");
|
||||
|
||||
const adminUser = await prisma.users.create({
|
||||
data: {
|
||||
username: TEST_ADMIN,
|
||||
email: `${TEST_ADMIN}@test.local`,
|
||||
password_hash:
|
||||
"$2a$12$LJ3m4ys3Lg4oLBFnYP2amuPBzJnJBbGzCl5Y6X9Y8r0q5.s3L6OyO",
|
||||
first_name: "Orders",
|
||||
last_name: "ListTest",
|
||||
is_active: true,
|
||||
role_id: adminRole.id,
|
||||
},
|
||||
});
|
||||
adminUserId = adminUser.id;
|
||||
adminToken = jwt.sign(
|
||||
{ sub: adminUser.id, username: adminUser.username, role: "admin" },
|
||||
config.jwt.secret,
|
||||
{ expiresIn: "15m" },
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await prisma.users.deleteMany({ where: { id: adminUserId } });
|
||||
await app.close();
|
||||
});
|
||||
|
||||
/** One order WITH a stored PO attachment, one WITHOUT. */
|
||||
async function makeOrderPair() {
|
||||
const customer = await makeCustomer();
|
||||
const withRes = await createOrder(
|
||||
{ ...baseOrder, customer_id: customer.id, create_project: false },
|
||||
PDF_BYTES,
|
||||
"po-test.pdf",
|
||||
);
|
||||
if (!("id" in withRes)) failResult(withRes);
|
||||
createdOrderIds.push(withRes.id!);
|
||||
|
||||
const withoutRes = await createOrder({
|
||||
...baseOrder,
|
||||
customer_id: customer.id,
|
||||
create_project: false,
|
||||
});
|
||||
if (!("id" in withoutRes)) failResult(withoutRes);
|
||||
createdOrderIds.push(withoutRes.id!);
|
||||
|
||||
return { withId: withRes.id!, withoutId: withoutRes.id! };
|
||||
}
|
||||
|
||||
it("listOrders rows never contain attachment_data; attachment_name signals existence", async () => {
|
||||
const { withId, withoutId } = await makeOrderPair();
|
||||
|
||||
const list = await listOrders(baseListParams);
|
||||
const withRow = list.data.find((o) => o.id === withId);
|
||||
const withoutRow = list.data.find((o) => o.id === withoutId);
|
||||
expect(withRow).toBeTruthy();
|
||||
expect(withoutRow).toBeTruthy();
|
||||
|
||||
expect("attachment_data" in withRow!).toBe(false);
|
||||
expect("attachment_data" in withoutRow!).toBe(false);
|
||||
expect(withRow!.attachment_name).toBe("po-test.pdf");
|
||||
expect(withoutRow!.attachment_name).toBeNull();
|
||||
});
|
||||
|
||||
it("getOrder detail never contains attachment_data but keeps attachment_name", async () => {
|
||||
const { withId, withoutId } = await makeOrderPair();
|
||||
|
||||
const withDetail = await getOrder(withId);
|
||||
const withoutDetail = await getOrder(withoutId);
|
||||
expect(withDetail).toBeTruthy();
|
||||
expect(withoutDetail).toBeTruthy();
|
||||
|
||||
expect("attachment_data" in withDetail!).toBe(false);
|
||||
expect("attachment_data" in withoutDetail!).toBe(false);
|
||||
expect(withDetail!.attachment_name).toBe("po-test.pdf");
|
||||
expect(withoutDetail!.attachment_name).toBeNull();
|
||||
});
|
||||
|
||||
it("HTTP list/detail JSON carries no attachment_data; /attachment still serves the binary", async () => {
|
||||
const { withId, withoutId } = await makeOrderPair();
|
||||
const headers = { authorization: `Bearer ${adminToken}` };
|
||||
|
||||
const listRes = await app.inject({
|
||||
method: "GET",
|
||||
url: "/api/admin/orders?sort=id&order=desc&per_page=100",
|
||||
headers,
|
||||
});
|
||||
expect(listRes.statusCode).toBe(200);
|
||||
const listJson = JSON.parse(listRes.body);
|
||||
expect(listJson.data.map((o: { id: number }) => o.id)).toContain(withId);
|
||||
expect(listRes.body).not.toContain("attachment_data");
|
||||
|
||||
const detailRes = await app.inject({
|
||||
method: "GET",
|
||||
url: `/api/admin/orders/${withId}`,
|
||||
headers,
|
||||
});
|
||||
expect(detailRes.statusCode).toBe(200);
|
||||
expect(detailRes.body).not.toContain("attachment_data");
|
||||
expect(JSON.parse(detailRes.body).data.attachment_name).toBe("po-test.pdf");
|
||||
|
||||
// The dedicated endpoint is the ONE place the blob is served — byte-exact.
|
||||
const attRes = await app.inject({
|
||||
method: "GET",
|
||||
url: `/api/admin/orders/${withId}/attachment`,
|
||||
headers,
|
||||
});
|
||||
expect(attRes.statusCode).toBe(200);
|
||||
expect(attRes.headers["content-type"]).toContain("application/pdf");
|
||||
expect(Buffer.from(attRes.rawPayload).equals(PDF_BYTES)).toBe(true);
|
||||
|
||||
const noAttRes = await app.inject({
|
||||
method: "GET",
|
||||
url: `/api/admin/orders/${withoutId}/attachment`,
|
||||
headers,
|
||||
});
|
||||
expect(noAttRes.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
it("getOrderAttachment round-trips the stored bytes (and null without one)", async () => {
|
||||
const { withId, withoutId } = await makeOrderPair();
|
||||
|
||||
const att = await getOrderAttachment(withId);
|
||||
expect(att).toBeTruthy();
|
||||
expect(att!.filename).toBe("po-test.pdf");
|
||||
expect(att!.data.equals(PDF_BYTES)).toBe(true);
|
||||
|
||||
expect(await getOrderAttachment(withoutId)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("listOrders month filter", () => {
|
||||
it("returns only orders whose created_at is in the given month", async () => {
|
||||
const customer = await makeCustomer();
|
||||
|
||||
Reference in New Issue
Block a user