From 35aaeb1bd725c3af74444ea8651bd60402bbe2ee Mon Sep 17 00:00:00 2001 From: Claudio Fuentes Date: Fri, 1 Aug 2025 13:19:15 -0400 Subject: [PATCH 1/3] feat: implement CORS and security headers for API routes - Added CORS headers to all API routes to handle cross-origin requests, including support for preflight OPTIONS requests. - Implemented security headers for all routes to enhance application security. - Updated middleware to include CORS handling for API routes, ensuring proper response headers are set. --- apps/app/next.config.ts | 52 +++++++++++++++ apps/app/src/app/api/health/route.ts | 39 +++++++---- apps/app/src/lib/cors.ts | 98 ++++++++++++++++++++++++++++ apps/app/src/middleware.ts | 25 ++++++- 4 files changed, 198 insertions(+), 16 deletions(-) create mode 100644 apps/app/src/lib/cors.ts diff --git a/apps/app/next.config.ts b/apps/app/next.config.ts index 088345ef9..f58c2e0c6 100644 --- a/apps/app/next.config.ts +++ b/apps/app/next.config.ts @@ -32,6 +32,58 @@ const config: NextConfig = { outputFileTracingIncludes: { '/api/**/*': ['./node_modules/.prisma/client/**/*'], }, + async headers() { + return [ + { + // Apply CORS headers to all API routes + source: '/api/:path*', + headers: [ + { + key: 'Access-Control-Allow-Origin', + value: '*', + }, + { + key: 'Access-Control-Allow-Methods', + value: 'GET, POST, PUT, DELETE, PATCH, OPTIONS', + }, + { + key: 'Access-Control-Allow-Headers', + value: 'Content-Type, Authorization, X-Requested-With, Accept, Origin, x-pathname', + }, + { + key: 'Access-Control-Allow-Credentials', + value: 'true', + }, + { + key: 'Access-Control-Max-Age', + value: '86400', // 24 hours + }, + ], + }, + { + // Apply security headers to all routes + source: '/(.*)', + headers: [ + { + key: 'X-DNS-Prefetch-Control', + value: 'on', + }, + { + key: 'X-Frame-Options', + value: 'SAMEORIGIN', + }, + { + key: 'X-Content-Type-Options', + value: 'nosniff', + }, + { + key: 'Referrer-Policy', + value: 'origin-when-cross-origin', + }, + ], + }, + ]; + }, async rewrites() { return [ { diff --git a/apps/app/src/app/api/health/route.ts b/apps/app/src/app/api/health/route.ts index 67eca7ec7..08e217d6d 100644 --- a/apps/app/src/app/api/health/route.ts +++ b/apps/app/src/app/api/health/route.ts @@ -1,33 +1,44 @@ +import { corsResponse, handleCorsPreflightRequest } from '@/lib/cors'; import { db } from '@db'; -import { NextResponse } from 'next/server'; +import { NextRequest } from 'next/server'; export const dynamic = 'force-dynamic'; -export async function GET() { +// Handle CORS preflight requests +export async function OPTIONS(request: NextRequest) { + return handleCorsPreflightRequest(request); +} + +export async function GET(request: NextRequest) { try { // Test database connection const userCount = await db.user.count(); - return NextResponse.json({ - status: 'ok', - database: 'connected', - userCount, - env: { - E2E_TEST_MODE: process.env.E2E_TEST_MODE, - NODE_ENV: process.env.NODE_ENV, - DATABASE_URL: process.env.DATABASE_URL ? 'set' : 'not set', - AUTH_SECRET: process.env.AUTH_SECRET ? 'set' : 'not set', - BETTER_AUTH_URL: process.env.BETTER_AUTH_URL || 'not set', + return corsResponse( + { + status: 'ok', + database: 'connected', + userCount, + env: { + E2E_TEST_MODE: process.env.E2E_TEST_MODE, + NODE_ENV: process.env.NODE_ENV, + DATABASE_URL: process.env.DATABASE_URL ? 'set' : 'not set', + AUTH_SECRET: process.env.AUTH_SECRET ? 'set' : 'not set', + BETTER_AUTH_URL: process.env.BETTER_AUTH_URL || 'not set', + }, }, - }); + { status: 200 }, + request, + ); } catch (error) { console.error('Health check failed:', error); - return NextResponse.json( + return corsResponse( { status: 'error', error: error instanceof Error ? error.message : 'Unknown error', }, { status: 500 }, + request, ); } } diff --git a/apps/app/src/lib/cors.ts b/apps/app/src/lib/cors.ts new file mode 100644 index 000000000..590e8d5a3 --- /dev/null +++ b/apps/app/src/lib/cors.ts @@ -0,0 +1,98 @@ +import { NextRequest, NextResponse } from 'next/server'; + +/** + * CORS configuration for API routes + */ +export const corsConfig = { + // Configure allowed origins based on environment + allowedOrigins: [ + // Development origins + 'http://localhost:3000', + 'http://localhost:3001', + 'http://127.0.0.1:3000', + 'http://127.0.0.1:3001', + // Production origins + 'https://portal.trycomp.ai', + 'https://app.trycomp.ai', + 'https://trycomp.ai', + // Staging origins + 'https://portal.staging.trycomp.ai', + 'https://app.staging.trycomp.ai', + ], + allowedMethods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'], + allowedHeaders: [ + 'Content-Type', + 'Authorization', + 'X-Requested-With', + 'Accept', + 'Origin', + 'x-pathname', + 'Cache-Control', + ], + allowCredentials: true, + maxAge: 86400, // 24 hours +}; + +/** + * Add CORS headers to a NextResponse + */ +export function addCorsHeaders(response: NextResponse, origin?: string | null): NextResponse { + // Check if origin is allowed + const isAllowedOrigin = + !origin || + corsConfig.allowedOrigins.includes(origin) || + corsConfig.allowedOrigins.includes('*'); + + if (isAllowedOrigin) { + response.headers.set('Access-Control-Allow-Origin', origin || '*'); + } + + response.headers.set('Access-Control-Allow-Methods', corsConfig.allowedMethods.join(', ')); + response.headers.set('Access-Control-Allow-Headers', corsConfig.allowedHeaders.join(', ')); + response.headers.set('Access-Control-Allow-Credentials', corsConfig.allowCredentials.toString()); + response.headers.set('Access-Control-Max-Age', corsConfig.maxAge.toString()); + + return response; +} + +/** + * Handle CORS preflight requests (OPTIONS) + */ +export function handleCorsPreflightRequest(request: NextRequest): NextResponse { + const origin = request.headers.get('origin'); + const response = new NextResponse(null, { status: 200 }); + + return addCorsHeaders(response, origin); +} + +/** + * Create a CORS-enabled response + */ +export function corsResponse( + data: any, + options: ResponseInit = {}, + request?: NextRequest, +): NextResponse { + const response = NextResponse.json(data, options); + const origin = request?.headers.get('origin'); + + return addCorsHeaders(response, origin); +} + +/** + * Higher-order function to wrap API handlers with CORS support + */ +export function withCors Promise>(handler: T): T { + return (async (...args: any[]) => { + const [request] = args; + const origin = request?.headers?.get?.('origin'); + + try { + const response = await handler(...args); + return addCorsHeaders(response, origin); + } catch (error) { + const errorResponse = NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); + return addCorsHeaders(errorResponse, origin); + } + }) as T; +} diff --git a/apps/app/src/middleware.ts b/apps/app/src/middleware.ts index d1a7adda0..8cbcd0c62 100644 --- a/apps/app/src/middleware.ts +++ b/apps/app/src/middleware.ts @@ -1,3 +1,4 @@ +import { addCorsHeaders } from '@/lib/cors'; import { auth } from '@/utils/auth'; import { db } from '@db'; import { headers } from 'next/headers'; @@ -6,8 +7,8 @@ import { NextRequest, NextResponse } from 'next/server'; export const config = { runtime: 'nodejs', matcher: [ - // Skip auth-related routes (removed onboarding from exclusions) - '/((?!api|_next/static|_next/image|favicon.ico|monitoring|ingest|research).*)', + // Include API routes for CORS handling, exclude static assets + '/((?!_next/static|_next/image|favicon.ico).*)', ], }; @@ -19,6 +20,17 @@ function isUnprotectedRoute(pathname: string): boolean { } export async function middleware(request: NextRequest) { + const { pathname } = request.nextUrl; + + // Handle CORS for API routes + if (pathname.startsWith('/api/')) { + // Handle preflight OPTIONS requests + if (request.method === 'OPTIONS') { + const response = new NextResponse(null, { status: 200 }); + return addCorsHeaders(response, request.headers.get('origin')); + } + } + // E2E Test Mode: Check for test auth header if (process.env.E2E_TEST_MODE === 'true') { const testAuthHeader = request.headers.get('x-e2e-test-auth'); @@ -29,6 +41,10 @@ export async function middleware(request: NextRequest) { // Allow the request to proceed without auth checks const response = NextResponse.next(); response.headers.set('x-pathname', request.nextUrl.pathname); + // Add CORS headers for API routes + if (pathname.startsWith('/api/')) { + return addCorsHeaders(response, request.headers.get('origin')); + } return response; } } catch (e) { @@ -177,5 +193,10 @@ export async function middleware(request: NextRequest) { } } + // Add CORS headers for API routes + if (pathname.startsWith('/api/')) { + return addCorsHeaders(response, request.headers.get('origin')); + } + return response; } From 2d183086b9f9e7c43bae08169371fba9e3076174 Mon Sep 17 00:00:00 2001 From: Claudio Fuentes Date: Fri, 1 Aug 2025 13:46:56 -0400 Subject: [PATCH 2/3] refactor: remove CORS handling from middleware and API routes - Eliminated CORS-related functions and headers from middleware and health check API route. - Updated middleware to skip auth-related routes in CORS handling. - Simplified health check response by using NextResponse directly instead of a custom CORS response function. - Removed the entire CORS utility module as it is no longer needed. --- apps/app/src/app/api/health/route.ts | 39 ++++------- apps/app/src/lib/cors.ts | 98 ---------------------------- apps/app/src/middleware.ts | 25 +------ 3 files changed, 16 insertions(+), 146 deletions(-) delete mode 100644 apps/app/src/lib/cors.ts diff --git a/apps/app/src/app/api/health/route.ts b/apps/app/src/app/api/health/route.ts index 08e217d6d..67eca7ec7 100644 --- a/apps/app/src/app/api/health/route.ts +++ b/apps/app/src/app/api/health/route.ts @@ -1,44 +1,33 @@ -import { corsResponse, handleCorsPreflightRequest } from '@/lib/cors'; import { db } from '@db'; -import { NextRequest } from 'next/server'; +import { NextResponse } from 'next/server'; export const dynamic = 'force-dynamic'; -// Handle CORS preflight requests -export async function OPTIONS(request: NextRequest) { - return handleCorsPreflightRequest(request); -} - -export async function GET(request: NextRequest) { +export async function GET() { try { // Test database connection const userCount = await db.user.count(); - return corsResponse( - { - status: 'ok', - database: 'connected', - userCount, - env: { - E2E_TEST_MODE: process.env.E2E_TEST_MODE, - NODE_ENV: process.env.NODE_ENV, - DATABASE_URL: process.env.DATABASE_URL ? 'set' : 'not set', - AUTH_SECRET: process.env.AUTH_SECRET ? 'set' : 'not set', - BETTER_AUTH_URL: process.env.BETTER_AUTH_URL || 'not set', - }, + return NextResponse.json({ + status: 'ok', + database: 'connected', + userCount, + env: { + E2E_TEST_MODE: process.env.E2E_TEST_MODE, + NODE_ENV: process.env.NODE_ENV, + DATABASE_URL: process.env.DATABASE_URL ? 'set' : 'not set', + AUTH_SECRET: process.env.AUTH_SECRET ? 'set' : 'not set', + BETTER_AUTH_URL: process.env.BETTER_AUTH_URL || 'not set', }, - { status: 200 }, - request, - ); + }); } catch (error) { console.error('Health check failed:', error); - return corsResponse( + return NextResponse.json( { status: 'error', error: error instanceof Error ? error.message : 'Unknown error', }, { status: 500 }, - request, ); } } diff --git a/apps/app/src/lib/cors.ts b/apps/app/src/lib/cors.ts deleted file mode 100644 index 590e8d5a3..000000000 --- a/apps/app/src/lib/cors.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; - -/** - * CORS configuration for API routes - */ -export const corsConfig = { - // Configure allowed origins based on environment - allowedOrigins: [ - // Development origins - 'http://localhost:3000', - 'http://localhost:3001', - 'http://127.0.0.1:3000', - 'http://127.0.0.1:3001', - // Production origins - 'https://portal.trycomp.ai', - 'https://app.trycomp.ai', - 'https://trycomp.ai', - // Staging origins - 'https://portal.staging.trycomp.ai', - 'https://app.staging.trycomp.ai', - ], - allowedMethods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'], - allowedHeaders: [ - 'Content-Type', - 'Authorization', - 'X-Requested-With', - 'Accept', - 'Origin', - 'x-pathname', - 'Cache-Control', - ], - allowCredentials: true, - maxAge: 86400, // 24 hours -}; - -/** - * Add CORS headers to a NextResponse - */ -export function addCorsHeaders(response: NextResponse, origin?: string | null): NextResponse { - // Check if origin is allowed - const isAllowedOrigin = - !origin || - corsConfig.allowedOrigins.includes(origin) || - corsConfig.allowedOrigins.includes('*'); - - if (isAllowedOrigin) { - response.headers.set('Access-Control-Allow-Origin', origin || '*'); - } - - response.headers.set('Access-Control-Allow-Methods', corsConfig.allowedMethods.join(', ')); - response.headers.set('Access-Control-Allow-Headers', corsConfig.allowedHeaders.join(', ')); - response.headers.set('Access-Control-Allow-Credentials', corsConfig.allowCredentials.toString()); - response.headers.set('Access-Control-Max-Age', corsConfig.maxAge.toString()); - - return response; -} - -/** - * Handle CORS preflight requests (OPTIONS) - */ -export function handleCorsPreflightRequest(request: NextRequest): NextResponse { - const origin = request.headers.get('origin'); - const response = new NextResponse(null, { status: 200 }); - - return addCorsHeaders(response, origin); -} - -/** - * Create a CORS-enabled response - */ -export function corsResponse( - data: any, - options: ResponseInit = {}, - request?: NextRequest, -): NextResponse { - const response = NextResponse.json(data, options); - const origin = request?.headers.get('origin'); - - return addCorsHeaders(response, origin); -} - -/** - * Higher-order function to wrap API handlers with CORS support - */ -export function withCors Promise>(handler: T): T { - return (async (...args: any[]) => { - const [request] = args; - const origin = request?.headers?.get?.('origin'); - - try { - const response = await handler(...args); - return addCorsHeaders(response, origin); - } catch (error) { - const errorResponse = NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); - return addCorsHeaders(errorResponse, origin); - } - }) as T; -} diff --git a/apps/app/src/middleware.ts b/apps/app/src/middleware.ts index 8cbcd0c62..d1a7adda0 100644 --- a/apps/app/src/middleware.ts +++ b/apps/app/src/middleware.ts @@ -1,4 +1,3 @@ -import { addCorsHeaders } from '@/lib/cors'; import { auth } from '@/utils/auth'; import { db } from '@db'; import { headers } from 'next/headers'; @@ -7,8 +6,8 @@ import { NextRequest, NextResponse } from 'next/server'; export const config = { runtime: 'nodejs', matcher: [ - // Include API routes for CORS handling, exclude static assets - '/((?!_next/static|_next/image|favicon.ico).*)', + // Skip auth-related routes (removed onboarding from exclusions) + '/((?!api|_next/static|_next/image|favicon.ico|monitoring|ingest|research).*)', ], }; @@ -20,17 +19,6 @@ function isUnprotectedRoute(pathname: string): boolean { } export async function middleware(request: NextRequest) { - const { pathname } = request.nextUrl; - - // Handle CORS for API routes - if (pathname.startsWith('/api/')) { - // Handle preflight OPTIONS requests - if (request.method === 'OPTIONS') { - const response = new NextResponse(null, { status: 200 }); - return addCorsHeaders(response, request.headers.get('origin')); - } - } - // E2E Test Mode: Check for test auth header if (process.env.E2E_TEST_MODE === 'true') { const testAuthHeader = request.headers.get('x-e2e-test-auth'); @@ -41,10 +29,6 @@ export async function middleware(request: NextRequest) { // Allow the request to proceed without auth checks const response = NextResponse.next(); response.headers.set('x-pathname', request.nextUrl.pathname); - // Add CORS headers for API routes - if (pathname.startsWith('/api/')) { - return addCorsHeaders(response, request.headers.get('origin')); - } return response; } } catch (e) { @@ -193,10 +177,5 @@ export async function middleware(request: NextRequest) { } } - // Add CORS headers for API routes - if (pathname.startsWith('/api/')) { - return addCorsHeaders(response, request.headers.get('origin')); - } - return response; } From 05193c1409d7cead8e71f378844a29dbb277eca9 Mon Sep 17 00:00:00 2001 From: Claudio Fuentes Date: Fri, 1 Aug 2025 14:10:51 -0400 Subject: [PATCH 3/3] fix: update Dockerfile to remove unnecessary bunfig.toml copy - Removed the copy command for bunfig.toml from the Dockerfile as it is no longer needed. - Adjusted the copy command to streamline the build process by only including essential files. --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 37789ba6a..19027bc71 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ FROM oven/bun:1.2.8 AS deps WORKDIR /app # Copy workspace configuration -COPY package.json bun.lock bunfig.toml ./ +COPY package.json bun.lock ./ # Copy package.json files for all packages COPY packages/db/package.json ./packages/db/