Files
pn-new-crm/next.config.ts

169 lines
6.5 KiB
TypeScript
Raw Normal View History

import type { NextConfig } from 'next';
import bundleAnalyzer from '@next/bundle-analyzer';
import createNextIntlPlugin from 'next-intl/plugin';
import { withSentryConfig } from '@sentry/nextjs';
// next-intl plugin — points at our request-config entrypoint. Even
// though we ship only English today, the plugin is wired so future
// locale additions are a config-only change, not a code rewrite.
const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts');
const isProd = process.env.NODE_ENV === 'production';
// Wrap the config with the bundle analyzer. Run `ANALYZE=true pnpm build`
// to get treemaps of the client + server bundles after the build
// completes. Pairs with the recharts dynamic-import work the audit
// flagged — gives us the tool to verify chart bundles only ship on the
// dashboard surface and not on routes that don't render them.
const withBundleAnalyzer = bundleAnalyzer({
enabled: process.env.ANALYZE === 'true',
});
/**
* Security headers applied to every response. Per audit-pass-#3 finding:
* the previous config emitted no CSP, X-Frame-Options, HSTS, or
* X-Content-Type-Options the app was open to clickjacking + MIME
* sniffing.
*
* CSP notes:
* - 'unsafe-inline' on style-src is required by Tailwind's runtime
* style injection and Radix; revisit when Tailwind v4 ships a
* nonce story.
* - 'unsafe-eval' on script-src is dev-only Next dev uses eval for
* HMR. Production drops it.
* - connect-src allows ws/wss for Socket.IO and https: for outgoing
* fetches; tighten in prod via per-port branding URLs once we move
* the s3 image references into a known allowlist.
* - img-src https: is wide because port branding pulls from
* s3.portnimara.com plus per-port image URLs configured at runtime.
*/
feat(client-archive): single-client smart-archive dialog + CSP/middleware fixups UI side of the smart-archive backend that shipped in d07f1ed. - SmartArchiveDialog renders the dossier as a sectioned modal: Pipeline interests, Berths (with next-in-line listed), Yachts, Active reservations, Outstanding invoices, In-flight Documenso envelopes, Auto-handled summary. Each section has a per-row decision dropdown with sensible defaults (release for available/under-offer berths, retain for sold berths and yachts, cancel for active reservations, leave for invoices and documents). - High-stakes deals show an amber warning panel + require a reason in the textarea before the Archive button enables. Signed-document acknowledgment checkbox blocks submission until checked. - Wires into client-detail-header in place of the previous dumb ArchiveConfirmDialog (the simple confirm dialog is kept for the restore case until the smart-restore wizard ships). - Pre-flight blocker banner surfaces dossier.blockers (e.g. active reservation on a sold berth) and disables the Archive button entirely. Two side fixes from CSP rollout: - next.config CSP allows unpkg.com in dev so the react-grab devtool loads. Stripped in prod via the existing isProd flag. - middleware whitelist now passes /manifest.json + icon-*.png + apple-touch-icon through unauthenticated, so PWA installability isn't blocked by the auth redirect. Bulk variant + restore wizard + hard-delete-with-email-code land in follow-on commits. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:19:34 +02:00
// Dev-only allow-list: react-grab (the in-page click-to-source devtool)
// is fetched from unpkg, so script/style/connect must allow it. Strip
// these entries in prod via the conditional below.
const devScriptHosts = isProd ? '' : ' http://unpkg.com https://unpkg.com';
const devConnectHosts = isProd ? '' : ' http://unpkg.com https://unpkg.com';
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
// Fallback CSP for paths the proxy doesn't run on (static assets,
// API JSON responses where script-src is moot). Production HTML
// responses get a stricter per-request nonce-based CSP set in
// `src/proxy.ts:applyCsp`; this header just provides a sane default
// so a misconfigured static-only route still has a CSP.
//
// Dev keeps 'unsafe-inline' + 'unsafe-eval' on script-src because
// Next's HMR runtime evaluates code dynamically and the nonce
// machinery doesn't reach it.
const csp = [
"default-src 'self'",
feat(client-archive): single-client smart-archive dialog + CSP/middleware fixups UI side of the smart-archive backend that shipped in d07f1ed. - SmartArchiveDialog renders the dossier as a sectioned modal: Pipeline interests, Berths (with next-in-line listed), Yachts, Active reservations, Outstanding invoices, In-flight Documenso envelopes, Auto-handled summary. Each section has a per-row decision dropdown with sensible defaults (release for available/under-offer berths, retain for sold berths and yachts, cancel for active reservations, leave for invoices and documents). - High-stakes deals show an amber warning panel + require a reason in the textarea before the Archive button enables. Signed-document acknowledgment checkbox blocks submission until checked. - Wires into client-detail-header in place of the previous dumb ArchiveConfirmDialog (the simple confirm dialog is kept for the restore case until the smart-restore wizard ships). - Pre-flight blocker banner surfaces dossier.blockers (e.g. active reservation on a sold berth) and disables the Archive button entirely. Two side fixes from CSP rollout: - next.config CSP allows unpkg.com in dev so the react-grab devtool loads. Stripped in prod via the existing isProd flag. - middleware whitelist now passes /manifest.json + icon-*.png + apple-touch-icon through unauthenticated, so PWA installability isn't blocked by the auth redirect. Bulk variant + restore wizard + hard-delete-with-email-code land in follow-on commits. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:19:34 +02:00
`script-src 'self' 'unsafe-inline'${isProd ? '' : " 'unsafe-eval'"}${devScriptHosts}`,
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: blob: https:",
"font-src 'self' data:",
feat(client-archive): single-client smart-archive dialog + CSP/middleware fixups UI side of the smart-archive backend that shipped in d07f1ed. - SmartArchiveDialog renders the dossier as a sectioned modal: Pipeline interests, Berths (with next-in-line listed), Yachts, Active reservations, Outstanding invoices, In-flight Documenso envelopes, Auto-handled summary. Each section has a per-row decision dropdown with sensible defaults (release for available/under-offer berths, retain for sold berths and yachts, cancel for active reservations, leave for invoices and documents). - High-stakes deals show an amber warning panel + require a reason in the textarea before the Archive button enables. Signed-document acknowledgment checkbox blocks submission until checked. - Wires into client-detail-header in place of the previous dumb ArchiveConfirmDialog (the simple confirm dialog is kept for the restore case until the smart-restore wizard ships). - Pre-flight blocker banner surfaces dossier.blockers (e.g. active reservation on a sold berth) and disables the Archive button entirely. Two side fixes from CSP rollout: - next.config CSP allows unpkg.com in dev so the react-grab devtool loads. Stripped in prod via the existing isProd flag. - middleware whitelist now passes /manifest.json + icon-*.png + apple-touch-icon through unauthenticated, so PWA installability isn't blocked by the auth redirect. Bulk variant + restore wizard + hard-delete-with-email-code land in follow-on commits. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:19:34 +02:00
`connect-src 'self' ws: wss: https:${devConnectHosts}`,
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
"object-src 'none'",
].join('; ');
const securityHeaders = [
{ key: 'Content-Security-Policy', value: csp },
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
{ key: 'Permissions-Policy', value: 'camera=(self), microphone=(), geolocation=()' },
...(isProd
? [{ key: 'Strict-Transport-Security', value: 'max-age=31536000; includeSubDomains' }]
: []),
];
const nextConfig: NextConfig = {
output: 'standalone',
feat(ui): broad consistency sweep — sources, dates, comboboxes, milestones Mobile + responsive - berth-form full-width on phones (was 480px fixed → overflowed iPhone) - currency-input switched to inputMode=decimal with live thousands separator - client-form Country/Timezone/Source/Preferred-Contact full-width <sm - contacts row restructured so Primary toggle + Remove get their own strip - customize-dashboard footer stacks vertically on mobile; Done full-width - interest-form client/berth pickers no longer cmdk-filter on UUID (typing "Carlos" now returns Carlos Vega instead of "No clients found") Data + consistency - SOURCES + SOURCE_LABELS + formatSource() in lib/constants; 9 surfaces now resolve interest/client source from one place - INTEREST_OUTCOMES adds lost_other (picker, badge, timeline) - Berth options natural-sort A1 → A2 → … → A10 via lib/utils/mooring-sort - archiver downgraded ^8 → ^7.0.1 so the GDPR export route compiles - TableBody last-row uses border-b-0 (not border-0); colored left-accent on the bottom berth row now renders - Hide Invite-to-Portal until port setting === true (was !== false default-show) - OwnerPicker primer query resolves entity name on first paint (no more UUID flash before the popover opens) Terminology - Replaced user-facing "Documenso" with "signing service" / "Generated EOI" / "Manual EOI" in 8 components (admin/internal references kept) - Plainer status-change copy on berth-detail-header Forms + editing - InlineEditableField gained a `date` variant (native picker); applied to company incorporation date and ready for other YYYY-MM-DD plaintext fields - Inline source picker on interest-tabs detail (was free text) - TagPicker self-hides when port has no tags AND nothing is selected - New ReminderDaysInput with preset chips (1d / 3d / 1wk / 2wk / 1mo / custom) - Compose dialog follow-up is now a toggle that reveals datetime picker Pipeline milestones - changeStageSchema accepts optional milestoneDate; service stamps it on the matching date column instead of always using now - MilestoneAdvanceButton popover collects a back-date before stage advance - Applied to every "Mark X manually" surface on the interest overview EOI / linked-berths polish - Add-bypass row aligned inline with toggle descriptions - Tooltips on "Specifically pitching" / "Mark in EOI bundle" explain their legal vs. public-map consequences Surfaces - Companies list now has the column picker + persisted hidden-column prefs - NotesList aggregate flag enabled on clients, companies, residential_clients (yachts already aggregated) ft/m unit toggle (interim, before drift fix) - "Berth size desired" gets a section-level ft/m toggle; per-field hint shows the converted value. Storage stays canonical-ft for now; the drift-safe persistence migration is the next step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 14:50:58 +02:00
// Hide the floating dev indicator (the little circle/N badge in the
// corner). Compile errors still surface via the full overlay; this
// only removes the idle "everything is fine" indicator that's been
// visible in every screenshot from the iPhone testing pass.
devIndicators: false,
// LAN access from a real iPhone hits the dev server via the Mac's
// local IP (e.g. 192.168.x.x), not localhost. Next 15 surfaces a
// warning for cross-origin /_next/* fetches unless we allow-list the
// origins explicitly. Wildcard the 192.168/0.0.0.0 ranges in dev so
// any LAN device works without a config edit per network.
...(isProd ? {} : { allowedDevOrigins: ['192.168.1.42'] }),
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
// Native/CJS-leaning server-only packages — list here so Next doesn't
// bundle them into the route trace (slower cold start + risk that
// native bindings fail at runtime). Build-auditor C3+M3: socket.io
// is only imported by the custom server entry point, so the Next
// tracer has no reason to include it; listing here makes the
// dependency visible to the build system.
serverExternalPackages: [
'pino',
'pino-pretty',
'bullmq',
'ioredis',
'minio',
'postgres',
'better-auth',
'nodemailer',
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
'socket.io',
'@socket.io/redis-adapter',
'imapflow',
'mailparser',
'pdf-lib',
'sharp',
'tesseract.js',
'@react-pdf/renderer',
'unpdf',
],
images: {
remotePatterns: [{ protocol: 'https', hostname: '*.portnimara.com' }],
},
typedRoutes: true,
outputFileTracingIncludes: {
// Bundle the EOI source PDF so the in-app EOI pathway can read it at
// runtime in the standalone build. Reading via fs.readFile from
// process.cwd() requires the file to be traced explicitly.
'/api/v1/document-templates/**': ['./assets/eoi-template.pdf'],
},
async redirects() {
return [
{
source: '/:portSlug/documents/files',
destination: '/:portSlug/documents',
permanent: true,
},
{
source: '/:portSlug/documents/files/:path*',
destination: '/:portSlug/documents',
permanent: true,
},
];
},
async headers() {
return [
{
source: '/:path*',
headers: securityHeaders,
},
];
},
};
// Sentry wrapper is conditional: if NEXT_PUBLIC_SENTRY_DSN isn't set we
// skip its build-time source-map upload + middleware injection so dev
// builds stay fast and CI doesn't need credentials. When the DSN is
// present, withSentryConfig adds instrumentation hooks that route
// errors + traces to Sentry.
const withSentry = process.env.NEXT_PUBLIC_SENTRY_DSN
? (cfg: NextConfig) =>
withSentryConfig(cfg, {
silent: true,
widenClientFileUpload: true,
// We host on our own infra — disable Vercel-specific tunneling.
tunnelRoute: undefined,
// Strip Sentry debug logger from prod bundle.
disableLogger: true,
})
: (cfg: NextConfig) => cfg;
export default withSentry(withBundleAnalyzer(withNextIntl(nextConfig)));