fix homepage storefront proxy and CSP
Build & Deploy / Build & Push Docker Image (push) Successful in 2m50s
Test / Type Check (all packages) (push) Successful in 52s
Build & Deploy / Deploy to VPS (push) Successful in 3s
Test / API Unit Tests (push) Successful in 54s
Test / Homepage Unit Tests (push) Successful in 44s
Test / Storefront Unit Tests (push) Successful in 41s
Test / Admin Unit Tests (push) Successful in 42s
Test / Dashboard Unit Tests (push) Successful in 41s
Test / API Integration Tests (push) Successful in 1m0s
Build & Deploy / Build & Push Docker Image (push) Successful in 2m50s
Test / Type Check (all packages) (push) Successful in 52s
Build & Deploy / Deploy to VPS (push) Successful in 3s
Test / API Unit Tests (push) Successful in 54s
Test / Homepage Unit Tests (push) Successful in 44s
Test / Storefront Unit Tests (push) Successful in 41s
Test / Admin Unit Tests (push) Successful in 42s
Test / Dashboard Unit Tests (push) Successful in 41s
Test / API Integration Tests (push) Successful in 1m0s
This commit is contained in:
@@ -54,8 +54,18 @@ const nextConfig: NextConfig = {
|
|||||||
process.env.DASHBOARD_INTERNAL_URL ?? 'http://dashboard:3001';
|
process.env.DASHBOARD_INTERNAL_URL ?? 'http://dashboard:3001';
|
||||||
const adminOrigin =
|
const adminOrigin =
|
||||||
process.env.ADMIN_INTERNAL_URL ?? 'http://admin:3002';
|
process.env.ADMIN_INTERNAL_URL ?? 'http://admin:3002';
|
||||||
|
const storefrontOrigin =
|
||||||
|
process.env.STOREFRONT_INTERNAL_URL ?? 'http://storefront:3004';
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
{
|
||||||
|
source: '/storefront',
|
||||||
|
destination: `${storefrontOrigin}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: '/storefront/:path*',
|
||||||
|
destination: `${storefrontOrigin}/:path*`,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
source: '/dashboard',
|
source: '/dashboard',
|
||||||
destination: `${dashboardOrigin}/dashboard`,
|
destination: `${dashboardOrigin}/dashboard`,
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ export function buildContentSecurityPolicy(
|
|||||||
development: boolean,
|
development: boolean,
|
||||||
apiOrigin?: string,
|
apiOrigin?: string,
|
||||||
): string {
|
): string {
|
||||||
const scriptSources = ["'self'", `'nonce-${nonce}'`, "'strict-dynamic'", "'unsafe-eval'"];
|
void nonce;
|
||||||
|
const scriptSources = ["'self'", "'unsafe-inline'", "'unsafe-eval'"];
|
||||||
|
|
||||||
const styleSources = ["'self'", "'unsafe-inline'"];
|
const styleSources = ["'self'", "'unsafe-inline'"];
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import type { NextRequest } from 'next/server';
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
|
|
||||||
const dashboardLanguageCookie = 'rentaldrivego-language';
|
const dashboardLanguageCookie = 'rentaldrivego-language';
|
||||||
|
const reservedProxySegments = new Set(['dashboard', 'admin', 'storefront']);
|
||||||
|
|
||||||
function applySecurityHeaders(response: NextResponse, contentSecurityPolicy: string): NextResponse {
|
function applySecurityHeaders(response: NextResponse, contentSecurityPolicy: string): NextResponse {
|
||||||
response.headers.set('Content-Security-Policy', contentSecurityPolicy);
|
response.headers.set('Content-Security-Policy', contentSecurityPolicy);
|
||||||
@@ -65,10 +66,17 @@ export function proxy(request: NextRequest): NextResponse {
|
|||||||
return applySecurityHeaders(response, csp);
|
return applySecurityHeaders(response, csp);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect locale-less paths (e.g. /sign-in → /en/sign-in), but not /dashboard, /admin, or files
|
const pathSegments = request.nextUrl.pathname.split('/').filter(Boolean);
|
||||||
|
if (isLocale(pathSegments[0]) && pathSegments[1] === 'storefront') {
|
||||||
|
const destination = request.nextUrl.clone();
|
||||||
|
destination.pathname = `/${pathSegments.slice(1).join('/')}`;
|
||||||
|
return applySecurityHeaders(NextResponse.redirect(destination), csp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect locale-less paths (e.g. /sign-in → /en/sign-in), but not proxied app paths or files.
|
||||||
const firstSegment = request.nextUrl.pathname.split('/')[1];
|
const firstSegment = request.nextUrl.pathname.split('/')[1];
|
||||||
const isFile = /\.\w+$/.test(request.nextUrl.pathname);
|
const isFile = /\.\w+$/.test(request.nextUrl.pathname);
|
||||||
if (firstSegment && !isFile && !isLocale(firstSegment) && firstSegment !== 'dashboard' && firstSegment !== 'admin') {
|
if (firstSegment && !isFile && !isLocale(firstSegment) && !reservedProxySegments.has(firstSegment)) {
|
||||||
const cookieLocale = request.cookies.get(localeCookie)?.value;
|
const cookieLocale = request.cookies.get(localeCookie)?.value;
|
||||||
const locale = isLocale(cookieLocale)
|
const locale = isLocale(cookieLocale)
|
||||||
? cookieLocale
|
? cookieLocale
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ test('explicit dark theme is present on the server-rendered root', async ({ brow
|
|||||||
const csp = response?.headers()['content-security-policy'] ?? '';
|
const csp = response?.headers()['content-security-policy'] ?? '';
|
||||||
const nonce = await page.locator('#theme-bootstrap').getAttribute('nonce');
|
const nonce = await page.locator('#theme-bootstrap').getAttribute('nonce');
|
||||||
expect(nonce).toBeTruthy();
|
expect(nonce).toBeTruthy();
|
||||||
expect(csp).toContain(`'nonce-${nonce}'`);
|
expect(csp).toContain("'unsafe-inline'");
|
||||||
|
expect(csp).not.toContain("'strict-dynamic'");
|
||||||
await context.close();
|
await context.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ describe('CSP foundation', () => {
|
|||||||
it('allows required runtime evaluation without enabling development-only sources', () => {
|
it('allows required runtime evaluation without enabling development-only sources', () => {
|
||||||
expect(buildContentSecurityPolicy('abc', true)).toContain("'unsafe-eval'");
|
expect(buildContentSecurityPolicy('abc', true)).toContain("'unsafe-eval'");
|
||||||
expect(buildContentSecurityPolicy('abc', false)).toContain("'unsafe-eval'");
|
expect(buildContentSecurityPolicy('abc', false)).toContain("'unsafe-eval'");
|
||||||
expect(buildContentSecurityPolicy('abc', false)).toContain("'nonce-abc'");
|
expect(buildContentSecurityPolicy('abc', false)).toContain("'unsafe-inline'");
|
||||||
|
expect(buildContentSecurityPolicy('abc', false)).not.toContain("'strict-dynamic'");
|
||||||
|
expect(buildContentSecurityPolicy('abc', false)).not.toContain("'nonce-abc'");
|
||||||
expect(buildContentSecurityPolicy('abc', false)).toContain(
|
expect(buildContentSecurityPolicy('abc', false)).toContain(
|
||||||
"style-src 'self' 'unsafe-inline'",
|
"style-src 'self' 'unsafe-inline'",
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
const nextServer = vi.hoisted(() => {
|
||||||
|
const next = vi.fn((init?: { request?: { headers?: Headers } }) => ({
|
||||||
|
kind: 'next',
|
||||||
|
init,
|
||||||
|
cookies: {
|
||||||
|
set: vi.fn(),
|
||||||
|
},
|
||||||
|
headers: new Headers(),
|
||||||
|
}));
|
||||||
|
const redirect = vi.fn((url: URL) => ({
|
||||||
|
kind: 'redirect',
|
||||||
|
url: url.toString(),
|
||||||
|
cookies: {
|
||||||
|
set: vi.fn(),
|
||||||
|
},
|
||||||
|
headers: new Headers(),
|
||||||
|
}));
|
||||||
|
return { next, redirect };
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock('next/server', () => ({
|
||||||
|
NextResponse: {
|
||||||
|
next: nextServer.next,
|
||||||
|
redirect: nextServer.redirect,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
function cloneableUrl(input: string): URL {
|
||||||
|
const url = new URL(input);
|
||||||
|
(url as URL & { clone: () => URL }).clone = () => cloneableUrl(url.toString());
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
function request(input: string) {
|
||||||
|
return {
|
||||||
|
nextUrl: cloneableUrl(input),
|
||||||
|
cookies: {
|
||||||
|
get: vi.fn(),
|
||||||
|
},
|
||||||
|
headers: new Headers(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
nextServer.next.mockClear();
|
||||||
|
nextServer.redirect.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('homepage proxy app boundaries', () => {
|
||||||
|
it('does not localize storefront proxy paths', async () => {
|
||||||
|
const { proxy } = await import('@/proxy');
|
||||||
|
|
||||||
|
const response = proxy(request('https://rentaldrivego.ma/storefront/en') as never) as any;
|
||||||
|
|
||||||
|
expect(response.kind).toBe('next');
|
||||||
|
expect(nextServer.redirect).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('redirects locale-prefixed storefront paths back to the storefront proxy mount', async () => {
|
||||||
|
const { proxy } = await import('@/proxy');
|
||||||
|
|
||||||
|
const response = proxy(request('https://rentaldrivego.ma/en/storefront/en') as never) as any;
|
||||||
|
|
||||||
|
expect(response).toMatchObject({
|
||||||
|
kind: 'redirect',
|
||||||
|
url: 'https://rentaldrivego.ma/storefront/en',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user