* feat(lib): add canonical money + format + fetch primitives (audit Tier 0) Foundation for post-audit cleanup: shared primitives so subsequent refactors import one helper instead of reinventing (the duplication the audit found). - lib/money.ts: canonical roundOre/ORE_TOLERANCE (+ equalOre/isZeroOre/sumOre); lib/bokslut/rounding.ts re-exports for back-compat - lib/utils.ts: formatAmount, formatWholeKr, formatDateTime - lib/hooks/use-fetch.ts: generic client fetch hook (abort, bilingual errors, refetch) - components/common/DataState.tsx: loading/error/empty wrapper over Skeleton/EmptyState - messages: common.retry / common.load_error (sv+en) - tests: 16 tests incl. the 1.005 half-ore case and locale-robust format assertions Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci(guards): ratchet against new MFA-bypassing routes and naive ore-rounding Adds scripts/checks/no-new-antipatterns.mjs + committed baseline. Fails CI only when a PR ADDS a route hand-rolling supabase.auth.getUser() (which skips MFA AAL2 enforcement) or a new Math.round(x*100)/100. Baseline: 178 raw-auth routes, 668 naive rounds — ratchets down as the A1 (route-auth) and D1 (rounding) migrations land. Wired into core-build.yml; green at baseline. Note: scripts/ is gitignored (.gitignore:70 '/scripts') yet tracks 39 files via force-add; these two were force-added to match that existing pattern. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(api,errors): enforce MFA on journal-entry mutation routes via withRouteContext (A1) Migrates the 4 journal-entry mutation routes (commit, correct, reverse, recordate) off hand-rolled supabase.auth.getUser() onto withRouteContext, which enforces MFA AAL2 (requireAuth) + non-viewer role (requireWrite) and routes thrown errors through the canonical errorResponse envelope. Fixes audit finding A1 for the most compliance-critical mutations and folds in C8 for these routes (drops bookkeepingErrorResponse; they now emit message_en). Also fixes a latent bug: errorResponse()/extractBookkeepingDetails only handled 11 of 15 typed bookkeeping errors, so MeaninglessCorrection / NoOpenPeriodForDate / TargetPeriodClosed / TargetPeriodLocked silently degraded to a generic 500 (affecting existing v1 callers too). Adds the 4 missing registry codes + extract cases -> correct 400/409. Behavior change: untyped engine throws now return the canonical 500 envelope instead of 400+raw-string; typed errors keep their status (verified against the registry). Tests updated to the realistic typed-error contract + a 403 write-gate test on commit. Updates .claude/rules/api-routes.md to prescribe withRouteContext. Ratchets the antipattern guard 178 -> 174. Full unit suite green (5023); tsc: no new errors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(api): enforce MFA on salary run authorization routes via withRouteContext (A1) Migrates the salary-run lifecycle write routes (approve, paid, revert) — the highest-PII A1 surface — off hand-rolled supabase.auth.getUser() onto withRouteContext (enforces MFA AAL2 + non-viewer role). Explicit { error } returns are preserved unchanged (passed through the wrapper); only auth changes, so no error-shape regression. Salary unit suite green (8). Ratchets the antipattern guard 174 -> 171. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * review: address PR #646 bot findings - guard: match withRouteContext/requireAuth at the CALL site (withRouteContext[<(]), not a bare import — closes the false-negative greptile flagged. It surfaced app/api/sandbox/seed (hand-rolled getUser; the loose regex had matched a code comment). Switched that route to requireAuth() — the documented stopgap for routes that can't use withRouteContext (it runs before a company exists; anonymous users, so MFA is a no-op but the auth path is now consistent). Guard stays at 171. - money.test: add the negative half-ore case roundOre(-1.005) === -1 to lock the rounding direction against regressions. - use-fetch: document keep-previous-data + deferred-loading (effect-tick) semantics. - structured-errors: drop the BFL 5 kap. 5 § citation from MEANINGLESS_CORRECTION per the swedish-compliance bot (5 § governs correction procedure, not the no-op precondition). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * review: enrich wrapper error logging + document sandbox GDPR controls (PR #646) - with-route-context: log unhandled errors and route errorResponse through the resolved { userId, companyId } logger, not just { requestId, operation } — closes the OWASP V16 audit-trail finding for all 82+ routes using the wrapper. Documented in the JSDoc. - sandbox/seed: document the GDPR Art.32 compensating controls for the anonymous write path (anonymous-only, /24 rate limit, synthetic demo data, own-company RLS scope). No functional change — the flagged behaviour is pre-existing by design; this records the reasoning inline. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
3.9 KiB
3.9 KiB
paths
| paths | |
|---|---|
|
API Route Pattern
Use the /erp-api-route skill when scaffolding new endpoints.
Default: wrap every cookie-session route in withRouteContext (lib/api/with-route-context.ts). It is the only path that enforces MFA (AAL2) on hosted — it calls requireAuth(), resolves the active companyId, optionally gates non-viewer role (requireWrite: true), and converts thrown errors into the canonical envelope. Never hand-roll supabase.auth.getUser() in a route — that skips MFA. CI enforces this via the ratchet guard (npm run check:guards); a new route calling getUser() directly fails the build.
import { NextResponse } from 'next/server'
import { ensureInitialized } from '@/lib/init'
import { withRouteContext } from '@/lib/api/with-route-context'
import { validateBody } from '@/lib/api/validate'
import { MySchema } from '@/lib/api/schemas'
ensureInitialized() // Module-level — loads extensions for event emission
// Dynamic route: pass the params type as the generic.
export const POST = withRouteContext<{ params: Promise<{ id: string }> }>(
'resource.action',
async (request, { supabase, companyId, user, log }, { params }) => {
const { id } = await params
const validation = await validateBody(request, MySchema)
if (!validation.success) return validation.response
// Business logic... always filter by company_id (defense in depth alongside RLS).
// Throw typed domain errors (e.g. lib/bookkeeping/errors) — the wrapper maps
// them to the right status + canonical { error: { code, message, message_en } }.
return NextResponse.json({ data: result })
},
{ requireWrite: true }, // omit for read-only routes
)
- Dynamic route params:
{ params }: { params: Promise<{ id: string }> }(Next.js 16 — params are async). WithwithRouteContext, pass that shape as the generic and destructureparamsfrom the 3rd handler arg. - Response shapes:
{ data }for success; failures are the canonical{ error: { code, message, message_en?, requestId? } }envelope (thrown errors →errorResponse). Don't hand-build{ error: 'string' }. - Zod schemas in
lib/api/schemas.ts— 100+ schemas with shared primitives (uuid, isoDate, accountNumber, nonNegativeAmount). - Routes that emit events must call
ensureInitialized()at module level. - Opt out of
withRouteContextonly when the route genuinely can't guarantee a company context (e.g. onboarding) — then callrequireAuth()directly so MFA is still enforced. - API-key auth (
/api/v1/*) usescreateServiceClientNoCookies()+v1ErrorResponse; every query still filters bycompany_id.
Endpoint map (app/api/)
/api/bookkeeping/*— accounts, fiscal periods, journal entries (CRUD/reverse/correct), mapping rules, voucher gaps/api/invoices/*,/api/supplier-invoices/*— CRUD + state transitions/api/transactions/*— categorize, describe, book, match-{invoice,supplier-invoice}, batch, AI suggestions/api/customers/*,/api/suppliers/*— CRUD/api/documents/*— CRUD, versions, link, match-sweep, verify cron/api/reports/*— report endpoints (GL, TB, BS, IS, AR/supplier ledger, VAT, SIE, INK2, NE-bilaga, KPI, audit, continuity, monthly, full-archive, salary, vacation, avgifter)/api/salary/*— employees, payroll-config, tax-tables, KU, runs/api/import/*— bank-file, SIE (parse/execute/mappings)/api/reconciliation/bank/*,/api/settings/*,/api/company/*,/api/team/*/api/deadlines/*,/api/tax-deadlines/*— CRUD + crons/api/pending-operations/*,/api/events/*,/api/audit-trail/*/api/calendar/feed/[token],/api/mcp-oauth/*,/api/support/contact,/api/account/delete/api/log,/api/health,/api/vat/validate,/api/currency/rate,/api/sandbox/*/api/extensions/ext/[...path]— dynamic extension routes (catch-all →/api/extensions/ext/{extensionId}/{routePath}, path params as_paramNamequery)