Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
cd1e779
Add stream POST negative tests
Emelie-Dev Jun 30, 2026
3fbf6ee
Fixed issues
Emelie-Dev Jun 30, 2026
1a7aa38
Merge branch 'main' into test/815-stream-post-negative-cases
Emelie-Dev Jun 30, 2026
162529b
Fixed issues
Emelie-Dev Jun 30, 2026
73969b6
Fixed issues
Emelie-Dev Jun 30, 2026
e148673
Fixed issues
Emelie-Dev Jun 30, 2026
75f1ceb
Fixed issues
Emelie-Dev Jun 30, 2026
4b81cfc
Fixed issues
Emelie-Dev Jul 1, 2026
5b301fb
Add stream POST negative tests
Emelie-Dev Jun 30, 2026
0cd1750
Fixed issues
Emelie-Dev Jun 30, 2026
89bd454
Fixed issues
Emelie-Dev Jun 30, 2026
5d76c89
Fixed issues
Emelie-Dev Jun 30, 2026
8c264f7
Fixed issues
Emelie-Dev Jun 30, 2026
9998661
Merge branch 'test/815-stream-post-negative-cases' of https://github.…
Emelie-Dev Jul 1, 2026
ccd1a05
Fixed Issues
Emelie-Dev Jul 1, 2026
09846c4
Add stream POST negative tests
Emelie-Dev Jun 30, 2026
00765e2
Fixed issues
Emelie-Dev Jun 30, 2026
97c9517
Fixed issues
Emelie-Dev Jun 30, 2026
aa93df0
Fixed issues
Emelie-Dev Jun 30, 2026
1113401
Fixed issues
Emelie-Dev Jun 30, 2026
841b885
Fixed issues
Emelie-Dev Jun 30, 2026
820c929
Fixed issues
Emelie-Dev Jun 30, 2026
e984744
Fixed Issues
Emelie-Dev Jul 1, 2026
cad80d7
Merge branch 'test/815-stream-post-negative-cases' of https://github.…
Emelie-Dev Jul 1, 2026
de63644
Fixed issues
Emelie-Dev Jul 1, 2026
04fd4f1
Merge branch 'main' into test/815-stream-post-negative-cases
Emelie-Dev Jul 4, 2026
a89b8e3
Fixed issues
Emelie-Dev Jul 4, 2026
a984266
Fixed issues
Emelie-Dev Jul 4, 2026
1766961
Fixed issues
Emelie-Dev Jul 4, 2026
15a5581
Fixed issues
Emelie-Dev Jul 4, 2026
1abe705
Merge branch 'main' into test/815-stream-post-negative-cases
Emelie-Dev Jul 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ COPY package*.json ./
RUN npm install

COPY tsconfig.json ./
COPY prisma.config.ts ./
COPY src ./src
COPY prisma ./prisma

Expand All @@ -23,10 +24,6 @@ RUN npm install --omit=dev
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/src/generated ./dist/generated
COPY --from=builder /app/prisma ./prisma
# Prisma 7 reads the schema location and datasource url from prisma.config.ts,
# and the schema's `datasource db {}` block has no inline url. The CI health
# check runs `prisma db push` inside this image, so the config must be present
# too (dotenv is a runtime dependency, so the config loads).
COPY prisma.config.ts ./

EXPOSE 3001
Expand Down
165 changes: 94 additions & 71 deletions backend/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,33 @@
import express, { type Request, type Response, type NextFunction } from 'express';
import cors from 'cors';
import swaggerUi from 'swagger-ui-express';
import { swaggerSpec } from './config/swagger.js';
import { apiVersionMiddleware, type VersionedRequest } from './middleware/api-version.middleware.js';
import { sandboxMiddleware } from './middleware/sandbox.middleware.js';
import { globalRateLimiter } from './middleware/rate-limiter.middleware.js';
import { requestIdMiddleware } from './middleware/requestId.js';
import v1Routes from './routes/v1/index.js';

import healthRoutes from './routes/health.routes.js';
import express, {
type Request,
type Response,
type NextFunction,
} from "express";
import cors from "cors";
import swaggerUi from "swagger-ui-express";
import { swaggerSpec } from "./config/swagger.js";
import {
apiVersionMiddleware,
type VersionedRequest,
} from "./middleware/api-version.middleware.js";
import { sandboxMiddleware } from "./middleware/sandbox.middleware.js";
import { globalRateLimiter } from "./middleware/rate-limiter.middleware.js";
import { requestIdMiddleware } from "./middleware/requestId.js";
import v1Routes from "./routes/v1/index.js";

import healthRoutes from "./routes/health.routes.js";

const app = express();
const isProduction = process.env.NODE_ENV === 'production';
const rawCors = process.env.CORS_ALLOWED_ORIGINS ?? '';
const isProduction = process.env.NODE_ENV === "production";
const rawCors = process.env.CORS_ALLOWED_ORIGINS ?? "";
const allowedOrigins = rawCors
.split(',')
.map((origin) => origin.trim())
.filter(Boolean);
.split(",")
.map((origin) => origin.trim())
.filter(Boolean);

// Default in development to only localhost:3000 (frontend dev server)
if (!process.env.CORS_ALLOWED_ORIGINS && !isProduction) {
allowedOrigins.push('http://localhost:3000');
allowedOrigins.push("http://localhost:3000");
}

