chore: bulk commit of pre-existing changes
Build & Deploy / Build & Push Docker Image (push) Successful in 48s
Test / API Unit Tests (push) Successful in 9m48s
Test / Marketplace Unit Tests (push) Successful in 9m38s
Test / Admin Unit Tests (push) Successful in 9m33s
Build & Deploy / Deploy to VPS (push) Has been cancelled
Test / Dashboard Unit Tests (push) Has been cancelled
Test / API Integration Tests (push) Has been cancelled
Build & Deploy / Build & Push Docker Image (push) Successful in 48s
Test / API Unit Tests (push) Successful in 9m48s
Test / Marketplace Unit Tests (push) Successful in 9m38s
Test / Admin Unit Tests (push) Successful in 9m33s
Build & Deploy / Deploy to VPS (push) Has been cancelled
Test / Dashboard Unit Tests (push) Has been cancelled
Test / API Integration Tests (push) Has been cancelled
Includes: - Admin dashboard layout and page updates - API account auth module (routes, schemas, service) - Dashboard sign-up form extraction, middleware refactor - Marketplace proxy and middleware updates - CORS origins expansion for dev environment - Seed email domain correction (.com → .ma) - Docs: create-account guide, fleet page, signup plan - Various UI component and layout refinements
This commit is contained in:
@@ -1,11 +1,15 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
|
||||
const { buildSecurityHeaders } = require('../../config/nextSecurityHeaders')
|
||||
const { buildSecurityHeaders, normalizeAssetPrefix } = require('../../config/nextSecurityHeaders')
|
||||
|
||||
// The marketplace proxies /dashboard and /admin to their own Next dev servers.
|
||||
// Allow those asset-prefix origins so proxied pages can load chunks and HMR.
|
||||
const dashboardAssetSource = normalizeAssetPrefix(
|
||||
process.env.DASHBOARD_ASSET_PREFIX ?? (process.env.NODE_ENV !== 'production' ? 'http://localhost:3001' : undefined),
|
||||
'/dashboard',
|
||||
)
|
||||
const securityHeaders = buildSecurityHeaders({
|
||||
assetSources: [process.env.DASHBOARD_ASSET_PREFIX, process.env.ADMIN_ASSET_PREFIX],
|
||||
assetSources: [dashboardAssetSource, process.env.ADMIN_ASSET_PREFIX],
|
||||
})
|
||||
const nextConfig = {
|
||||
images: {
|
||||
@@ -25,11 +29,27 @@ const nextConfig = {
|
||||
},
|
||||
]
|
||||
},
|
||||
async redirects() {
|
||||
return [
|
||||
{
|
||||
source: '/dashboard/dashboard',
|
||||
destination: '/dashboard',
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: '/dashboard/dashboard/:path*',
|
||||
destination: '/dashboard/:path*',
|
||||
permanent: false,
|
||||
},
|
||||
]
|
||||
},
|
||||
async rewrites() {
|
||||
const dashboardOrigin = process.env.DASHBOARD_INTERNAL_URL ?? 'http://dashboard:3001'
|
||||
const adminOrigin = process.env.ADMIN_INTERNAL_URL ?? 'http://admin:3002'
|
||||
|
||||
return [
|
||||
// Dashboard has basePath '/dashboard'. The proxy preserves the prefix
|
||||
// so the dashboard can strip it and route internally.
|
||||
{
|
||||
source: '/dashboard',
|
||||
destination: `${dashboardOrigin}/dashboard`,
|
||||
@@ -38,6 +58,7 @@ const nextConfig = {
|
||||
source: '/dashboard/:path*',
|
||||
destination: `${dashboardOrigin}/dashboard/:path*`,
|
||||
},
|
||||
// Admin routes (also uses basePath)
|
||||
{
|
||||
source: '/admin',
|
||||
destination: `${adminOrigin}/admin`,
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<rect width="32" height="32" rx="8" fill="#0f172a" />
|
||||
<text
|
||||
x="16"
|
||||
y="21"
|
||||
text-anchor="middle"
|
||||
font-family="Inter, Arial, sans-serif"
|
||||
font-size="18"
|
||||
font-weight="700"
|
||||
fill="#ffffff"
|
||||
>
|
||||
R
|
||||
</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 302 B |
@@ -8,13 +8,19 @@ const nextServer = vi.hoisted(() => {
|
||||
set: vi.fn(),
|
||||
},
|
||||
}))
|
||||
const redirect = vi.fn((url: URL, status?: number) => ({
|
||||
kind: 'redirect',
|
||||
url: url.toString(),
|
||||
status,
|
||||
}))
|
||||
const MockNextResponse = vi.fn((body?: string, init?: { status?: number }) => ({
|
||||
kind: 'response',
|
||||
body,
|
||||
status: init?.status,
|
||||
})) as any
|
||||
MockNextResponse.next = next
|
||||
return { next, MockNextResponse }
|
||||
MockNextResponse.redirect = redirect
|
||||
return { next, redirect, MockNextResponse }
|
||||
})
|
||||
|
||||
vi.mock('next/server', () => ({
|
||||
@@ -25,6 +31,7 @@ function request(options: { cookies?: Record<string, string>; headers?: Record<s
|
||||
const cookies = new Map(Object.entries(options.cookies ?? {}))
|
||||
const headers = new Headers(options.headers ?? {})
|
||||
return {
|
||||
nextUrl: cloneableUrl('http://localhost:3000/'),
|
||||
cookies: {
|
||||
get: vi.fn((name: string) => {
|
||||
const value = cookies.get(name)
|
||||
@@ -35,8 +42,25 @@ function request(options: { cookies?: Record<string, string>; headers?: Record<s
|
||||
}
|
||||
}
|
||||
|
||||
function cloneableUrl(input: string): URL {
|
||||
const url = new URL(input)
|
||||
;(url as URL & { clone: () => URL }).clone = () => cloneableUrl(url.toString())
|
||||
return url
|
||||
}
|
||||
|
||||
function middlewareRequest(input: string) {
|
||||
return {
|
||||
cookies: {
|
||||
get: vi.fn(),
|
||||
},
|
||||
headers: new Headers(),
|
||||
nextUrl: cloneableUrl(input),
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
nextServer.next.mockClear()
|
||||
nextServer.redirect.mockClear()
|
||||
})
|
||||
|
||||
describe('marketplace proxy language bootstrap', () => {
|
||||
@@ -102,3 +126,21 @@ describe('marketplace proxy language bootstrap', () => {
|
||||
expect(response.init?.request?.headers?.get('cookie')).toBe('rentaldrivego-language=fr')
|
||||
})
|
||||
})
|
||||
|
||||
describe('marketplace middleware dashboard path canonicalization', () => {
|
||||
it('redirects duplicate dashboard prefixes to the clean dashboard URL', async () => {
|
||||
const { proxy } = await import('./proxy')
|
||||
|
||||
const response = proxy(middlewareRequest('http://localhost:3000/dashboard/dashboard') as never)
|
||||
|
||||
expect(response).toEqual({ kind: 'redirect', url: 'http://localhost:3000/dashboard', status: 307 })
|
||||
})
|
||||
|
||||
it('preserves nested dashboard paths while removing the duplicate prefix', async () => {
|
||||
const { proxy } = await import('./proxy')
|
||||
|
||||
const response = proxy(middlewareRequest('http://localhost:3000/dashboard/dashboard/fleet?tab=active') as never)
|
||||
|
||||
expect(response).toEqual({ kind: 'redirect', url: 'http://localhost:3000/dashboard/fleet?tab=active', status: 307 })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { NextRequest } from 'next/server'
|
||||
|
||||
const SHARED_LANGUAGE_COOKIE = 'rentaldrivego-language'
|
||||
const MARKETPLACE_LANGUAGE_COOKIE = 'marketplace-language'
|
||||
const DASHBOARD_BASE_PATH = '/dashboard'
|
||||
|
||||
function rejectInternalSubrequest(request: NextRequest): NextResponse | null {
|
||||
if (!request.headers.has('x-middleware-subrequest')) return null
|
||||
@@ -22,10 +23,27 @@ function detectFromAcceptLanguage(header: string | null): 'en' | 'fr' | 'ar' {
|
||||
return 'en'
|
||||
}
|
||||
|
||||
function deduplicateDashboardPath(pathname: string): string | null {
|
||||
if (!pathname.startsWith(`${DASHBOARD_BASE_PATH}${DASHBOARD_BASE_PATH}`)) return null
|
||||
|
||||
let normalized = pathname
|
||||
while (normalized.startsWith(`${DASHBOARD_BASE_PATH}${DASHBOARD_BASE_PATH}`)) {
|
||||
normalized = normalized.slice(DASHBOARD_BASE_PATH.length)
|
||||
}
|
||||
return normalized || DASHBOARD_BASE_PATH
|
||||
}
|
||||
|
||||
export function proxy(request: NextRequest) {
|
||||
const rejected = rejectInternalSubrequest(request)
|
||||
if (rejected) return rejected
|
||||
|
||||
const deduped = deduplicateDashboardPath(request.nextUrl.pathname)
|
||||
if (deduped) {
|
||||
const url = request.nextUrl.clone()
|
||||
url.pathname = deduped
|
||||
return NextResponse.redirect(url, 307)
|
||||
}
|
||||
|
||||
const sharedLang = request.cookies.get(SHARED_LANGUAGE_COOKIE)?.value
|
||||
|
||||
if (isValidLanguage(sharedLang)) return NextResponse.next()
|
||||
@@ -53,6 +71,7 @@ export function proxy(request: NextRequest) {
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
'/((?!_next/static|_next/image|favicon\.ico|.*\.(?:png|ico|jpg|jpeg|svg|webp)$).*)',
|
||||
'/dashboard/dashboard/:path*',
|
||||
'/((?!_next/static|_next/image|_next/webpack-hmr|favicon\\.ico|.*\\.(?:png|ico|jpg|jpeg|svg|webp)$).*)',
|
||||
],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user