From a665c942d2b67b554d1bdde3499ae7cd66f91904 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 29 Jul 2026 00:24:48 -0400 Subject: [PATCH] fix cors production --- apps/api/src/app.ts | 44 +++++++++++++++++-- apps/api/src/middleware/csrf.ts | 3 ++ .../src/tests/api/api-foundation.api.test.ts | 18 +++++++- 3 files changed, 61 insertions(+), 4 deletions(-) diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index 2f8eac1..507e434 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -55,9 +55,47 @@ const defaultCorsOrigins = [ 'http://127.0.0.1:4000', ] -export const corsOrigins = process.env.CORS_ORIGINS - ? process.env.CORS_ORIGINS.split(',').map((o) => o.trim()).filter(Boolean) - : defaultCorsOrigins +const frontendOriginEnvKeys = [ + 'SITE_ORIGIN', + 'WEBSITE_URL', + 'HOMEPAGE_URL', + 'DASHBOARD_URL', + 'ADMIN_URL', + 'CARPLACE_URL', + 'NEXT_PUBLIC_HOMEPAGE_URL', + 'NEXT_PUBLIC_WEBSITE_URL', + 'NEXT_PUBLIC_DASHBOARD_URL', + 'NEXT_PUBLIC_ADMIN_URL', + 'NEXT_PUBLIC_CARPLACE_URL', +] as const + +function normalizeConfiguredOrigin(value: string): string | null { + try { + return new URL(value).origin + } catch { + return null + } +} + +export function getConfiguredCorsOrigins(env: NodeJS.ProcessEnv = process.env) { + const configuredOrigins = (env.CORS_ORIGINS ?? '') + .split(',') + .map((origin) => origin.trim()) + .filter(Boolean) + + const frontendOrigins = frontendOriginEnvKeys + .flatMap((key) => (env[key] ?? '').split(',')) + .map((origin) => origin.trim()) + .filter(Boolean) + + const rawOrigins = configuredOrigins.length > 0 || frontendOrigins.length > 0 + ? [...configuredOrigins, ...frontendOrigins] + : defaultCorsOrigins + + return Array.from(new Set(rawOrigins.map(normalizeConfiguredOrigin).filter((origin): origin is string => Boolean(origin)))) +} + +export const corsOrigins = getConfiguredCorsOrigins() function isAllowedLocalDevOrigin(origin: string) { if (process.env.NODE_ENV === 'production') return false diff --git a/apps/api/src/middleware/csrf.ts b/apps/api/src/middleware/csrf.ts index c14d489..6aea362 100644 --- a/apps/api/src/middleware/csrf.ts +++ b/apps/api/src/middleware/csrf.ts @@ -5,10 +5,13 @@ const SESSION_COOKIE_PATTERN = /(?:^|;\s*)(?:admin_session|employee_session|rent function configuredOrigins() { return [ + process.env.SITE_ORIGIN, process.env.DASHBOARD_URL, process.env.ADMIN_URL, process.env.CARPLACE_URL, + process.env.HOMEPAGE_URL, process.env.WEBSITE_URL, + process.env.NEXT_PUBLIC_HOMEPAGE_URL, process.env.NEXT_PUBLIC_DASHBOARD_URL, process.env.NEXT_PUBLIC_ADMIN_URL, process.env.NEXT_PUBLIC_CARPLACE_URL, diff --git a/apps/api/src/tests/api/api-foundation.api.test.ts b/apps/api/src/tests/api/api-foundation.api.test.ts index 99a8622..dc489d6 100644 --- a/apps/api/src/tests/api/api-foundation.api.test.ts +++ b/apps/api/src/tests/api/api-foundation.api.test.ts @@ -16,7 +16,7 @@ vi.mock('../../lib/redis', () => ({ import request from 'supertest' import { describe, expect, it } from 'vitest' -import { createApp, isCorsOriginAllowed } from '../../app' +import { createApp, getConfiguredCorsOrigins, isCorsOriginAllowed } from '../../app' import { isTrustedBrowserOrigin } from '../../middleware/csrf' const app = createApp() @@ -88,6 +88,22 @@ describe('API foundation integration', () => { expect(res.headers['access-control-allow-credentials']).toBe('true') }) + it('derives production CORS origins from configured frontend URLs when CORS_ORIGINS is absent', () => { + expect(getConfiguredCorsOrigins({ + NODE_ENV: 'production', + SITE_ORIGIN: 'https://rentaldrivego.ma', + NEXT_PUBLIC_DASHBOARD_URL: 'https://rentaldrivego.ma/dashboard', + NEXT_PUBLIC_ADMIN_URL: 'https://rentaldrivego.ma/admin', + })).toEqual(['https://rentaldrivego.ma']) + }) + + it('normalizes explicit CORS origins and frontend URL origins without duplicates', () => { + expect(getConfiguredCorsOrigins({ + CORS_ORIGINS: 'https://rentaldrivego.ma, https://www.rentaldrivego.ma', + NEXT_PUBLIC_DASHBOARD_URL: 'https://rentaldrivego.ma/dashboard', + })).toEqual(['https://rentaldrivego.ma', 'https://www.rentaldrivego.ma']) + }) + it('trusts private LAN app origins during local development only on known app ports', () => { expect(isCorsOriginAllowed('http://192.168.3.3:3000')).toBe(true) expect(isTrustedBrowserOrigin('http://192.168.3.3:3000')).toBe(true)