Commit Graph

5 Commits

Author SHA1 Message Date
Matt Ciaccio
d3a6a9beef fix(audit-tier-2-routes): manual NextResponse.json error sweep + admin form banners
Two final waves of error-surface hygiene closing the audit's MED §12 +
HIGH §15 + HIGH §17 findings:

* 50 route files swept (61 sites): manual NextResponse.json({error,
  status: 4xx|5xx}) early-returns replaced by typed throws +
  errorResponse(err) at the catch.
  - Super-admin gates (13 sites) use new requireSuperAdmin(ctx, action)
    helper from src/lib/api/helpers.ts so denials hit the audit log.
  - Path-param + body validation 400s become ValidationError throws.
  - 404s become NotFoundError or CodedError('NOT_FOUND') for AI
    feature-flag paths.
  - 11 manual 5xx returns now re-throw so error_events captures the
    request-id (the admin error inspector becomes usable from real
    incidents).
  - website-analytics 200-with-error anti-pattern flipped to 409 +
    UMAMI_NOT_CONFIGURED. 502 upstream paths use UMAMI_UPSTREAM_ERROR.
  - 11 sites intentionally preserved: storage/[token] anti-enumeration
    token-failure paths, webhook-secret 401, "Unknown port" 400 in
    public intake.

* 7 admin forms (roles, users, ports, webhooks, custom-fields,
  document-templates, tags) gain a formatErrorBanner() helper from
  src/lib/api/toast-error.ts that builds a multi-line "Error code / Reference ID"
  banner — the rep can copy the request id when reporting a failed
  save.  Banners get whitespace-pre-line so newlines render.

Test status: 1168/1168 vitest, tsc clean.

Refs: docs/audit-comprehensive-2026-05-05.md MED §12 (auditor-F Issue 1)
+ HIGH §15 (auditor-F Issue 2) + HIGH §17 (auditor-H Issue 2).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 20:36:59 +02:00
Matt Ciaccio
8699f81879 chore(style): codebase em-dash sweep + minor layout polish
Some checks failed
Build & Push Docker Images / lint (push) Failing after 1m18s
Build & Push Docker Images / build-and-push (push) Has been skipped
Replaces every em-dash and en-dash with regular ASCII hyphens
across comments, JSX strings, and dev-facing logs. Mostly cosmetic
but stops the inconsistent mix that crept in over the last few
months (some files used em-dashes in comments, others didn't,
some used both).

Bundles two small dashboard-layout tweaks that touch a couple of
already-modified files:
- (dashboard)/layout.tsx main padding goes from p-6 to pt-3 px-6
  pb-6 so page content sits closer to the topbar.
- Sidebar now receives the ports list it needs for the footer
  port switcher.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 22:57:01 +02:00
Matt Ciaccio
9a5479c2c7 sec: lock down socket.io room subscription + crm-invite cross-tenant ops
1. HIGH — Socket.IO accepted client-supplied `auth.portId` in the
   handshake without verifying the user actually held a role in that
   port, then unconditionally joined the socket to `port:${portId}`.
   The `join:entity` handler also skipped authorization. This let any
   authenticated CRM user receive realtime events from any other
   tenant: invoice numbers + totals + client names, document signer
   emails, registration events with full client name + berth, file
   uploads, etc. Auth middleware now resolves the user's
   userPortRoles (or isSuperAdmin) before honouring portId, and
   join:entity verifies the entity's port matches a port the user
   has access to. Pre-existing pre-branch issue but fixed here given
   the explicit "all data is extremely sensitive" directive.

2. MEDIUM — listCrmInvites issued a global SELECT with no port
   scope. The crm_user_invites table has no portId column (invites
   mint global better-auth users, then port roles are assigned
   later). The previous gating on per-port admin.manage_users let
   any director enumerate every other tenant's pending invitee
   emails + isSuperAdmin flags — a phishing target list and a
   super-admin onboarding timing oracle. Restrict GET (list),
   DELETE (revoke), and POST resend to ctx.isSuperAdmin.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 03:00:55 +02:00
Matt Ciaccio
4c5334d471 sec: gate super-admin invite minting, OCR settings, and alert mutations
Three findings from the branch security review:

1. HIGH — Privilege escalation via super-admin invite. POST
   /api/v1/admin/invitations was gated only by manage_users (held by the
   port-scoped director role). The body schema accepted isSuperAdmin
   from the request, createCrmInvite persisted it verbatim, and
   consumeCrmInvite copied it into userProfiles.isSuperAdmin — granting
   the new account cross-tenant access. Now the route rejects
   isSuperAdmin=true unless ctx.isSuperAdmin, and createCrmInvite
   requires invitedBy.isSuperAdmin as defense-in-depth.

2. HIGH — Receipt-image exfiltration via OCR settings. The route
   /api/v1/admin/ocr-settings (and the sibling /test) were wrapped only
   in withAuth — any port role including viewer could PUT a swapped
   provider apiKey + flip aiEnabled, redirecting every subsequent
   receipt scan to attacker infrastructure. Both are now wrapped in
   withPermission('admin','manage_settings',…) matching the sibling
   admin routes (ai-budget, settings).

3. MEDIUM — Cross-tenant alert IDOR. dismissAlert / acknowledgeAlert
   issued UPDATE … WHERE id=? with no portId predicate. Any
   authenticated user with a foreign alert UUID could mutate it. Both
   service functions now require portId and add it to the WHERE; the
   route handlers pass ctx.portId.

The dev-trigger-crm-invite script passes a synthetic super-admin caller
identity since it runs out-of-band.

The two public-form tests randomize their IP prefix per run so a fresh
test process doesn't collide with leftover redis sliding-window entries
from a prior run (publicForm limiter pexpires after 1h).

Two new regression test files cover the fixes (6 tests).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 02:27:01 +02:00
Matt Ciaccio
4877b97f27 feat(admin): per-port email/Documenso/branding/reminder settings + invitations
All checks were successful
Build & Push Docker Images / lint (pull_request) Successful in 1m1s
Build & Push Docker Images / build-and-push (pull_request) Has been skipped
Centralizes everything operators need to configure into the admin panel,
each setting per-port with env fallback.

New admin pages
- /admin              landing page linking to every admin section as a card
- /admin/email        FROM name+address, reply-to, signature/footer HTML,
                      optional SMTP host/port/user/pass override
- /admin/documenso    API URL+key override, EOI Documenso template ID,
                      default EOI pathway (documenso-template vs inapp),
                      "Test connection" button
- /admin/branding     logo URL, primary color, app name, email
                      header/footer HTML
- /admin/reminders    port-level defaults for new interests +
                      port-wide daily-digest delivery window
- /admin/invitations  send / list / resend / revoke CRM invitations

Per-user reminder digest
- /notifications/preferences gains a Reminder digest card:
  immediate / daily / weekly / off, with HH:MM, day-of-week,
  IANA timezone fields. Stored in user_profiles.preferences.reminders.

Plumbing
- port-config.ts typed accessors (getPortEmailConfig, getPortDocumensoConfig,
  getPortBrandingConfig, getPortReminderConfig) — settings → env fallback.
- sendEmail accepts optional portId; resolves From/SMTP from settings
  when supplied.
- documensoFetch + downloadSignedPdf accept optional portId; each public
  function takes it through. checkDocumensoHealth() backs the test button.
- crm-invite.service gains listCrmInvites / revokeCrmInvite / resendCrmInvite
  with audit-log entries (revoke_invite, resend_invite added to AuditAction).
- AdminLandingPage card grid + shared SettingsFormCard component to remove
  per-page form boilerplate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 23:21:54 +02:00