diff --git a/.dockerignore b/.dockerignore index 74fa836a6..71cdb8b9d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,4 +4,6 @@ Dockerfile .gitignore .DS_Store node_modules -.idea \ No newline at end of file +.idea +.env +.env.* diff --git a/Dockerfile b/Dockerfile index 78419e652..43e127e35 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,6 +12,7 @@ FROM node:22-alpine AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . +COPY docker/middleware.ts ./src ARG DATABASE_TYPE ARG BASE_PATH @@ -41,7 +42,7 @@ RUN set -x \ && apk add --no-cache curl # Script dependencies -RUN pnpm add npm-run-all dotenv prisma@6.8.2 +RUN pnpm add npm-run-all dotenv chalk semver prisma@6.16.0 @prisma/adapter-pg@6.16.0 # Permissions for prisma RUN chown -R nextjs:nodejs node_modules/.pnpm/ @@ -49,15 +50,13 @@ RUN chown -R nextjs:nodejs node_modules/.pnpm/ COPY --from=builder --chown=nextjs:nodejs /app/public ./public COPY --from=builder /app/prisma ./prisma COPY --from=builder /app/scripts ./scripts +COPY --from=builder /app/generated ./generated # Automatically leverage output traces to reduce image size # https://nextjs.org/docs/advanced-features/output-file-tracing COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static -# Custom routes -RUN mv ./.next/routes-manifest.json ./.next/routes-manifest-orig.json - USER nextjs EXPOSE 3000 diff --git a/docker/middleware.ts b/docker/middleware.ts new file mode 100644 index 000000000..ae143140d --- /dev/null +++ b/docker/middleware.ts @@ -0,0 +1,69 @@ +import { NextRequest, NextResponse } from 'next/server'; + +export const config = { + matcher: '/:path*', +}; + +const TRACKER_NAME = '/script.js'; +const COLLECT_ENDPOINT = '/api/send'; + +const apiHeaders = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Headers': '*', + 'Access-Control-Allow-Methods': 'GET, DELETE, POST, PUT', + 'Access-Control-Max-Age': process.env.CORS_MAX_AGE || '86400', + 'Cache-Control': 'no-cache', +}; + +const trackerHeaders = { + 'Access-Control-Allow-Origin': '*', + 'Cache-Control': 'public, max-age=86400, must-revalidate', +}; + +function customCollectEndpoint(request: NextRequest) { + const collectEndpoint = process.env.COLLECT_API_ENDPOINT; + + if (collectEndpoint) { + const url = request.nextUrl.clone(); + + if (url.pathname.endsWith(collectEndpoint)) { + url.pathname = COLLECT_ENDPOINT; + return NextResponse.rewrite(url, { headers: apiHeaders }); + } + } +} + +function customScriptName(request: NextRequest) { + const scriptName = process.env.TRACKER_SCRIPT_NAME; + + if (scriptName) { + const url = request.nextUrl.clone(); + const names = scriptName.split(',').map(name => name.trim().replace(/^\/+/, '')); + + if (names.find(name => url.pathname.endsWith(name))) { + url.pathname = TRACKER_NAME; + return NextResponse.rewrite(url, { headers: trackerHeaders }); + } + } +} + +function customScriptUrl(request: NextRequest) { + const scriptUrl = process.env.TRACKER_SCRIPT_URL; + + if (scriptUrl && request.nextUrl.pathname.endsWith(TRACKER_NAME)) { + return NextResponse.rewrite(scriptUrl, { headers: trackerHeaders }); + } +} + +export default function middleware(req: NextRequest) { + const fns = [customCollectEndpoint, customScriptName, customScriptUrl]; + + for (const fn of fns) { + const res = fn(req); + if (res) { + return res; + } + } + + return NextResponse.next(); +} diff --git a/package.json b/package.json index 8e35c620a..d2325de54 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "build": "npm-run-all check-env build-db check-db build-tracker build-geo build-app", "start": "next start", "build-docker": "npm-run-all build-db build-tracker build-geo build-app", - "start-docker": "npm-run-all check-db update-tracker set-routes-manifest start-server", + "start-docker": "npm-run-all check-db update-tracker start-server", "start-env": "node scripts/start-env.js", "start-server": "node server.js", "build-app": "next build --turbo", @@ -28,7 +28,6 @@ "build-db": "npm-run-all build-db-client build-prisma-client", "build-db-schema": "prisma db pull", "build-db-client": "prisma generate", - "set-routes-manifest": "node scripts/set-routes-manifest.js", "update-tracker": "node scripts/update-tracker.js", "update-db": "prisma migrate deploy", "check-db": "node scripts/check-db.js", diff --git a/scripts/set-routes-manifest.js b/scripts/set-routes-manifest.js deleted file mode 100644 index 392c7b743..000000000 --- a/scripts/set-routes-manifest.js +++ /dev/null @@ -1,81 +0,0 @@ -/* eslint-disable no-console */ -import 'dotenv/config'; -import fs from 'node:fs'; -import path from 'node:path'; -import { createRequire } from 'module'; - -const require = createRequire(import.meta.url); - -const routesManifestPath = path.resolve(process.cwd(), '.next/routes-manifest.json'); -const originalPath = path.resolve(process.cwd(), '.next/routes-manifest-orig.json'); -const originalManifest = require(originalPath); - -const basePath = originalManifest.basePath; - -const API_PATH = basePath + '/api/:path*'; -const TRACKER_SCRIPT = basePath + '/script.js'; - -const collectApiEndpoint = process.env.COLLECT_API_ENDPOINT; -const trackerScriptName = process.env.TRACKER_SCRIPT_NAME; - -const headers = []; -const rewrites = []; - -if (collectApiEndpoint) { - const apiRoute = originalManifest.headers.find(route => route.source === API_PATH); - const routeRegex = new RegExp(apiRoute.regex); - - const normalizedSource = basePath + collectApiEndpoint; - - rewrites.push({ - source: normalizedSource, - destination: basePath + '/api/send', - }); - - if (!routeRegex.test(normalizedSource)) { - headers.push({ - source: normalizedSource, - headers: apiRoute.headers, - }); - } -} - -if (trackerScriptName) { - const trackerRoute = originalManifest.headers.find(route => route.source === TRACKER_SCRIPT); - - const names = trackerScriptName?.split(',').map(name => name.trim()); - - if (names) { - names.forEach(name => { - const normalizedSource = `${basePath}/${name.replace(/^\/+/, '')}`; - - rewrites.push({ - source: normalizedSource, - destination: TRACKER_SCRIPT, - }); - - headers.push({ - source: normalizedSource, - headers: trackerRoute.headers, - }); - }); - } -} - -const routesManifest = { ...originalManifest }; - -if (rewrites.length !== 0) { - const { buildCustomRoute } = require('next/dist/lib/build-custom-route'); - - const builtHeaders = headers.map(header => buildCustomRoute('header', header)); - const builtRewrites = rewrites.map(rewrite => buildCustomRoute('rewrite', rewrite)); - - routesManifest.headers = [...originalManifest.headers, ...builtHeaders]; - routesManifest.rewrites = [...builtRewrites, ...originalManifest.rewrites]; - - console.log('Using updated Next.js routes manifest'); -} else { - console.log('Using original Next.js routes manifest'); -} - -fs.writeFileSync(routesManifestPath, JSON.stringify(routesManifest, null, 2)); diff --git a/src/components/input/FilterBar.tsx b/src/components/input/FilterBar.tsx index 96ad51b4f..b81f77e30 100644 --- a/src/components/input/FilterBar.tsx +++ b/src/components/input/FilterBar.tsx @@ -25,13 +25,14 @@ export function FilterBar({ websiteId }: { websiteId: string }) { const { formatValue } = useFormat(); const { router, + pathname, updateParams, replaceParams, query: { segment, cohort }, } = useNavigation(); const { filters, operatorLabels } = useFilters(); const { data, isLoading } = useWebsiteSegmentQuery(websiteId, segment || cohort); - const canSaveSegment = filters.length > 0 && !segment && !cohort; + const canSaveSegment = filters.length > 0 && !segment && !cohort && !pathname.includes('/share'); const handleCloseFilter = (param: string) => { router.push(updateParams({ [param]: undefined }));