From 94a9fb8b5a52fc7c6c3b9e4062ca02b8bb692699 Mon Sep 17 00:00:00 2001 From: BOHA Date: Sat, 6 Jun 2026 02:35:25 +0200 Subject: [PATCH] docs(claude): codify enforced conventions; correct stale sanitization/error-catch notes Adds a 'Conventions (enforced)' section (response helpers, parseId, parseBody, roleName-not-role admin check, broad invalidation, single-source label maps, the two date regimes + no UTC-today, Zod 4 idioms, Czech messages) and corrects known-issue #4 (NAS catches now logged) and #5 (HTML sanitization is in place, not a gap). Co-Authored-By: Claude Opus 4.8 (1M context) --- CLAUDE.md | 42 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 3f46903..d362374 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -364,6 +364,44 @@ npx prisma migrate resolve --applied --- +## Conventions (enforced — verified by the 2026-06-06 audit) + +These are the unified rules across the codebase. Follow them for new code; the +audit found and fixed the deviations. Full report: `docs/codebase-audit-2026-06-06.md`. + +**Routes** + +- **Responses:** always `success(reply, data[, status, message])` / `error(reply, msg, status)` / the paginated helper. Never raw `reply.send({ success: true, ... })`. (Some legacy files still do — convert when you touch them.) +- **Route ids:** parse numeric params with `parseId((request.params as any).id, reply)` then `if (id === null) return;`. Do not use raw `parseInt` (it yields `NaN`, not a 400). +- **Bodies:** validate every body with `parseBody(Schema, request.body)` → `if ("error" in body) return error(reply, body.error, 400)`. +- **Permissions:** guard with `requirePermission` / `requireAnyPermission`. The admin shortcut is `authData.roleName === "admin"`. ⚠️ `AuthData` has **`roleName`**, not `role` (`role` only exists on `JwtPayload`). Don't type `authData` as `any` — it hides exactly this bug. +- **Audit:** call `logAudit` on every create/update/delete (and on security-relevant actions like session/token termination) with `oldValues`/`newValues`. + +**Services** + +- Plain exported async functions; return `{ data }` or `{ error, status }` (preferred over discriminated unions). Services own Prisma; routes stay thin. +- Respect soft-delete (`is_deleted: false`) in reads where the model has it. + +**Schemas (Zod 4)** + +- Use Zod 4 idioms: `z.strictObject({...})` / `z.looseObject({...})` (not deprecated `.strict()` / `.passthrough()`). +- Shared coercion helpers (number-from-form, nullable-number, boolean-from-form) belong in `src/schemas/common.ts` — don't copy-paste the `z.union([z.number(), z.string()]).transform(Number)` idiom (it's duplicated ~150× today; consolidating is a tracked follow-up). New number coercions should guard against `NaN`. +- User-facing messages in **Czech**; identifiers/keys in English. + +**Frontend** + +- **Query invalidation:** invalidate the **broad domain key** (`["offers"]`, not `["offers","list"]`). Prefix-matching covers sub-queries. +- **Single source of truth for shared maps:** audit `entity_type` → Czech label lives in `src/admin/lib/entityTypeLabels.ts` and MUST be keyed to the server's `EntityType` values (add the new key there when you add an entity type). Plan categories come from the DB via `lib/queries/plan.ts`. Don't duplicate label maps across files or across server/client. +- Prefer deriving state over `useEffect`; use React Query for fetching, not effects. + +**Dates (two deliberate regimes — don't "fix" either)** + +- **Plan module** does all date-only math in **UTC** (`setUTCDate`, `toISOString().slice(0,10)`) because its columns are `@db.Date` (UTC-midnight). Correct and stable. +- **Attendance/leave** writes `@db.Date` at **local noon** (`new Date(y, m, d, 12, 0, 0)`). +- **Frontend "today" / date-string round-trips:** use `utils/date.ts` `localDateStr` (server) / `normalizeDateStr` (client). **Never** use `new Date().toISOString().split("T")[0]` for "today" — it's the UTC date and is a day early during the late-evening Prague window. + +--- + ## Known Issues & Gotchas 1. **Date.prototype.toJSON override** — global monkey-patch in `src/config/env.ts`. Side-effects on third-party libraries that serialize dates. Do not remove without migrating all date serialization. @@ -372,9 +410,9 @@ npx prisma migrate resolve --applied 3. **Mixed error patterns** — Some services return `{ error, status }`, others return discriminated unions `{ type: 'success' | 'error' }`. Prefer `{ error, status }` for consistency with existing routes. -4. **Silent error catches** — A few service functions swallow errors in catch blocks. Always log at minimum; never use empty catch blocks. +4. **Never swallow errors** — log at minimum; never use empty `catch` blocks. The service layer logs via `console.*` (there is no request-scoped pino logger in services). The NAS managers' previously-silent filesystem catches are now logged. One exception: a `catch` that handles an _expected_ condition (e.g. `ENOENT` used as an existence check) may stay silent **only with a comment** saying so. -5. **HTML sanitization gap** — Rich text fields in invoices use DOMPurify, but quotation scope and order scope fields may not. If modifying those, add sanitization. +5. **HTML sanitization is in place — keep applying it** — Rich-text fields (invoice notes, quotation scope, order notes) ARE sanitized: server-side DOMPurify (jsdom) plus a `cleanQuillHtml` regex pass at all three PDF routes, and the frontend sanitizes before every `dangerouslySetInnerHTML` (InvoiceDetail, OrderDetail). (The old "sanitization gap" note was stale — verified by the 2026-06-06 audit.) When you add ANY new rich-text/HTML field, apply the same sanitization on BOTH the PDF path and the render path. 6. **Puppeteer PDF generation** — Runs a headless browser. Input to the HTML template must be sanitized. Do not pass unsanitized user data into PDF templates.