Commit Graph

10 Commits

Author SHA1 Message Date
72028a7f32 fix(proxy): trust forwarded Host header for CSRF origin check
All checks were successful
Build & Push Docker Images / lint (push) Successful in 2m53s
Build & Push Docker Images / build-and-push (push) Successful in 7m22s
The previous attempt compared the Origin host against request.nextUrl.host,
but behind the custom-server + reverse-proxy setup nextUrl.host does NOT
resolve to the public host (mutations stayed 403 in prod). Accept the
Origin/Referer host if it matches ANY of: the forwarded Host header
(nginx sets `proxy_set_header Host $host` → crm.portnimara.com), APP_URL's
host, or nextUrl.host. The Host header is the reliable source here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 04:10:57 +02:00
d485695357 fix: CSRF host-compare behind proxy + default port = creation order
All checks were successful
Build & Push Docker Images / lint (push) Successful in 3m1s
Build & Push Docker Images / build-and-push (push) Successful in 7m30s
Two prod-only breakages found after go-live:

1. CSRF guard rejected EVERY /api/v1 mutation ("Cross-origin state-changing
   request rejected", 403) — making the CRM read-only. It compared the
   browser Origin (https://crm.portnimara.com) against request.nextUrl.origin,
   but TLS terminates at nginx so the app sees http://127.0.0.1 → protocol
   mismatch. Compare hosts instead (Host header survives the proxy; a
   cross-site attacker can't forge the browser-set Origin host).

2. Post-login landed on port-amador (empty tenant), not port-nimara. Three
   queries ordered ports by name (alphabetical → Amador first): the bare
   /dashboard redirect (app/dashboard/page.tsx), the dashboard layout's
   defaultPortId, and /api/v1/me/ports. Order by createdAt so the primary
   (first-seeded) port — Port Nimara — leads, matching listPorts().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 03:38:12 +02:00
23a5811342 fix(proxy): accept the __Secure- prefixed session cookie in production
All checks were successful
Build & Push Docker Images / lint (push) Successful in 3m35s
Build & Push Docker Images / build-and-push (push) Successful in 13m38s
The auth gate read only `pn-crm.session_token`, but better-auth prefixes
the cookie `__Secure-pn-crm.session_token` whenever it issues secure
cookies (production/HTTPS). So in prod every authenticated request was
bounced to /login — sign-in returned 200 + Set-Cookie, but the gate
couldn't see the (prefixed) cookie on the next navigation. Worked in dev
(HTTP → no prefix). Check both names.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 03:13:22 +02:00
7aa639f195 fix(audit): criticals C1 (currency-scoped deposit gate), C2 (outcome-aware berth rule), C4 (/q/ allowlist)
C1: getDepositTotalForInterest now filters to the interest's
depositExpectedCurrency for the auto-advance gate, so a wrong-currency
payment can no longer satisfy the deposit expectation (and mark the berth
Sold). C2: setInterestOutcome fires interest_completed only for 'won';
lost/cancelled fire a new 'deal_lost' rule that frees the berth instead of
flipping it to 'sold'. C4: add '/q/' to proxy PUBLIC_PATHS so tracked
links in outbound mail reach external recipients.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 11:54:36 +02:00
221ae5784e chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged:

- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
  never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
  after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
  redirects (ocr to ai, reports to dashboard, invitations to users),
  docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
  flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
  let-reassign), set-state-in-effect disables in CountryFlag and
  UploadForSigning preview-bytes effect, unused 'confirm' destructures in
  interest contract + reservation tabs, unescaped apostrophe in test-template
  card copy
2026-05-23 00:52:59 +02:00
0f99f054b3 feat(post-audit): batch A+B quick-wins + audit-side residuals
Bundles the user-prioritised follow-ups from the post-audit punch-list.

Batch A — pipeline + EOI safety:
 - §1.1 timeline buildAuditDescription renders diff fields ("leadCategory → hot_lead").
 - §4.13 EOI rejection cascade: notification to assigned rep + audit row + rose banner.
 - §4.10b finish doc-detail: SigningProgress reuse, linked-entity names (server-resolved),
   per-event icons + tooltips + show-more in activity panel.
 - §7.2 stage guidance card replaces empty Payments slot pre-reservation.
 - §4.15 deal-pulse trigger audit (docs/deal-pulse-trigger-audit.md).

Batch B — UX consistency + docs:
 - §1.4 quick log-contact button on interest header.
 - §2.1 contact-log compose: Dialog → Sheet.
 - §7.1 docs/deal-pulse explainer page; /docs/ in PUBLIC_PATHS.
 - DocumentStatus now includes 'rejected' + 'declined' across constants, labels, tone maps.

Audit-side residuals:
 - M-NEW-1 /me/ports skips port-context requirement.
 - M-AU03 audit log CSV export endpoint + UI button.
 - M-IN03 dead receipt-scanner.ts deleted; live path already per-port.
 - M-P01 pg_trgm GIN indexes (migration 0071).
 - §10.1 webhook tests verified passing (was stale).

Deferred per user direction:
 - §11.3 email copy refactor (needs old-CRM reference).
 - M-EM03 IMAP bounce-to-interest linking.

Tests: 1374/1374. tsc + lint clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 14:22:11 +02:00
4b5f85cb7d fix(audit): comprehensive 2026-05-15 audit fix wave + Documenso v2 polish
Bundles the prior session's 50-task fix sweep (Documenso v2 + EOI/signing-
progress redesign + env-to-admin migration + dev-mode banner) with the
2026-05-18 audit fix wave (3 CRITICAL, 14 HIGH, 28 MEDIUM, 6 LOW).

CRITICAL (3):
 - C-01 interest-berths INNER JOIN -> LEFT JOIN so hard-deleted berths
   no longer silently drop interest links
 - C-02 /setup added to PUBLIC_PATHS; fresh-deploy bootstrap loop fixed
 - C-03 generic PATCH /interests/[id] no longer accepts pipelineStage —
   callers must go through /stage with the override-guard chain

HIGH (14/15):
 - H-01 explicit ON DELETE on previously-implicit NO ACTION FKs across
   interests/documents/reservations/reminders/invoices (migration 0070)
 - H-02 login page reads ?redirect= param with same-origin guard
 - H-03 CRM invite token moves to URL fragment so it never lands in
   nginx access logs / Referer headers
 - H-04 Retry-After header on sign-in-by-identifier 429 (RFC 6585 §4)
 - H-05 toggleAccount writes an audit row
 - H-06 upsertSetting masks any value whose key ends with _encrypted
 - H-07 archiveClient cascade fires per-interest audit rows
 - H-08 createSalesTransporter applies SMTP_TIMEOUTS
 - H-09 AppShell stable children — viewport flip across breakpoint no
   longer destroys in-progress form drafts
 - H-10 portal documents page swaps Unicode glyph status icons for
   Lucide CheckCircle2/XCircle/Circle + aria-labels
 - H-12 list components swap alert(...) for toast.warning(...)
 - H-13 5 icon-only buttons gain aria-label
 - H-14 parseBody treats empty bodies as {}
 - H-15 admin layout renders a 403 panel instead of silent bounce
 - H-11 not applicable — mobile-search-overlay IS a mobile bottom-sheet

MEDIUM (28+):
 - M-MT01-05 defense-in-depth port_id/parent-id filters on UPDATE/DELETE
   WHEREs across custom-fields, notes (all 6 entity types x update +
   delete), client-contacts, yacht ownerClient lookup, webhook reads
 - M-D01 documents-hub realtime event-name typo (file:created -> uploaded)
 - M-EM01 portal-auth emails thread through portId
 - M-EM02 sendEmail accepts cc/bcc params
 - M-EM04 notification_digest catalog key
 - M-IN01 portal presigned download URLs use 4h TTL
 - M-IN02 OpenAI client lazy-instantiated
 - M-IN04 stale pdfme refs updated to pdf-lib AcroForm
 - M-IN05 umami.testConnection returns tagged union
 - M-L01 reservations tenure_type unified with berths
 - M-L02 report-generators canonicalize stage values
 - M-AU01 audit log placeholder copy fixed
 - M-AU04 outcome_set / outcome_cleared distinct audit verbs
 - M-NEW-2 activity feed entity name+type separator
 - M-R01 portal allowlist narrowed + portal_session backstop in proxy
 - M-SC02 companies archived partial index
 - M-SC04 audit_logs.searchText documented as DB-managed
 - M-S01 storage_s3_access_key_encrypted admin field
 - M-U01 audit log empty state uses <EmptyState>
 - M-U09 invoice delete dialog -> <AlertDialog>
 - M-U10 toast.success on ClientForm + InterestForm create/edit
 - M-U11 settings-form-card logo preview alt text
 - M-U14 mobile topbar title on clients/yachts/interests/berths
 - M-U15 Invoices in mobile More-sheet

LOW (6/8):
 - L-AU01 severity defaults for security-relevant verbs
 - L-AU02 +13 missing actions in admin audit filter
 - L-AU03 +7 missing entity types in admin audit filter
 - L-AU04 dead listAuditLogs stubbed
 - L-D02 CLAUDE.md Owner-wins chain tightened

Bonus — Document detail polish (#67 partial, 3/6 deliverables):
 - state-aware action button per signer
 - watcher Add UI with display-name resolution
 - cleanSignerName cleanup

Prior session work bundled in:
 - Documenso v2 webhook + envelope-ID normalization + sequential signing
 - SigningProgress UI redesign (avatars, per-signer state, timestamps)
 - env->admin settings registry + RegistryDrivenForm + encrypted creds
 - Embedded-signing card + Test connection + setup help
 - Dev-mode EMAIL_REDIRECT_TO banner
 - Pipeline rules admin page
 - Sales email config card
 - Audit log details Sheet
 - EOI tab: Finalising badge, absolute timestamps, sequential indicator
 - Notes pipeline_stage_at_creation (migration 0069)
 - Documenso numeric ID dual-key webhook (migration 0068)
 - Dimensions criterion copy (migration 0067)

Tests: 1374/1374 vitest pass. tsc clean. lint clean.

See docs/AUDIT-FIX-WAVE-2026-05-18.md for the full progress report and
the user-input items still pending.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 13:28:50 +02:00
446342aa69 fix: P0 — bootstrap proxy + interest detail Date crash
Two pre-deploy blockers found during click-testing:

1. /api/v1/bootstrap/status returned 401 to anonymous visitors because
   /api/v1/bootstrap/ was not in proxy.ts's PUBLIC_PATHS allow-list. Fresh
   VPS deploys couldn't bootstrap their first super-admin via /setup — the
   page reads bootstrap status to decide whether to render the form and got
   no signal back. The route handlers self-protect via hasAnySuperAdmin().

2. getInterestById() crashed every interest detail request with
   `CONNECT_TIMEOUT` / "string argument must be of type string or Buffer"
   because the contact-log count query passed a raw Date through a sql
   template fragment. postgres-js's Bind step can't serialize a Date
   that way. Switched to drizzle's gte() operator which routes the value
   through the column-aware serializer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 22:37:47 +02:00
19002f4c21 fix(audit-wave-11): CSP nonce middleware — drops 'unsafe-inline' in prod
build-auditor H1: prod `script-src` previously kept `'unsafe-inline'`
because dropping it requires a per-request nonce that Next's RSC
bootstrap + Server Actions can thread into their inline scripts.

Implement the nonce mechanism in `src/proxy.ts`:

1. Mint a base64-encoded UUID per request as the CSP nonce.
2. Set the nonce on the REQUEST headers via
   `content-security-policy` + `x-nonce` so Next.js's RSC layer reads
   the active CSP and stamps `nonce=<value>` onto every inline
   `<script>` it emits (Next's documented pattern).
3. Set the matching `Content-Security-Policy` on the RESPONSE so the
   browser actually enforces it.

Prod CSP becomes:
  `script-src 'self' 'nonce-<value>' 'strict-dynamic'`

`'strict-dynamic'` lets nonce-tagged scripts load further scripts they
trust, which is how Next chunks the rest of the bundle in. Inline
`<script>` without a nonce is now rejected by the browser — closes
the canonical XSS pathway.

Dev keeps `'unsafe-inline' 'unsafe-eval'` because Next's HMR evaluates
code at runtime and the nonce machinery doesn't reach it.

`style-src` keeps `'unsafe-inline'` because Tailwind + Radix runtime
style injection has no nonce story yet. Revisit when Tailwind v5
ships a nonce-able API.

The static CSP in `next.config.ts` stays as a fallback for static
assets / API JSON paths that don't run through the proxy. Updated
the comment so future readers know the proxy CSP takes precedence
for HTML responses.

Tests 1315/1315.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 13:04:30 +02:00
4879b17cff feat(deps): Next 15 → 16 (proxy.ts rename + native flat ESLint config)
Applied @next/codemod migrations:
- middleware-to-proxy: src/middleware.ts → src/proxy.ts + function rename
- remove-experimental-ppr: no hits
- remove-unstable-prefix: no hits

tsconfig.json picked up Next 16's autofixes:
- jsx: 'preserve' → 'react-jsx'
- include .next/dev/types/**/*.ts (dev-mode route types)
- next-env.d.ts: triple-slash reference → ES import (TS 6 / Next 16 style)

eslint-config-next@16 ships a native flat config, so dropped the
@eslint/eslintrc + FlatCompat shim. eslint.config.mjs now imports
eslint-config-next/core-web-vitals + eslint-config-prettier/flat
directly.

Note on ESLint 10: bumped + reverted. eslint-config-next@16 still
has a transitive eslint-plugin-react@7 that uses the eslint-9
context API (getFilename on context); breaks under eslint 10.
Audit anticipated lockstep — but the transitive isn't ready yet.
Holding at eslint 9.x until upstream lands. Tracked in BACKLOG.

React Compiler safety rules (react-hooks v7) shipped with config-
next 16 surfaced ~89 legitimate findings (set-state-in-effect,
ref-during-render, immutability). Demoted the new rules to `warn`
so the codebase isn't blocked; triage tracked in BACKLOG §G.

Verified: tsc 0 errors, eslint 0 errors / 105 warnings (89 new
Compiler-rule warns + 16 pre-existing), next build clean, custom
server build clean, vitest 1315/1315.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 22:24:51 +02:00