diff --git a/docker/middleware.ts b/docker/middleware.ts index 4b189df89..15111884d 100644 --- a/docker/middleware.ts +++ b/docker/middleware.ts @@ -1,4 +1,5 @@ import { type NextRequest, NextResponse } from 'next/server'; +import { matchesConfiguredPath } from '@/lib/match-configured-path'; export const config = { matcher: '/:path*', @@ -7,6 +8,7 @@ export const config = { const TRACKER_PATH = '/script.js'; const COLLECT_PATH = '/api/send'; const LOGIN_PATH = '/login'; +const BASE_PATH = process.env.BASE_PATH || ''; const apiHeaders = { 'Access-Control-Allow-Origin': '*', @@ -27,7 +29,7 @@ function customCollectEndpoint(request: NextRequest) { if (collectEndpoint) { const url = request.nextUrl.clone(); - if (url.pathname.endsWith(collectEndpoint)) { + if (matchesConfiguredPath(url.pathname, collectEndpoint, BASE_PATH)) { url.pathname = COLLECT_PATH; return NextResponse.rewrite(url, { headers: apiHeaders }); } @@ -41,7 +43,7 @@ function customScriptName(request: NextRequest) { const url = request.nextUrl.clone(); const names = scriptName.split(',').map(name => name.trim().replace(/^\/+/, '')); - if (names.find(name => url.pathname.endsWith(name))) { + if (names.find(name => matchesConfiguredPath(url.pathname, name, BASE_PATH))) { url.pathname = TRACKER_PATH; return NextResponse.rewrite(url, { headers: trackerHeaders }); } @@ -51,7 +53,7 @@ function customScriptName(request: NextRequest) { function customScriptUrl(request: NextRequest) { const scriptUrl = process.env.TRACKER_SCRIPT_URL; - if (scriptUrl && request.nextUrl.pathname.endsWith(TRACKER_PATH)) { + if (scriptUrl && matchesConfiguredPath(request.nextUrl.pathname, TRACKER_PATH, BASE_PATH)) { return NextResponse.rewrite(scriptUrl, { headers: trackerHeaders }); } } @@ -59,7 +61,7 @@ function customScriptUrl(request: NextRequest) { function disableLogin(request: NextRequest) { const loginDisabled = process.env.DISABLE_LOGIN; - if (loginDisabled && request.nextUrl.pathname.endsWith(LOGIN_PATH)) { + if (loginDisabled && matchesConfiguredPath(request.nextUrl.pathname, LOGIN_PATH, BASE_PATH)) { return new NextResponse('Access denied', { status: 403 }); } } diff --git a/src/lib/__tests__/match-configured-path.test.ts b/src/lib/__tests__/match-configured-path.test.ts new file mode 100644 index 000000000..f70ba9c1e --- /dev/null +++ b/src/lib/__tests__/match-configured-path.test.ts @@ -0,0 +1,17 @@ +import { matchesConfiguredPath } from '../match-configured-path'; + +test('matches the exact configured path', () => { + expect(matchesConfiguredPath('/d.js', 'd.js')).toBe(true); +}); + +test('does not match unrelated asset paths that only share the suffix', () => { + expect(matchesConfiguredPath('/_next/static/chunks/app/dashboard.js', 'd.js')).toBe(false); +}); + +test('matches paths under the configured base path', () => { + expect(matchesConfiguredPath('/umami/d.js', 'd.js', '/umami')).toBe(true); +}); + +test('normalizes leading slashes in configured paths', () => { + expect(matchesConfiguredPath('/script.js', '/script.js')).toBe(true); +}); diff --git a/src/lib/match-configured-path.ts b/src/lib/match-configured-path.ts new file mode 100644 index 000000000..104319f2d --- /dev/null +++ b/src/lib/match-configured-path.ts @@ -0,0 +1,38 @@ +function normalizePathname(pathname?: string) { + if (!pathname) { + return ''; + } + + return `/${pathname.replace(/^\/+/, '')}`; +} + +function normalizeBasePath(basePath?: string) { + if (!basePath) { + return ''; + } + + return `/${basePath.replace(/^\/+|\/+$/g, '')}`; +} + +export function matchesConfiguredPath( + pathname: string, + configuredPath?: string, + basePath?: string, +) { + const normalizedPathname = normalizePathname(pathname); + const normalizedConfiguredPath = normalizePathname(configuredPath); + + if (!normalizedConfiguredPath) { + return false; + } + + if (normalizedPathname === normalizedConfiguredPath) { + return true; + } + + const normalizedBasePath = normalizeBasePath(basePath); + + return normalizedBasePath + ? normalizedPathname === `${normalizedBasePath}${normalizedConfiguredPath}` + : false; +}