From 4a6df3dcba07be55576ab011a44809a9e866bcf0 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 12 Aug 2026 17:21:28 -0400 Subject: [PATCH] fix cicd issues --- apps/admin/src/{middleware.ts => proxy.ts} | 2 +- apps/api/src/middleware/rateLimiter.ts | 19 ++++---- .../api/src/middleware/redisRateLimitStore.ts | 2 - apps/dashboard/README.md | 8 ++-- .../src/{middleware.test.ts => proxy.test.ts} | 46 +++++++++---------- .../dashboard/src/{middleware.ts => proxy.ts} | 6 +-- 6 files changed, 41 insertions(+), 42 deletions(-) rename apps/admin/src/{middleware.ts => proxy.ts} (89%) rename apps/dashboard/src/{middleware.test.ts => proxy.test.ts} (68%) rename apps/dashboard/src/{middleware.ts => proxy.ts} (96%) diff --git a/apps/admin/src/middleware.ts b/apps/admin/src/proxy.ts similarity index 89% rename from apps/admin/src/middleware.ts rename to apps/admin/src/proxy.ts index 68e9b95..f10ba03 100644 --- a/apps/admin/src/middleware.ts +++ b/apps/admin/src/proxy.ts @@ -1,7 +1,7 @@ import { NextResponse } from 'next/server' import type { NextRequest } from 'next/server' -export function middleware(request: NextRequest) { +export function proxy(request: NextRequest) { if (request.headers.has('x-middleware-subrequest')) { return new NextResponse('Unsupported internal request header', { status: 400 }) } diff --git a/apps/api/src/middleware/rateLimiter.ts b/apps/api/src/middleware/rateLimiter.ts index 0b5f638..96551da 100644 --- a/apps/api/src/middleware/rateLimiter.ts +++ b/apps/api/src/middleware/rateLimiter.ts @@ -2,7 +2,7 @@ import rateLimit, { ipKeyGenerator } from 'express-rate-limit' import type { Request } from 'express' import { verifyAnyActorToken } from '../security/tokens' import { getSessionCookieName } from '../security/sessionCookies' -import { sharedRateLimitStore } from './redisRateLimitStore' +import { RedisRateLimitStore } from './redisRateLimitStore' const SESSION_COOKIE_NAMES = [ getSessionCookieName('admin'), @@ -53,14 +53,15 @@ function getAuthenticatedActorKey(req: Request): string | null { const getClientIpKey = (req: Request) => ipKeyGenerator(req.ip ?? '') const skipPreflightRequest = (req: Request) => req.method === 'OPTIONS' -function withStore(options: Parameters[0]) { +/** express-rate-limit forbids reusing one Store across limiters — unique prefix per limiter. */ +function withStore(prefix: string, options: Parameters[0]) { return rateLimit({ ...options, - store: sharedRateLimitStore, + store: new RedisRateLimitStore(prefix), }) } -export const authLimiter = withStore({ +export const authLimiter = withStore('rl:auth:', { windowMs: 15 * 60 * 1000, max: 20, standardHeaders: 'draft-7', @@ -71,7 +72,7 @@ export const authLimiter = withStore({ message: { error: 'too_many_requests', message: 'Too many attempts, please try again later', statusCode: 429 }, }) -export const apiLimiter = withStore({ +export const apiLimiter = withStore('rl:api:', { windowMs: 60 * 1000, max: 300, standardHeaders: 'draft-7', @@ -87,7 +88,7 @@ export const apiLimiter = withStore({ message: { error: 'too_many_requests', message: 'Rate limit exceeded', statusCode: 429 }, }) -export const publicLimiter = withStore({ +export const publicLimiter = withStore('rl:public:', { windowMs: 60 * 1000, max: 60, standardHeaders: 'draft-7', @@ -97,7 +98,7 @@ export const publicLimiter = withStore({ message: { error: 'too_many_requests', message: 'Rate limit exceeded', statusCode: 429 }, }) -export const webhookLimiter = withStore({ +export const webhookLimiter = withStore('rl:webhook:', { windowMs: 60 * 1000, max: 30, standardHeaders: 'draft-7', @@ -107,7 +108,7 @@ export const webhookLimiter = withStore({ message: { error: 'too_many_requests', message: 'Webhook rate limit exceeded', statusCode: 429 }, }) -export const adminLimiter = withStore({ +export const adminLimiter = withStore('rl:admin:', { windowMs: 15 * 60 * 1000, max: 100, standardHeaders: 'draft-7', @@ -120,7 +121,7 @@ export const adminLimiter = withStore({ message: { error: 'too_many_requests', message: 'Too many admin requests', statusCode: 429 }, }) -export const actorLimiter = withStore({ +export const actorLimiter = withStore('rl:actor:', { windowMs: 60 * 1000, max: 240, standardHeaders: 'draft-7', diff --git a/apps/api/src/middleware/redisRateLimitStore.ts b/apps/api/src/middleware/redisRateLimitStore.ts index 48b485b..4c2a263 100644 --- a/apps/api/src/middleware/redisRateLimitStore.ts +++ b/apps/api/src/middleware/redisRateLimitStore.ts @@ -76,5 +76,3 @@ export class RedisRateLimitStore implements Store { await redis.del(`${this.prefix}${key}`) } } - -export const sharedRateLimitStore = new RedisRateLimitStore('rl:api:') diff --git a/apps/dashboard/README.md b/apps/dashboard/README.md index a95df6c..4bc11e4 100644 --- a/apps/dashboard/README.md +++ b/apps/dashboard/README.md @@ -8,7 +8,7 @@ Source of truth: - `apps/dashboard/src/components/*` - `apps/dashboard/src/lib/*` - `apps/dashboard/src/hooks/*` -- `apps/dashboard/src/middleware.ts` +- `apps/dashboard/src/proxy.ts` - `apps/dashboard/next.config.js` ## Purpose @@ -93,7 +93,7 @@ apps/dashboard/ preferences.ts language/theme persistence scoped per employee dashboardPaths.ts normalizes basePath-aware routes urls.ts resolves Carplace/admin app links - src/middleware.ts auth gate and redirect logic + src/proxy.ts auth gate and redirect logic (Next.js 16 proxy convention) ``` ## Rendering Strategy @@ -136,7 +136,7 @@ The sign-in page also supports: ### Protected-route middleware -`src/middleware.ts` protects the app. +`src/proxy.ts` protects the app. Important behavior: @@ -691,7 +691,7 @@ Tradeoffs of this approach: When changing the dashboard, keep these rules in mind: 1. Preserve the `/dashboard` base path and `toDashboardAppPath` normalization behavior. -2. Keep route protection aligned with `src/middleware.ts`. +2. Keep route protection aligned with `src/proxy.ts`. 3. If a new page depends on employee role, gate it both in navigation and in-page behavior. 4. Reuse `apiFetch` for auth and error handling consistency. 5. Keep language/theme strings inside the dashboard i18n system rather than scattering literals. diff --git a/apps/dashboard/src/middleware.test.ts b/apps/dashboard/src/proxy.test.ts similarity index 68% rename from apps/dashboard/src/middleware.test.ts rename to apps/dashboard/src/proxy.test.ts index b365783..4985bb2 100644 --- a/apps/dashboard/src/middleware.test.ts +++ b/apps/dashboard/src/proxy.test.ts @@ -37,10 +37,10 @@ function request(input: string, options: { token?: string; headers?: Record { @@ -53,19 +53,19 @@ afterEach(() => { vi.resetModules() }) -describe('dashboard middleware', () => { +describe('dashboard proxy', () => { it('redirects duplicate dashboard prefixes to the clean dashboard URL', async () => { - const { default: middleware } = await loadMiddleware() + const { default: proxy } = await loadProxy() - const response = middleware(request('https://workspace.example.com/dashboard/dashboard?x=1') as never) + const response = proxy(request('https://workspace.example.com/dashboard/dashboard?x=1') as never) expect(response).toEqual({ kind: 'redirect', url: 'https://workspace.example.com/dashboard?x=1' }) }) it('rejects middleware subrequest headers at the app layer', async () => { - const { default: middleware } = await loadMiddleware() + const { default: proxy } = await loadProxy() - const response = middleware(request('https://workspace.example.com/dashboard', { + const response = proxy(request('https://workspace.example.com/dashboard', { headers: { 'x-middleware-subrequest': 'middleware:middleware' }, }) as never) @@ -73,25 +73,25 @@ describe('dashboard middleware', () => { }) it('redirects unauthenticated protected internal-host requests to homepage sign-in on the website origin', async () => { - const { default: middleware } = await loadMiddleware('https://rentaldrivego.example') + const { default: proxy } = await loadProxy('https://rentaldrivego.example') - const response = middleware(request('http://dashboard:3001/dashboard/team') as never) + const response = proxy(request('http://dashboard:3001/dashboard/team') as never) expect(response).toEqual({ kind: 'redirect', url: 'https://rentaldrivego.example/en/dark/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 { default: proxy } = await loadProxy('https://rentaldrivego.example') - const response = middleware(request('http://dashboard:3001/reservations') as never) + const response = proxy(request('http://dashboard:3001/reservations') as never) expect(response).toEqual({ kind: 'redirect', url: 'https://rentaldrivego.example/en/dark/sign-in?redirect=%2Fdashboard%2Freservations' }) }) it('ignores spoofed forwarded host/proto when building the dashboard sign-in redirect', async () => { - const { default: middleware } = await loadMiddleware('https://market.example.com') + const { default: proxy } = await loadProxy('https://market.example.com') - const response = middleware(request('http://dashboard:3001/dashboard/billing', { + const response = proxy(request('http://dashboard:3001/dashboard/billing', { headers: { 'x-forwarded-host': 'workspace.customer.example', 'x-forwarded-proto': 'https', @@ -102,9 +102,9 @@ describe('dashboard middleware', () => { }) it('ignores internal forwarded hosts when building the dashboard sign-in redirect', async () => { - const { default: middleware } = await loadMiddleware('https://market.example.com') + const { default: proxy } = await loadProxy('https://market.example.com') - const response = middleware(request('http://dashboard:3001/dashboard/fleet', { + const response = proxy(request('http://dashboard:3001/dashboard/fleet', { headers: { 'x-forwarded-host': 'api:4000', 'x-forwarded-proto': 'https', @@ -115,34 +115,34 @@ describe('dashboard middleware', () => { }) it('redirects legacy dashboard sign-in requests to the localized homepage sign-in route', async () => { - const { default: middleware } = await loadMiddleware('https://market.example.com') + const { default: proxy } = await loadProxy('https://market.example.com') - const response = middleware(request('https://workspace.example.com/dashboard/sign-in?redirect=/dashboard/fleet&lang=fr&theme=dark') as never) + const response = proxy(request('https://workspace.example.com/dashboard/sign-in?redirect=/dashboard/fleet&lang=fr&theme=dark') as never) expect(response).toEqual({ kind: 'redirect', url: 'https://market.example.com/fr/dark/sign-in?redirect=%2Fdashboard%2Ffleet' }) }) it('redirects signed-in users away from the sign-in page', async () => { - const { default: middleware } = await loadMiddleware() + const { default: proxy } = await loadProxy() - const response = middleware(request('https://workspace.example.com/dashboard/sign-in?redirect=/dashboard/fleet', { token: 'employee-token' }) as never) + const response = proxy(request('https://workspace.example.com/dashboard/sign-in?redirect=/dashboard/fleet', { token: 'employee-token' }) as never) expect(response).toEqual({ kind: 'redirect', url: 'https://market.example.com/dashboard' }) }) it('allows public dashboard auth pages without a token', async () => { - const { default: middleware } = await loadMiddleware() + const { default: proxy } = await loadProxy() - const response = middleware(request('https://workspace.example.com/dashboard/forgot-password') as never) + const response = proxy(request('https://workspace.example.com/dashboard/forgot-password') as never) expect(response).toEqual({ kind: 'next' }) expect(nextServer.next).toHaveBeenCalledTimes(1) }) it('allows dashboard API requests through so auth endpoints can set and read cookies', async () => { - const { default: middleware } = await loadMiddleware() + const { default: proxy } = await loadProxy() - const response = middleware(request('https://workspace.example.com/dashboard/api/v1/auth/employee/login') as never) + const response = proxy(request('https://workspace.example.com/dashboard/api/v1/auth/employee/login') as never) expect(response).toEqual({ kind: 'next' }) expect(nextServer.redirect).not.toHaveBeenCalled() diff --git a/apps/dashboard/src/middleware.ts b/apps/dashboard/src/proxy.ts similarity index 96% rename from apps/dashboard/src/middleware.ts rename to apps/dashboard/src/proxy.ts index 6a5c3ff..6d038da 100644 --- a/apps/dashboard/src/middleware.ts +++ b/apps/dashboard/src/proxy.ts @@ -88,7 +88,7 @@ function isProtectedRoute(req: NextRequest) { return pathname === '/' || (pathname.startsWith('/') && !pathname.startsWith('/_next')) } -function localJwtMiddleware(req: NextRequest): NextResponse { +function localJwtProxy(req: NextRequest): NextResponse { const token = req.cookies.get('employee_session')?.value const pathname = toDashboardAppPath(req.nextUrl.pathname) @@ -109,7 +109,7 @@ function localJwtMiddleware(req: NextRequest): NextResponse { return NextResponse.next() } -export default function middleware(req: NextRequest) { +export default function proxy(req: NextRequest) { if (req.headers.has('x-middleware-subrequest')) { return new NextResponse('Unsupported internal request header', { status: 400 }) } @@ -121,7 +121,7 @@ export default function middleware(req: NextRequest) { return NextResponse.redirect(redirectUrl) } - return localJwtMiddleware(req) + return localJwtProxy(req) } export const config = {