1- import express , { type Request , type Response , type NextFunction } from 'express' ;
2- import cors from 'cors' ;
3- import swaggerUi from 'swagger-ui-express' ;
4- import { swaggerSpec } from './config/swagger.js' ;
5- import { apiVersionMiddleware , type VersionedRequest } from './middleware/api-version.middleware.js' ;
6- import { sandboxMiddleware } from './middleware/sandbox.middleware.js' ;
7- import { globalRateLimiter } from './middleware/rate-limiter.middleware.js' ;
8- import { requestIdMiddleware } from './middleware/requestId.js' ;
9- import v1Routes from './routes/v1/index.js' ;
10-
11- import healthRoutes from './routes/health.routes.js' ;
1+ import express , {
2+ type Request ,
3+ type Response ,
4+ type NextFunction ,
5+ } from "express" ;
6+ import cors from "cors" ;
7+ import swaggerUi from "swagger-ui-express" ;
8+ import { swaggerSpec } from "./config/swagger.js" ;
9+ import {
10+ apiVersionMiddleware ,
11+ type VersionedRequest ,
12+ } from "./middleware/api-version.middleware.js" ;
13+ import { sandboxMiddleware } from "./middleware/sandbox.middleware.js" ;
14+ import { globalRateLimiter } from "./middleware/rate-limiter.middleware.js" ;
15+ import { requestIdMiddleware } from "./middleware/requestId.js" ;
16+ import v1Routes from "./routes/v1/index.js" ;
17+
18+ import healthRoutes from "./routes/health.routes.js" ;
1219
1320const app = express ( ) ;
14- const isProduction = process . env . NODE_ENV === ' production' ;
15- const rawCors = process . env . CORS_ALLOWED_ORIGINS ?? '' ;
21+ const isProduction = process . env . NODE_ENV === " production" ;
22+ const rawCors = process . env . CORS_ALLOWED_ORIGINS ?? "" ;
1623const allowedOrigins = rawCors
17- . split ( ',' )
18- . map ( ( origin ) => origin . trim ( ) )
19- . filter ( Boolean ) ;
24+ . split ( "," )
25+ . map ( ( origin ) => origin . trim ( ) )
26+ . filter ( Boolean ) ;
2027
2128// Default in development to only localhost:3000 (frontend dev server)
2229if ( ! process . env . CORS_ALLOWED_ORIGINS && ! isProduction ) {
23- allowedOrigins . push ( ' http://localhost:3000' ) ;
30+ allowedOrigins . push ( " http://localhost:3000" ) ;
2431}
2532
2633// Apply global rate limiter first
@@ -29,52 +36,60 @@ app.use(globalRateLimiter);
2936// Request ID tracing
3037app . use ( requestIdMiddleware ) ;
3138
32- app . disable ( ' x-powered-by' ) ;
39+ app . disable ( " x-powered-by" ) ;
3340
3441// Helmet-equivalent core headers without external dependency.
3542// Strict CSP applied globally; the /api-docs route overrides it below for Swagger UI.
3643app . use ( ( req : Request , res : Response , next : NextFunction ) => {
37- res . setHeader ( 'X-Content-Type-Options' , 'nosniff' ) ;
38- res . setHeader ( 'X-Frame-Options' , 'DENY' ) ;
39- res . setHeader ( 'Referrer-Policy' , 'no-referrer' ) ;
40- res . setHeader ( 'X-DNS-Prefetch-Control' , 'off' ) ;
41- res . setHeader ( 'X-Download-Options' , 'noopen' ) ;
42- res . setHeader ( 'X-Permitted-Cross-Domain-Policies' , 'none' ) ;
43- res . setHeader ( 'Content-Security-Policy' , "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; frame-ancestors 'none'; object-src 'none'" ) ;
44- res . setHeader ( 'Cross-Origin-Opener-Policy' , 'same-origin' ) ;
45- res . setHeader ( 'Cross-Origin-Resource-Policy' , 'same-origin' ) ;
46- if ( process . env . NODE_ENV === 'production' ) {
47- res . setHeader ( 'Strict-Transport-Security' , 'max-age=31536000; includeSubDomains' ) ;
48- }
49- next ( ) ;
44+ res . setHeader ( "X-Content-Type-Options" , "nosniff" ) ;
45+ res . setHeader ( "X-Frame-Options" , "DENY" ) ;
46+ res . setHeader ( "Referrer-Policy" , "no-referrer" ) ;
47+ res . setHeader ( "X-DNS-Prefetch-Control" , "off" ) ;
48+ res . setHeader ( "X-Download-Options" , "noopen" ) ;
49+ res . setHeader ( "X-Permitted-Cross-Domain-Policies" , "none" ) ;
50+ res . setHeader (
51+ "Content-Security-Policy" ,
52+ "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; frame-ancestors 'none'; object-src 'none'" ,
53+ ) ;
54+ res . setHeader ( "Cross-Origin-Opener-Policy" , "same-origin" ) ;
55+ res . setHeader ( "Cross-Origin-Resource-Policy" , "same-origin" ) ;
56+ if ( process . env . NODE_ENV === "production" ) {
57+ res . setHeader (
58+ "Strict-Transport-Security" ,
59+ "max-age=31536000; includeSubDomains" ,
60+ ) ;
61+ }
62+ next ( ) ;
5063} ) ;
5164
52- app . use ( cors ( {
65+ app . use (
66+ cors ( {
5367 origin ( origin , callback ) {
54- // Allow non-browser clients (no Origin header)
55- if ( ! origin ) {
56- callback ( null , true ) ;
57- return ;
58- }
59-
60- if ( allowedOrigins . includes ( origin ) ) {
61- callback ( null , true ) ;
62- return ;
63- }
64-
65- // Not allowed
66- callback ( new Error ( ' CORS origin not allowed' ) ) ;
68+ // Allow non-browser clients (no Origin header)
69+ if ( ! origin ) {
70+ callback ( null , true ) ;
71+ return ;
72+ }
73+
74+ if ( allowedOrigins . includes ( origin ) ) {
75+ callback ( null , true ) ;
76+ return ;
77+ }
78+
79+ // Not allowed
80+ callback ( new Error ( " CORS origin not allowed" ) ) ;
6781 } ,
6882 credentials : true ,
69- } ) ) ;
83+ } ) ,
84+ ) ;
7085
7186// Convert CORS errors into 403 responses so callers get a clear status code
7287app . use ( ( err : unknown , req : Request , res : Response , next : NextFunction ) => {
73- if ( err instanceof Error && err . message === ' CORS origin not allowed' ) {
74- res . status ( 403 ) . json ( { error : ' CORS origin not allowed' } ) ;
75- return ;
76- }
77- next ( err ) ;
88+ if ( err instanceof Error && err . message === " CORS origin not allowed" ) {
89+ res . status ( 403 ) . json ( { error : " CORS origin not allowed" } ) ;
90+ return ;
91+ }
92+ next ( err ) ;
7893} ) ;
7994app . use ( express . json ( ) ) ;
8095
@@ -83,38 +98,76 @@ app.use(sandboxMiddleware);
8398
8499// Swagger UI setup
85100// Override CSP for /api-docs only: Swagger UI requires inline scripts/styles.
86- app . use ( '/api-docs' , ( req : Request , res : Response , next : NextFunction ) => {
87- res . setHeader ( 'Content-Security-Policy' , "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none'; object-src 'none'" ) ;
101+ app . use (
102+ "/api-docs" ,
103+ ( req : Request , res : Response , next : NextFunction ) => {
104+ res . setHeader (
105+ "Content-Security-Policy" ,
106+ "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none'; object-src 'none'" ,
107+ ) ;
88108 next ( ) ;
89- } , swaggerUi . serve , swaggerUi . setup ( swaggerSpec , {
90- customCss : '.swagger-ui .topbar { display: none }' ,
91- customSiteTitle : 'FlowFi API Documentation' ,
92- } ) ) ;
109+ } ,
110+ swaggerUi . serve ,
111+ swaggerUi . setup ( swaggerSpec , {
112+ customCss : ".swagger-ui .topbar { display: none }" ,
113+ customSiteTitle : "FlowFi API Documentation" ,
114+ } ) ,
115+ ) ;
93116
94117// Serve raw OpenAPI spec as JSON
95- app . get ( ' /api-docs.json' , ( req : Request , res : Response ) => {
96- res . setHeader ( ' Content-Type' , ' application/json' ) ;
97- res . send ( swaggerSpec ) ;
118+ app . get ( " /api-docs.json" , ( req : Request , res : Response ) => {
119+ res . setHeader ( " Content-Type" , " application/json" ) ;
120+ res . send ( swaggerSpec ) ;
98121} ) ;
99122
100123// API Versioning
101124// All versioned routes must include version prefix (e.g., /v1/streams)
102125app . use ( apiVersionMiddleware ) ;
103126
127+ // Legacy (pre-versioning) routes: respond with 410 Gone and a migration hint
128+ // instead of falling through to a bare 404. Only the specific legacy paths
129+ // that used to exist before /v1 are covered here.
130+ const DEPRECATED_ROUTES : Record < string , string > = {
131+ "/streams" : "/v1/streams" ,
132+ "/events" : "/v1/events" ,
133+ } ;
134+
135+ app . use ( ( req : Request , res : Response , next : NextFunction ) => {
136+ const versionedReq = req as VersionedRequest ;
137+ if ( versionedReq . apiVersion ) {
138+ // Already versioned; nothing to do here.
139+ return next ( ) ;
140+ }
141+
142+ const newPath = DEPRECATED_ROUTES [ req . path ] ;
143+ if ( newPath ) {
144+ res . status ( 410 ) . json ( {
145+ deprecated : true ,
146+ migration : {
147+ old : req . path ,
148+ new : newPath ,
149+ } ,
150+ } ) ;
151+ return ;
152+ }
153+
154+ return next ( ) ;
155+ } ) ;
156+
104157// Versioned API routes
105158// After versioning middleware, /v1/streams becomes /streams, so we mount v1Routes at root
106159// But only handle requests that had a version prefix (apiVersion is set)
107160app . use ( ( req : Request , res : Response , next : NextFunction ) => {
108- const versionedReq = req as VersionedRequest ;
109- if ( versionedReq . apiVersion ) {
110- // This was a versioned request, route to v1 handlers
111- return v1Routes ( req , res , next ) ;
112- }
113- return next ( ) ; // Not versioned, continue to deprecated handlers
161+ const versionedReq = req as VersionedRequest ;
162+ if ( versionedReq . apiVersion ) {
163+ // This was a versioned request, route to v1 handlers
164+ return v1Routes ( req , res , next ) ;
165+ }
166+ return next ( ) ; // Not versioned, continue to deprecated handlers
114167} ) ;
115168
116169// Health check routes
117- app . use ( ' /health' , healthRoutes ) ;
170+ app . use ( " /health" , healthRoutes ) ;
118171
119172/**
120173 * @openapi
@@ -128,11 +181,11 @@ app.use('/health', healthRoutes);
128181 * 200:
129182 * description: API is running successfully
130183 */
131- app . get ( '/' , ( req : Request , res : Response ) => {
132- res . send ( ' FlowFi Backend is running' ) ;
184+ app . get ( "/" , ( req : Request , res : Response ) => {
185+ res . send ( " FlowFi Backend is running" ) ;
133186} ) ;
134187
135- import { errorHandler } from ' ./middleware/error.middleware.js' ;
188+ import { errorHandler } from " ./middleware/error.middleware.js" ;
136189
137190app . use ( errorHandler ) ;
138191
0 commit comments