Files
pn-new-crm/Dockerfile

70 lines
3.4 KiB
Docker
Raw Permalink Normal View History

# 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
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile --prod=false
# 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
# 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
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
ENV NEXT_TELEMETRY_DISABLED=1
ENV SKIP_ENV_VALIDATION=1
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
COPY --from=builder --chown=nextjs:nodejs /app/dist/server.js ./server-custom.js
# 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
# — is handled up-front by src/server-runtime-preamble.ts.
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
# pg_dump for the backup/DR bundle engine (src/lib/services/backup.service.ts
# spawns `pg_dump`). Version pinned to match the postgres:16 server.
RUN apk add --no-cache postgresql16-client
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
CMD ["node", "server-custom.js"]