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>
2026-03-26 11:52:51 +01:00
|
|
|
# Stage 1: Install dependencies
|
|
|
|
|
FROM node:20-alpine AS deps
|
fix(audit-tier-6): validation, perms, ops/infra, per-port webhook secret
Final audit polish — closes the remaining LOW + MED items the previous
tiers didn't reach:
* Validation hardening: me.preferences uses .strict() + 8KB cap
instead of unbounded .passthrough(); files.uploadFile gains
magic-byte verification (jpeg/png/gif/webp/pdf/doc/xlsx); OCR scan
endpoint enforces 10MB cap + magic-byte check on receipt images;
port logoUrl + me.avatarUrl reject javascript:/data: schemes via
a shared httpUrl refinement.
* Permission gates: document-sends/{brochure,berth-pdf} now require
email.send (was withAuth-only); document-sends/{preview,list} on
email.view; ai/email-draft on email.send; documents/[id]/send
uses send_for_signing (was create); expenses/export/parent-company
flips from hard isSuperAdmin to expenses.export for parity;
admin/users/options gated on reminders.assign_others (was withAuth).
* Envelope hygiene: auth/set-password switches the third {message}
variant to errorResponse + {data: {email}}; ai/email-draft wraps
jobId in {data: {jobId}}.
* UI polish: reports-list.handleDownload surfaces failures via
toastError (was console-only).
* Ops/infra: pin pnpm@10.33.2 across all three Dockerfiles +
packageManager field in package.json; Dockerfile.worker re-orders
user creation BEFORE pnpm install so node_modules / .cache dirs
are worker-owned (fixes tesseract.js + sharp EACCES at first PDF
parse); add Redis-ping HEALTHCHECK to the worker container.
* Public health endpoint: returns full env+appUrl payload only when
the caller presents X-Intake-Secret, otherwise a minimal {status}
so generic uptime monitors still work but anonymous internet
doesn't get deployment fingerprints.
* Per-port Documenso webhook secret: new system_settings key
+ listDocumensoWebhookSecrets() helper. The webhook receiver
iterates every configured per-port secret with timing-safe
comparison + falls back to env, then forwards the resolved portId
into handleDocumentExpired so two ports sharing a documensoId
cannot cross-mutate.
Deferred (handled in dedicated follow-up PRs):
* Tier 5.1 — direct service tests for portal-auth / users /
email-accounts / document-sends / sales-email-config. MED, large
test-writing scope.
* The {ok: true} → {data: null} envelope migration across
alerts/expenses/admin-ocr-settings/storage routes. Mechanical but
needs coordinated client + test updates.
* CSP-nonce migration (drop unsafe-inline) — needs middleware-level
nonce generation that the Next 15 router has to thread through.
* Idempotency-Key header on Documenso createDocument. Requires
schema column on documents to persist the key; deferred so it
doesn't bundle a migration into this commit.
* The 16 better-auth user_id FKs — separate dedicated migration
with care (some columns are NOT NULL today and cascade decisions
matter).
* PermissionGate / Skeleton / EmptyState wraps across 5 admin lists
(auditor-H §§36–37) and the residential-clients filter bar.
Test status: 1175/1175 vitest, tsc clean.
Refs: docs/audit-comprehensive-2026-05-05.md MED §§28,29,30 + LOW §§32–43
+ HIGH §9 (Documenso secrets follow-up).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 21:03:31 +02:00
|
|
|
RUN corepack enable && corepack prepare pnpm@10.33.2 --activate
|
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>
2026-03-26 11:52:51 +01:00
|
|
|
WORKDIR /app
|
|
|
|
|
COPY package.json pnpm-lock.yaml ./
|
|
|
|
|
RUN pnpm install --frozen-lockfile --prod=false
|
|
|
|
|
|
2026-06-02 23:03:57 +02:00
|
|
|
# Stage 1b: Production dependency tree in a flat (hoisted) node_modules.
|
|
|
|
|
# Hoisted = symlink-free, so a Docker COPY into the runner is faithful
|
|
|
|
|
# (copying pnpm's default symlinked layout dereferences and breaks
|
|
|
|
|
# transitive resolution); complete = the custom socket.io server's deps
|
|
|
|
|
# (engine.io, accepts, ws, ...) all resolve at runtime.
|
|
|
|
|
FROM node:20-alpine AS prod-deps
|
|
|
|
|
RUN corepack enable && corepack prepare pnpm@10.33.2 --activate
|
|
|
|
|
WORKDIR /app
|
|
|
|
|
COPY package.json pnpm-lock.yaml ./
|
|
|
|
|
RUN echo "node-linker=hoisted" > .npmrc && pnpm install --frozen-lockfile --prod
|
|
|
|
|
|
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>
2026-03-26 11:52:51 +01:00
|
|
|
# Stage 2: Build the application
|
|
|
|
|
FROM node:20-alpine AS builder
|
fix(audit-tier-6): validation, perms, ops/infra, per-port webhook secret
Final audit polish — closes the remaining LOW + MED items the previous
tiers didn't reach:
* Validation hardening: me.preferences uses .strict() + 8KB cap
instead of unbounded .passthrough(); files.uploadFile gains
magic-byte verification (jpeg/png/gif/webp/pdf/doc/xlsx); OCR scan
endpoint enforces 10MB cap + magic-byte check on receipt images;
port logoUrl + me.avatarUrl reject javascript:/data: schemes via
a shared httpUrl refinement.
* Permission gates: document-sends/{brochure,berth-pdf} now require
email.send (was withAuth-only); document-sends/{preview,list} on
email.view; ai/email-draft on email.send; documents/[id]/send
uses send_for_signing (was create); expenses/export/parent-company
flips from hard isSuperAdmin to expenses.export for parity;
admin/users/options gated on reminders.assign_others (was withAuth).
* Envelope hygiene: auth/set-password switches the third {message}
variant to errorResponse + {data: {email}}; ai/email-draft wraps
jobId in {data: {jobId}}.
* UI polish: reports-list.handleDownload surfaces failures via
toastError (was console-only).
* Ops/infra: pin pnpm@10.33.2 across all three Dockerfiles +
packageManager field in package.json; Dockerfile.worker re-orders
user creation BEFORE pnpm install so node_modules / .cache dirs
are worker-owned (fixes tesseract.js + sharp EACCES at first PDF
parse); add Redis-ping HEALTHCHECK to the worker container.
* Public health endpoint: returns full env+appUrl payload only when
the caller presents X-Intake-Secret, otherwise a minimal {status}
so generic uptime monitors still work but anonymous internet
doesn't get deployment fingerprints.
* Per-port Documenso webhook secret: new system_settings key
+ listDocumensoWebhookSecrets() helper. The webhook receiver
iterates every configured per-port secret with timing-safe
comparison + falls back to env, then forwards the resolved portId
into handleDocumentExpired so two ports sharing a documensoId
cannot cross-mutate.
Deferred (handled in dedicated follow-up PRs):
* Tier 5.1 — direct service tests for portal-auth / users /
email-accounts / document-sends / sales-email-config. MED, large
test-writing scope.
* The {ok: true} → {data: null} envelope migration across
alerts/expenses/admin-ocr-settings/storage routes. Mechanical but
needs coordinated client + test updates.
* CSP-nonce migration (drop unsafe-inline) — needs middleware-level
nonce generation that the Next 15 router has to thread through.
* Idempotency-Key header on Documenso createDocument. Requires
schema column on documents to persist the key; deferred so it
doesn't bundle a migration into this commit.
* The 16 better-auth user_id FKs — separate dedicated migration
with care (some columns are NOT NULL today and cascade decisions
matter).
* PermissionGate / Skeleton / EmptyState wraps across 5 admin lists
(auditor-H §§36–37) and the residential-clients filter bar.
Test status: 1175/1175 vitest, tsc clean.
Refs: docs/audit-comprehensive-2026-05-05.md MED §§28,29,30 + LOW §§32–43
+ HIGH §9 (Documenso secrets follow-up).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 21:03:31 +02:00
|
|
|
RUN corepack enable && corepack prepare pnpm@10.33.2 --activate
|
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>
2026-03-26 11:52:51 +01:00
|
|
|
WORKDIR /app
|
|
|
|
|
COPY --from=deps /app/node_modules ./node_modules
|
|
|
|
|
COPY . .
|
fix(audit-wave-10): build-auditor fixes — CSP, server externals, healthcheck
Address the highest-leverage CRITICAL/HIGH/MEDIUM items from the
build-auditor that weren't already covered by Wave 1 (EMAIL_REDIRECT_TO
production guard) or the existing `.dockerignore`.
**C3 — socket.io in standalone trace**
- Add socket.io + @socket.io/redis-adapter to serverExternalPackages
in next.config so the build system sees the dependency (the custom
server is the only importer, no Next route touches it).
- Belt-and-braces: COPY both from the deps stage into the runner stage
of Dockerfile, mirroring the audit's suggested fix.
**H1 — CSP `'unsafe-inline'` in prod**
- Audit recommends nonce-based scripts. Implementing nonces requires
middleware that emits a per-request nonce + threading it through
Next's RSC bootstrap + Server Actions. Out of scope for this wave;
documented the rationale at the CSP definition so the next pass
knows where to start, and noted that the in-the-wild XSS surfaces
are already closed via escapeHtml/escapeUrl in the email + webhook
pipelines.
**H2 — NEXT_PUBLIC_APP_URL validation**
- Add `NEXT_PUBLIC_APP_URL: z.string().url()` to the env schema so a
missing build-time value fails validation instead of silently
inlining the empty string into the client bundle and breaking
multi-origin deploys.
**M3 — serverExternalPackages completeness**
- Add imapflow, mailparser, pdf-lib, sharp, tesseract.js,
@react-pdf/renderer, unpdf — all heavy native/CJS-leaning
server-only deps that should not be route-traced.
**H5 — healthcheck PORT templatization**
- docker-compose.{,prod.}yml: replace hardcoded
`http://localhost:3000/api/health` with `${PORT:-3000}` so
overriding PORT via .env doesn't put the container into a
restart loop.
**M9 — NODE_ENV=production in builder**
- Dockerfile builder stage now sets NODE_ENV=production above
`RUN pnpm build` so the prod-only branches in next.config
(CSP, etc.) compile deterministically.
**M7 — HEALTHCHECK directive in image**
- Add image-level HEALTHCHECK to the app Dockerfile (mirrors the
one in Dockerfile.worker for Redis) so the image is
self-describing for non-compose orchestrators.
Items already addressed prior to this wave:
- C1 (.dockerignore exists, comprehensive)
- C2 (EMAIL_REDIRECT_TO production refusal — Wave 1)
- H4 (compose resource + log limits — already in prod compose)
Tests 1315/1315 throughout.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:30:22 +02:00
|
|
|
# NODE_ENV=production in the builder makes `next build` and any code
|
|
|
|
|
# branching on isProd deterministic (build-auditor M9). Without this,
|
|
|
|
|
# CSP and other prod-only paths would compile under whatever NODE_ENV
|
|
|
|
|
# the host carried in.
|
|
|
|
|
ENV NODE_ENV=production
|
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>
2026-03-26 11:52:51 +01:00
|
|
|
ENV NEXT_TELEMETRY_DISABLED=1
|
2026-04-08 15:31:33 -04:00
|
|
|
ENV SKIP_ENV_VALIDATION=1
|
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>
2026-03-26 11:52:51 +01:00
|
|
|
RUN pnpm build
|
|
|
|
|
|
|
|
|
|
# Stage 3: Production runner
|
|
|
|
|
FROM node:20-alpine AS runner
|
|
|
|
|
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs
|
|
|
|
|
WORKDIR /app
|
|
|
|
|
ENV NODE_ENV=production
|
|
|
|
|
ENV NEXT_TELEMETRY_DISABLED=1
|
|
|
|
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
|
|
|
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
|
|
|
|
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
|
2026-04-08 15:31:33 -04:00
|
|
|
COPY --from=builder --chown=nextjs:nodejs /app/dist/server.js ./server-custom.js
|
fix(docker): merge prod deps into standalone node_modules (not replace)
Replacing the Next standalone node_modules broke turbopack's externalized-
module resolution: the standalone tree is a matched set with the turbopack
server chunks, resolving externals (better-auth, postgres, pino, minio, ...)
by hashed id. With it replaced, every route using them 500'd with
"Failed to load external module <pkg>-<hash>" — confirmed on prod, while
`node .next/standalone/server.js` with the intact tree serves GET / (307)
and /api/health (200) cleanly.
So keep the standalone tree intact and MERGE the complete hoisted prod tree
in with `rsync --ignore-existing`: it adds the custom server's missing CJS
requires (socket.io closure: accepts/ws/engine.io/cors; drizzle-orm/index.cjs)
and skips everything the trace already provides — and tolerates the trace's
pnpm symlinks, where COPY/cp/tar/fs.cpSync all error on symlink-vs-dir.
Validated end-to-end on a host assembly of (intact standalone + merged prod
deps + the polyfilled server bundle): GET / → 307, /api/health → 200, zero
"Failed to load external module", zero MODULE_NOT_FOUND, server listening.
rsync --ignore-existing merge semantics verified in node:20-alpine.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 00:31:33 +02:00
|
|
|
# The Next standalone node_modules is a MATCHED SET with the turbopack
|
|
|
|
|
# server chunks — it resolves turbopack's externalized packages (better-auth,
|
|
|
|
|
# postgres, pino, minio, ...) by their hashed ids, so REPLACING it makes
|
|
|
|
|
# every route that uses them 500 with "Failed to load external module".
|
|
|
|
|
# But the custom server (server-custom.js, CJS via esbuild --packages=external)
|
|
|
|
|
# require()s deps the trace omits or ships ESM-only: socket.io's closure
|
|
|
|
|
# (accepts/ws/engine.io/cors) and drizzle-orm's CJS entry (index.cjs). So
|
|
|
|
|
# MERGE the complete hoisted prod tree INTO the standalone node_modules with
|
|
|
|
|
# rsync --ignore-existing: it ADDS the missing packages/files and SKIPS
|
|
|
|
|
# everything the trace already provides (and unlike COPY/cp it tolerates the
|
|
|
|
|
# trace's pnpm symlinks instead of erroring on symlink-vs-dir). The one
|
|
|
|
|
# thing the standalone server bootstrap would set — globalThis.AsyncLocalStorage
|
2026-06-03 00:03:11 +02:00
|
|
|
# — is handled up-front by src/server-runtime-preamble.ts.
|
fix(docker): merge prod deps into standalone node_modules (not replace)
Replacing the Next standalone node_modules broke turbopack's externalized-
module resolution: the standalone tree is a matched set with the turbopack
server chunks, resolving externals (better-auth, postgres, pino, minio, ...)
by hashed id. With it replaced, every route using them 500'd with
"Failed to load external module <pkg>-<hash>" — confirmed on prod, while
`node .next/standalone/server.js` with the intact tree serves GET / (307)
and /api/health (200) cleanly.
So keep the standalone tree intact and MERGE the complete hoisted prod tree
in with `rsync --ignore-existing`: it adds the custom server's missing CJS
requires (socket.io closure: accepts/ws/engine.io/cors; drizzle-orm/index.cjs)
and skips everything the trace already provides — and tolerates the trace's
pnpm symlinks, where COPY/cp/tar/fs.cpSync all error on symlink-vs-dir.
Validated end-to-end on a host assembly of (intact standalone + merged prod
deps + the polyfilled server bundle): GET / → 307, /api/health → 200, zero
"Failed to load external module", zero MODULE_NOT_FOUND, server listening.
rsync --ignore-existing merge semantics verified in node:20-alpine.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 00:31:33 +02:00
|
|
|
COPY --from=prod-deps --chown=nextjs:nodejs /app/node_modules /opt/prod-node-modules
|
|
|
|
|
RUN apk add --no-cache --virtual .merge-deps rsync \
|
|
|
|
|
&& rsync -a --ignore-existing /opt/prod-node-modules/ ./node_modules/ \
|
|
|
|
|
&& rm -rf /opt/prod-node-modules \
|
|
|
|
|
&& apk del .merge-deps
|
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>
2026-03-26 11:52:51 +01:00
|
|
|
USER nextjs
|
|
|
|
|
EXPOSE 3000
|
fix(audit-wave-10): build-auditor fixes — CSP, server externals, healthcheck
Address the highest-leverage CRITICAL/HIGH/MEDIUM items from the
build-auditor that weren't already covered by Wave 1 (EMAIL_REDIRECT_TO
production guard) or the existing `.dockerignore`.
**C3 — socket.io in standalone trace**
- Add socket.io + @socket.io/redis-adapter to serverExternalPackages
in next.config so the build system sees the dependency (the custom
server is the only importer, no Next route touches it).
- Belt-and-braces: COPY both from the deps stage into the runner stage
of Dockerfile, mirroring the audit's suggested fix.
**H1 — CSP `'unsafe-inline'` in prod**
- Audit recommends nonce-based scripts. Implementing nonces requires
middleware that emits a per-request nonce + threading it through
Next's RSC bootstrap + Server Actions. Out of scope for this wave;
documented the rationale at the CSP definition so the next pass
knows where to start, and noted that the in-the-wild XSS surfaces
are already closed via escapeHtml/escapeUrl in the email + webhook
pipelines.
**H2 — NEXT_PUBLIC_APP_URL validation**
- Add `NEXT_PUBLIC_APP_URL: z.string().url()` to the env schema so a
missing build-time value fails validation instead of silently
inlining the empty string into the client bundle and breaking
multi-origin deploys.
**M3 — serverExternalPackages completeness**
- Add imapflow, mailparser, pdf-lib, sharp, tesseract.js,
@react-pdf/renderer, unpdf — all heavy native/CJS-leaning
server-only deps that should not be route-traced.
**H5 — healthcheck PORT templatization**
- docker-compose.{,prod.}yml: replace hardcoded
`http://localhost:3000/api/health` with `${PORT:-3000}` so
overriding PORT via .env doesn't put the container into a
restart loop.
**M9 — NODE_ENV=production in builder**
- Dockerfile builder stage now sets NODE_ENV=production above
`RUN pnpm build` so the prod-only branches in next.config
(CSP, etc.) compile deterministically.
**M7 — HEALTHCHECK directive in image**
- Add image-level HEALTHCHECK to the app Dockerfile (mirrors the
one in Dockerfile.worker for Redis) so the image is
self-describing for non-compose orchestrators.
Items already addressed prior to this wave:
- C1 (.dockerignore exists, comprehensive)
- C2 (EMAIL_REDIRECT_TO production refusal — Wave 1)
- H4 (compose resource + log limits — already in prod compose)
Tests 1315/1315 throughout.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:30:22 +02:00
|
|
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
|
|
|
|
CMD wget --no-verbose --tries=1 --spider http://localhost:${PORT:-3000}/api/health || exit 1
|
2026-04-08 15:31:33 -04:00
|
|
|
CMD ["node", "server-custom.js"]
|