// Apply global rate limiter first
Expand All @@ -29,72 +36,88 @@ app.use(globalRateLimiter);
// Request ID tracing
app.use(requestIdMiddleware);

app.disable('x-powered-by');
app.disable("x-powered-by");

// Helmet-equivalent core headers without external dependency.
// Strict CSP applied globally; the /api-docs route overrides it below for Swagger UI.
app.use((req: Request, res: Response, next: NextFunction) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('Referrer-Policy', 'no-referrer');
res.setHeader('X-DNS-Prefetch-Control', 'off');
res.setHeader('X-Download-Options', 'noopen');
res.setHeader('X-Permitted-Cross-Domain-Policies', 'none');
res.setHeader('Content-Security-Policy', "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; frame-ancestors 'none'; object-src 'none'");
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
res.setHeader('Cross-Origin-Resource-Policy', 'same-origin');
if (process.env.NODE_ENV === 'production') {
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
}
next();
res.setHeader("X-Content-Type-Options", "nosniff");
res.setHeader("X-Frame-Options", "DENY");
res.setHeader("Referrer-Policy", "no-referrer");
res.setHeader("X-DNS-Prefetch-Control", "off");
res.setHeader("X-Download-Options", "noopen");
res.setHeader("X-Permitted-Cross-Domain-Policies", "none");
res.setHeader(
"Content-Security-Policy",
"default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; frame-ancestors 'none'; object-src 'none'",
);
res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
res.setHeader("Cross-Origin-Resource-Policy", "same-origin");
if (process.env.NODE_ENV === "production") {
res.setHeader(
"Strict-Transport-Security",
"max-age=31536000; includeSubDomains",
);
}
next();
});

app.use(cors({
app.use(
cors({
origin(origin, callback) {
// Allow non-browser clients (no Origin header)
if (!origin) {
callback(null, true);
return;
}

if (allowedOrigins.includes(origin)) {
callback(null, true);
return;
}

// Not allowed
callback(new Error('CORS origin not allowed'));
// Allow non-browser clients (no Origin header)
if (!origin) {
callback(null, true);
return;
}

if (allowedOrigins.includes(origin)) {
callback(null, true);
return;
}

// Not allowed
callback(new Error("CORS origin not allowed"));
},
credentials: true,
}));
}),
);

// Convert CORS errors into 403 responses so callers get a clear status code
app.use((err: unknown, req: Request, res: Response, next: NextFunction) => {
if (err instanceof Error && err.message === 'CORS origin not allowed') {
res.status(403).json({ error: 'CORS origin not allowed' });
return;
}
next(err);
if (err instanceof Error && err.message === "CORS origin not allowed") {
res.status(403).json({ error: "CORS origin not allowed" });
return;
}
next(err);
});
app.use(express.json({ limit: '1mb' }));
app.use(express.json({ limit: "1mb" }));

// Sandbox mode detection (before versioning)
app.use(sandboxMiddleware);

// Swagger UI setup
// Override CSP for /api-docs only: Swagger UI requires inline scripts/styles.
app.use('/api-docs', (req: Request, res: Response, next: NextFunction) => {
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'");
app.use(
"/api-docs",
(req: Request, res: Response, next: NextFunction) => {
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'",
);
next();
}, swaggerUi.serve, swaggerUi.setup(swaggerSpec, {
customCss: '.swagger-ui .topbar { display: none }',
customSiteTitle: 'FlowFi API Documentation',
}));
},
swaggerUi.serve,
swaggerUi.setup(swaggerSpec, {
customCss: ".swagger-ui .topbar { display: none }",
customSiteTitle: "FlowFi API Documentation",
}),
);

// Serve raw OpenAPI spec as JSON
app.get('/api-docs.json', (req: Request, res: Response) => {
res.setHeader('Content-Type', 'application/json');
res.send(swaggerSpec);
app.get("/api-docs.json", (req: Request, res: Response) => {
res.setHeader("Content-Type", "application/json");
res.send(swaggerSpec);
});

// API Versioning
Expand All @@ -105,16 +128,16 @@ app.use(apiVersionMiddleware);
// After versioning middleware, /v1/streams becomes /streams, so we mount v1Routes at root
// But only handle requests that had a version prefix (apiVersion is set)
app.use((req: Request, res: Response, next: NextFunction) => {
const versionedReq = req as VersionedRequest;
if (versionedReq.apiVersion) {
// This was a versioned request, route to v1 handlers
return v1Routes(req, res, next);
}
return next(); // Not versioned, continue to deprecated handlers
const versionedReq = req as VersionedRequest;
if (versionedReq.apiVersion) {
// This was a versioned request, route to v1 handlers
return v1Routes(req, res, next);
}
return next(); // Not versioned, continue to deprecated handlers
});

// Health check routes
app.use('/health', healthRoutes);
app.use("/health", healthRoutes);

/**
* @openapi
Expand All @@ -128,11 +151,11 @@ app.use('/health', healthRoutes);
* 200:
* description: API is running successfully
*/
app.get('/', (req: Request, res: Response) => {
res.send('FlowFi Backend is running');
app.get("/", (req: Request, res: Response) => {
res.send("FlowFi Backend is running");
});

import { errorHandler } from './middleware/error.middleware.js';
import { errorHandler } from "./middleware/error.middleware.js";

app.use(errorHandler);

Expand Down
Loading
Loading