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>
72 lines
2.8 KiB
TypeScript
72 lines
2.8 KiB
TypeScript
import { z } from "zod";
|
|
import {
|
|
numberFromForm,
|
|
numberInRange,
|
|
nonNegativeNumberFromForm,
|
|
positiveNumberFromForm,
|
|
nullableIntIdFromForm,
|
|
booleanFromForm,
|
|
} from "./common";
|
|
|
|
const InvoiceItemSchema = z.object({
|
|
description: z.string().max(8000).nullish(),
|
|
item_description: z.string().max(8000).nullish(),
|
|
quantity: positiveNumberFromForm.default(1),
|
|
unit: z.string().max(255).nullish(),
|
|
unit_price: numberFromForm.default(0),
|
|
vat_rate: numberInRange(0, 100).optional().default(21.0),
|
|
position: nonNegativeNumberFromForm.optional(),
|
|
});
|
|
|
|
export const CreateInvoiceSchema = z.object({
|
|
invoice_number: z.string().max(255).nullish(),
|
|
order_id: nullableIntIdFromForm.nullish(),
|
|
customer_id: nullableIntIdFromForm.nullish(),
|
|
status: z.string().max(20).optional().default("draft"),
|
|
currency: z.string().max(20).optional().default("CZK"),
|
|
vat_rate: numberInRange(0, 100).optional().default(21.0),
|
|
apply_vat: booleanFromForm.optional().default(true),
|
|
payment_method: z.string().max(255).nullish(),
|
|
constant_symbol: z.string().max(255).nullish(),
|
|
bank_name: z.string().max(255).nullish(),
|
|
bank_swift: z.string().max(255).nullish(),
|
|
bank_iban: z.string().max(255).nullish(),
|
|
bank_account: z.string().max(255).nullish(),
|
|
issue_date: z.string().max(255).nullish(),
|
|
due_date: z.string().max(255).nullish(),
|
|
tax_date: z.string().max(255).nullish(),
|
|
issued_by: z.string().max(255).nullish(),
|
|
billing_text: z.string().max(8000).nullish(),
|
|
language: z.string().max(20).optional().default("cs"),
|
|
notes: z.string().max(8000).nullish(),
|
|
internal_notes: z.string().max(8000).nullish(),
|
|
items: z.array(InvoiceItemSchema).optional(),
|
|
});
|
|
|
|
export const UpdateInvoiceSchema = z.object({
|
|
status: z.string().max(20).optional(),
|
|
currency: z.string().max(20).optional(),
|
|
language: z.string().max(20).optional(),
|
|
payment_method: z.string().max(255).nullish(),
|
|
constant_symbol: z.string().max(255).nullish(),
|
|
bank_name: z.string().max(255).nullish(),
|
|
bank_swift: z.string().max(255).nullish(),
|
|
bank_iban: z.string().max(255).nullish(),
|
|
bank_account: z.string().max(255).nullish(),
|
|
issued_by: z.string().max(255).nullish(),
|
|
billing_text: z.string().max(8000).nullish(),
|
|
customer_id: nullableIntIdFromForm.optional(),
|
|
vat_rate: numberInRange(0, 100).optional(),
|
|
apply_vat: booleanFromForm.optional(),
|
|
issue_date: z.union([z.string().max(255), z.null()]).optional(),
|
|
due_date: z.union([z.string().max(255), z.null()]).optional(),
|
|
tax_date: z.union([z.string().max(255), z.null()]).optional(),
|
|
notes: z.string().max(8000).nullish(),
|
|
internal_notes: z.string().max(8000).nullish(),
|
|
paid_date: z.union([z.string().max(255), z.null()]).optional(),
|
|
items: z.array(InvoiceItemSchema).optional(),
|
|
});
|
|
|
|
export type CreateInvoiceInput = z.infer<typeof CreateInvoiceSchema>;
|
|
export type UpdateInvoiceInput = z.infer<typeof UpdateInvoiceSchema>;
|