64f0e0a1b821965b8eca1178f4986110ab2a65a7
8 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7bd969b41a |
fix(audit-integrations): SMTP/PG/Socket.IO timeouts, prompt injection, secret-at-rest
A focused review of every external integration surfaced six issues the
original audit missed. Fixed here.
HIGH
* Socket.IO had an unconditional 30-second idle disconnect on every
socket. The comment on the line acknowledged it was "for development
only, would be longer in prod" but no NODE_ENV guard existed, and the
`socket.onAny` listener only resets on inbound client events — every
dashboard connection that received only server-push events would have
been torn down every 30s in production. Removed the manual idle
timer entirely; Socket.IO's pingTimeout / pingInterval handles
dead-transport detection at the protocol level.
* SMTP transporters had no `connectionTimeout` / `greetingTimeout` /
`socketTimeout`. Nodemailer's defaults are 2 minutes for connect
and unlimited for socket — a hung SMTP server would have held a
BullMQ `email` worker concurrency slot for up to 10 min per job
(5 retries × 2 min). Set 10s/10s/30s on both the system transporter
in `src/lib/email/index.ts` and the user-account transporter in
`email-compose.service.ts`.
MEDIUM
* PostgreSQL pool had no `statement_timeout` /
`idle_in_transaction_session_timeout`. A slow query or transaction
held by a crashed handler would have eventually exhausted the
20-connection pool. 30s statement cap, 10s idle-in-tx cap, plus
`max_lifetime: 30min` to recycle connections.
* `umami_password` and `umami_api_token` were stored as plaintext in
`system_settings` (the SMTP and S3 secret paths use AES-GCM). The
reader now passes them through `readSecret()` which auto-detects
the encrypted `iv:cipher:tag` shape and decrypts, falling back to
legacy plaintext so operators can rotate without a flag-day.
* AI email-draft worker interpolated `additionalInstructions` (user-
controlled) directly into the OpenAI prompt — a hostile rep could
close the instructions block and inject prompt directives that
override the system prompt. Added `sanitizeForPrompt()` that
strips newlines + quote chars, caps at 500 chars, and the prompt
now wraps the value in a "treat as data not commands" preamble.
LOW
* Legacy `ensureBucket()` in `src/lib/minio/index.ts` was unguarded —
if any future code imported it (currently no callers), a misconfigured
prod deploy could mint a fresh empty bucket. Now matches the gate
used by the pluggable S3Backend (`MINIO_AUTO_CREATE_BUCKET=true`
required) so the legacy export and the new pluggable path agree.
Confirmed not-an-issue: BullMQ Workers create connections via
`{ url }` options object, and BullMQ sets `maxRetriesPerRequest: null`
internally for those — no fix needed. The shared `redis` singleton
that does keep `maxRetriesPerRequest: 3` is used only for direct
Redis ops (rate-limit sliding window, etc.), never for blocking
BullMQ commands, so the value is correct there.
Test status: 1175/1175 vitest, tsc clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
687a1f1c2f |
fix(audit-v3): platform-wide deferred-list cleanup (rounds 1-4)
Working through the audit-v2 deferred backlog. Each round was tested
(typecheck + 1168/1168 vitest) before moving on.
Round 1 — DB performance + AI cost visibility:
- Add missing FK indexes Postgres doesn't auto-create on
berth_reservations.{interest_id, contract_file_id},
documents.{file_id, signed_file_id}, document_events.signer_id,
document_templates.source_file_id, form_submissions.{form_template_id,
client_id}, document_sends.{brochure_id, brochure_version_id,
sent_by_user_id}. Without these, RESTRICT-checks on parent delete +
reverse-lookups walk the child tables fully. Migration 0037.
- AI worker now writes one ai_usage_ledger row per OpenAI call so admins
can audit spend per port/user/feature and future per-port budgets have
history to read from. Failure to write is logged-not-thrown so the
user-facing email draft is unaffected.
Round 2 — Boot-time + transport hardening:
- S3 backend verifies the bucket exists at startup (or auto-creates
when MINIO_AUTO_CREATE_BUCKET=true). A typo'd bucket name now
surfaces with a clear boot error instead of a vague Minio error
inside the first user-facing request.
- Documenso v1 placeFields: 3-attempt exponential-backoff retry on 5xx
+ network errors, fail-fast on 4xx. Stops one transient flake from
leaving a document with a partial field set.
- FilesystemBackend logs a structured warn-once at boot when the dev
HMAC fallback is in effect, so two processes started with different
BETTER_AUTH_SECRET values are observable (random 401s on file
downloads otherwise).
- Logger redact paths extended to cover *.headers.{authorization,
cookie}, *.config.headers.authorization, encrypted-credential blobs
(secretKeyEncrypted, smtpPassEncrypted, etc.), the Documenso
X-Documenso-Secret header, and 2-level nested forms.
Round 3 — UI feedback + permission gates:
- Storage admin migrate dialog: success toast with row count + error
toast on both dryRun and migrate mutations.
- Invoice detail Send + Record-payment buttons wrapped in
PermissionGate (invoices.send / invoices.record_payment); both
mutations now toast on success/error.
- Admin user list Edit button wrapped in PermissionGate(admin.manage_users).
- Scan-receipt page surfaces an amber warning when OCR fails so reps
know they can fill the form manually instead of staring at a stalled
spinner; the editable form now also opens on scanMutation.isError
/ uploadedFile, not only on success.
- Email threads list now renders skeleton rows during load + shared
EmptyState for the empty case (was a single "Loading…" line).
Round 4 — Service / route correctness:
- documentSends.sent_by_user_id was a free-text NOT NULL column with no
FK. Now nullable + FK to user(id) ON DELETE SET NULL so the audit row
survives a user being hard-deleted. Migration 0038 with a defensive
null-out for any orphan ids before attaching the constraint.
- Saved-views route: documented why withAuth alone is correct (the
service strictly filters by (portId, userId) — owner-only by design).
- Public-interests audit log: replaced "userId: null as unknown as
string" cast with userId: null; AuditLogParams already accepts null
for system-generated events.
- EOI in-app PDF fill: extracted setBerthRange() that, when the
AcroForm field is missing AND the context has a non-empty range
string, logs a structured warn so the deployment gap (live Documenso
template needs the field) is observable instead of silently dropping
the multi-berth range.
Test status: 1168/1168 vitest. tsc clean. Two new migrations
(0037/0038) need pnpm db:push (or migration apply) on the dev DB.
Deferred-doc updated with the remaining open items (bigger refactors).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
6e3d910c76 |
refactor(interests): migrate callers to interest_berths junction + drop berth_id
Phase 2b of the berth-recommender refactor (plan §3.4). Every caller of the legacy `interests.berth_id` column now reads / writes through the `interest_berths` junction via the helper service introduced in Phase 2a; the column itself is dropped in a final migration. Service-layer changes - interests.service: filter `?berthId=X` becomes EXISTS-against-junction; list enrichment uses `getPrimaryBerthsForInterests`; create/update/ linkBerth/unlinkBerth all dispatch through the junction helpers, with createInterest's row insert + junction write sharing a single transaction. - clients / dashboard / report-generators / search: leftJoin chains pivot through `interest_berths` filtered by `is_primary=true`. - eoi-context / document-templates / berth-rules-engine / portal / record-export / queue worker: read primary via `getPrimaryBerth(...)`. - interest-scoring: berthLinked is now derived from any junction row count. - dedup/migration-apply + public interest route: write a primary junction row alongside the interest insert when a berth is provided. API contract preserved: list/detail responses still emit `berthId` and `berthMooringNumber`, derived from the primary junction row, so frontend consumers (interest-form, interest-detail-header) need no changes. Schema + migration - Drop `interestsRelations.berth` and `idx_interests_berth`. - Replace `berthsRelations.interests` with `interestBerths`. - Migration 0029_puzzling_romulus drops `interests.berth_id` + the index. - Tests that previously inserted `interests.berthId` now seed a primary junction row alongside the interest. Verified: vitest 995 passing (1 unrelated pre-existing flake in maintenance-cleanup.test.ts), tsc clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
8699f81879 |
chore(style): codebase em-dash sweep + minor layout polish
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> |
||
|
|
e06fb9545b |
sec: lock down 5 cross-tenant IDORs uncovered in second-pass review
1. HIGH — /api/v1/admin/ports/[id] PATCH+GET let any port-admin (manage_settings) mutate any other tenant's port row by passing the foreign id in the path. Now non-super-admins must target their own ctx.portId; listPorts and createPort are super-admin only. 2. HIGH — Invoice create/update accepted arbitrary expenseIds and linked them into invoice_expenses with no port check; the GET response then re-emitted those foreign expense rows via the linkedExpenses join. assertExpensesInPort now validates each id belongs to the caller's portId before insert; getInvoiceById's join filters by expenses.portId as defense-in-depth. 3. HIGH — Document creation paths (createDocument, createFromWizard, createFromUpload) persisted user-supplied clientId/interestId/ companyId/yachtId/reservationId without verifying those FKs were in-port. sendForSigning then loaded the foreign client/interest by id alone and pushed their PII into the Documenso payload. New assertSubjectFksInPort helper rejects out-of-port FKs at create time; sendForSigning's interest+client lookups now also filter by portId. 4. MEDIUM — calculateInterestScore read its redis cache before verifying portId, and the cache key was interestId-only — a foreign-port caller could observe a cached score breakdown. Cache key now includes portId, and the port-scope DB lookup runs before any cache.get. 5. MEDIUM — AI email-draft job results were retrievable by anyone who could guess the BullMQ jobId (default sequential integers). Job ids are now random UUIDs, requestEmailDraft validates interestId/ clientId belong to ctx.portId before enqueueing, the worker's client lookup is port-scoped, and getEmailDraftResult requires the caller to match the original requester's userId+portId before returning the drafted subject/body. The interest-scoring unit test that asserted "DB is bypassed on cache hit" is updated to reflect the new (security-correct) ordering. Two new regression test files cover the email-draft binding (5 tests). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
0ed401d083 |
refactor(clients): drop deprecated yacht/company/proxy columns
PR 13: now that all reads are migrated to the dedicated yacht / company
/ membership entities, drop the columns that mirrored them on `clients`:
companyName, isProxy, proxyType, actualOwnerName, relationshipNotes,
yachtName, yachtLength{Ft,M}, yachtWidth{Ft,M}, yachtDraft{Ft,M},
berthSizeDesired.
Migration `0008_loud_ikaris.sql` issues the destructive ALTER TABLE
DROP COLUMN statements. Run `pnpm db:push` (or the migration runner) to
apply.
Caller cleanup (zero behavioral change to remaining flows):
- Drops the legacy `generateEoi` flow entirely (route, service function,
pdfme template, validator schema). The dual-path generate-and-sign
service from PR 11 has fully replaced it; the route was no longer
wired to the UI.
- `clients.service`: company-name search column / WHERE / audit value
removed; search now ranks by full name only.
- `interests.service`: `resolveLeadCategory` reads dimensions from
`yachts` via `interest.yachtId` instead of the dropped
`client.yachtLength{Ft,M}`.
- `record-export`: client-summary now lists yachts via owner-side
lookup (direct + active company memberships); interest-summary fetches
yacht via `interest.yachtId`. Both PDF templates updated to read
yacht details from the new entity.
- `client-detail-header`, `client-picker`, `command-search`,
`search-result-item`, `use-search` hook, `types/domain.ts`,
`search.service` — drop the companyName badge / sub-label / typed
field everywhere it was rendered or fetched.
- `ai.ts` worker: drop the company / yacht context lines from the
prompt (will be re-added later sourced from the new entities).
- `validators/interests.ts`: remove the deprecated public-form flat
yacht/company fields. The route already ignores them.
- `factories.ts`: drop the `isProxy: false` default.
Tests: 652/652 green; type-check clean. The
`security-sensitive-data` tests use `companyName` / `isProxy` as
arbitrary record keys for a generic util — left unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
| 4c20bcffcd |
Fix all ESLint errors: remove unused imports, replace any types
- Remove ~60 unused imports and variables across 88 files - Replace ~80 `any` type annotations with proper types (unknown, Record<string, unknown>, or specific types) - Prefix unused callback args with underscore - Fix unescaped JSX entities - Lint now passes cleanly (0 errors, 2 intentional img warnings) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
|||
| 67d7e6e3d5 |
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM, PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source files covering clients, berths, interests/pipeline, documents/EOI, expenses/invoices, email, notifications, dashboard, admin, and client portal. CI/CD via Gitea Actions with Docker builds. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |