diff --git a/apps/dashboard/src/components/layout/DashboardAccessGuard.boundary.test.ts b/apps/dashboard/src/components/layout/DashboardAccessGuard.boundary.test.ts index c6f2104..661f73a 100644 --- a/apps/dashboard/src/components/layout/DashboardAccessGuard.boundary.test.ts +++ b/apps/dashboard/src/components/layout/DashboardAccessGuard.boundary.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { resolveDashboardRoutePolicy, roleCanAccessPolicy } from '@/lib/dashboardRoutePolicies' -import { flattenInternalRoutes, getBaselineInternalRoutes, isAllowedRoute, resolveAccessRedirect, resolveAllowedRoutes } from './DashboardAccessGuard' +import { buildSignInRedirect, flattenInternalRoutes, getBaselineInternalRoutes, isAllowedRoute, resolveAccessRedirect, resolveAllowedRoutes } from './DashboardAccessGuard' describe('DashboardAccessGuard route helpers', () => { it('normalizes dashboard-prefixed menu routes before checking access', () => { @@ -108,6 +108,11 @@ describe('DashboardAccessGuard route helpers', () => { expect(resolveAccessRedirect('/fleet/123', ['/', '/fleet'])).toBeNull() }) + it('builds sign-in redirects with public dashboard return paths', () => { + expect(buildSignInRedirect('/reservations')).toBe('/sign-in?redirect=%2Fdashboard%2Freservations') + expect(buildSignInRedirect('/dashboard/fleet')).toBe('/sign-in?redirect=%2Fdashboard%2Ffleet') + }) + it('treats subscription as an owner-only recovery route independent of menu registration', () => { const policy = resolveDashboardRoutePolicy('/subscription') diff --git a/apps/dashboard/src/components/layout/DashboardAccessGuard.tsx b/apps/dashboard/src/components/layout/DashboardAccessGuard.tsx index 90206a4..9df18d2 100644 --- a/apps/dashboard/src/components/layout/DashboardAccessGuard.tsx +++ b/apps/dashboard/src/components/layout/DashboardAccessGuard.tsx @@ -3,7 +3,7 @@ import { usePathname, useRouter } from 'next/navigation' import { useEffect, useState } from 'react' import { apiFetch } from '@/lib/api' -import { toDashboardAppPath } from '@/lib/dashboardPaths' +import { toDashboardAppPath, toPublicDashboardPath } from '@/lib/dashboardPaths' import { getDashboardFallbackRoute, resolveDashboardRoutePolicy, @@ -90,9 +90,9 @@ export function resolveAccessRedirect(currentPath: string, allowedRoutes: string return fallbackRoute === currentPath ? null : fallbackRoute } -function buildSignInRedirect(currentPath: string) { +export function buildSignInRedirect(currentPath: string) { const params = new URLSearchParams() - params.set('redirect', currentPath) + params.set('redirect', toPublicDashboardPath(currentPath)) return `/sign-in?${params.toString()}` } diff --git a/apps/dashboard/src/middleware.test.ts b/apps/dashboard/src/middleware.test.ts index 55ec5b5..3373d1b 100644 --- a/apps/dashboard/src/middleware.test.ts +++ b/apps/dashboard/src/middleware.test.ts @@ -80,6 +80,14 @@ describe('dashboard middleware', () => { expect(response).toEqual({ kind: 'redirect', url: 'https://rentaldrivego.example/dashboard/sign-in?redirect=%2Fdashboard%2Fteam' }) }) + it('redirects unprefixed internal app paths with a public dashboard return path', async () => { + const { default: middleware } = await loadMiddleware('https://rentaldrivego.example') + + const response = middleware(request('http://dashboard:3001/reservations') as never) + + expect(response).toEqual({ kind: 'redirect', url: 'https://rentaldrivego.example/dashboard/sign-in?redirect=%2Fdashboard%2Freservations' }) + }) + it('uses trusted forwarded host/proto when building the dashboard sign-in redirect', async () => { const { default: middleware } = await loadMiddleware('https://market.example.com') diff --git a/apps/dashboard/src/middleware.ts b/apps/dashboard/src/middleware.ts index f94b156..6f0d6c1 100644 --- a/apps/dashboard/src/middleware.ts +++ b/apps/dashboard/src/middleware.ts @@ -86,7 +86,7 @@ function localJwtMiddleware(req: NextRequest): NextResponse { if (!token) { const signInUrl = resolveProxyUrl(req, toPublicDashboardPath('/sign-in')) - signInUrl.searchParams.set('redirect', req.nextUrl.pathname) + signInUrl.searchParams.set('redirect', toPublicDashboardPath(req.nextUrl.pathname)) return NextResponse.redirect(signInUrl) } return NextResponse.next()