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

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:
root
2026-06-25 00:57:59 -04:00
parent 572d115003
commit 3b142ca4c7
36 changed files with 2987 additions and 1156 deletions
+23 -2
View File
@@ -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`,
+14
View File
@@ -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

+43 -1
View File
@@ -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 })
})
})
+20 -1
View File
@@ -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)$).*)',
],
}