Files
worklenz/worklenz-backend/src/app.ts
chamikaJ 65af5f659e refactor(build): remove Gruntfile and transition to npm scripts for build process
- Deleted Gruntfile.js to streamline the build process.
- Updated package.json to include new npm scripts for build, clean, and watch tasks.
- Added dependencies for concurrent execution and CSRF token management.
- Integrated csrf-sync for improved CSRF protection in the application.
- Refactored app and API client to utilize the new CSRF token management approach.
2025-05-30 10:56:19 +05:30

240 lines
7.0 KiB
TypeScript

import createError from "http-errors";
import express, { NextFunction, Request, Response } from "express";
import path from "path";
import cookieParser from "cookie-parser";
import logger from "morgan";
import helmet from "helmet";
import compression from "compression";
import passport from "passport";
import { csrfSync } from "csrf-sync";
import rateLimit from "express-rate-limit";
import cors from "cors";
import flash from "connect-flash";
import hpp from "hpp";
import passportConfig from "./passport";
import apiRouter from "./routes/apis";
import authRouter from "./routes/auth";
import emailTemplatesRouter from "./routes/email-templates";
import public_router from "./routes/public";
import { isInternalServer, isProduction } from "./shared/utils";
import sessionMiddleware from "./middlewares/session-middleware";
import safeControllerFunction from "./shared/safe-controller-function";
import AwsSesController from "./controllers/aws-ses-controller";
import { CSP_POLICIES } from "./shared/csp";
const app = express();
// Trust first proxy if behind reverse proxy
app.set("trust proxy", 1);
// Basic middleware setup
app.use(compression());
app.use(logger("dev"));
app.use(express.json({ limit: "50mb" }));
app.use(express.urlencoded({ extended: false, limit: "50mb" }));
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(hpp());
// Helmet security headers
app.use(helmet({
crossOriginEmbedderPolicy: false,
crossOriginResourcePolicy: false,
}));
// Custom security headers
app.use((_req: Request, res: Response, next: NextFunction) => {
res.setHeader("X-XSS-Protection", "1; mode=block");
res.removeHeader("server");
res.setHeader("Content-Security-Policy", CSP_POLICIES);
next();
});
// CORS configuration
const allowedOrigins = [
isProduction()
? [
`http://localhost:5000`,
`http://127.0.0.1:5000`,
process.env.SERVER_CORS || "", // Add hostname from env
process.env.FRONTEND_URL || "" // Support FRONTEND_URL as well
].filter(Boolean) // Remove empty strings
: [
"http://localhost:3000",
"http://localhost:5173",
"http://127.0.0.1:5173",
"http://127.0.0.1:3000",
"http://127.0.0.1:5000",
`http://localhost:5000`,
process.env.SERVER_CORS || "", // Add hostname from env
process.env.FRONTEND_URL || "" // Support FRONTEND_URL as well
].filter(Boolean) // Remove empty strings
].flat();
app.use(cors({
origin: (origin, callback) => {
if (!isProduction() || !origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
console.log("Blocked origin:", origin, process.env.NODE_ENV);
callback(new Error("Not allowed by CORS"));
}
},
credentials: true,
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"],
allowedHeaders: [
"Origin",
"X-Requested-With",
"Content-Type",
"Accept",
"Authorization",
"X-CSRF-Token"
],
exposedHeaders: ["Set-Cookie", "X-CSRF-Token"]
}));
// Handle preflight requests
app.options("*", cors());
// Session setup - must be before passport and CSRF
app.use(sessionMiddleware);
// Passport initialization
passportConfig(passport);
app.use(passport.initialize());
app.use(passport.session());
// Flash messages
app.use(flash());
// Auth check middleware
function isLoggedIn(req: Request, _res: Response, next: NextFunction) {
return req.user ? next() : next(createError(401));
}
// CSRF configuration using csrf-sync for session-based authentication
const {
invalidCsrfTokenError,
generateToken,
csrfSynchronisedProtection,
} = csrfSync({
getTokenFromRequest: (req: Request) => req.headers["x-csrf-token"] as string || (req.body && req.body["_csrf"])
});
// Apply CSRF selectively (exclude webhooks and public routes)
app.use((req, res, next) => {
if (
req.path.startsWith("/webhook/") ||
req.path.startsWith("/secure/") ||
req.path.startsWith("/api/") ||
req.path.startsWith("/public/")
) {
next();
} else {
csrfSynchronisedProtection(req, res, next);
}
});
// Set CSRF token method on request object for compatibility
app.use((req: Request, res: Response, next: NextFunction) => {
// Add csrfToken method to request object for compatibility
if (!req.csrfToken && generateToken) {
req.csrfToken = (overwrite?: boolean) => generateToken(req, overwrite);
}
next();
});
// CSRF token refresh endpoint
app.get("/csrf-token", (req: Request, res: Response) => {
try {
const token = generateToken(req);
res.status(200).json({ done: true, message: "CSRF token refreshed", token });
} catch (error) {
res.status(500).json({ done: false, message: "Failed to generate CSRF token" });
}
});
// Webhook endpoints (no CSRF required)
app.post("/webhook/emails/bounce", safeControllerFunction(AwsSesController.handleBounceResponse));
app.post("/webhook/emails/complaints", safeControllerFunction(AwsSesController.handleComplaintResponse));
app.post("/webhook/emails/reply", safeControllerFunction(AwsSesController.handleReplies));
// Static file serving
if (isProduction()) {
app.use(express.static(path.join(__dirname, "build"), {
maxAge: "1y",
etag: false,
}));
// Handle compressed files
app.get("*.js", (req, res, next) => {
if (req.header("Accept-Encoding")?.includes("br")) {
req.url = `${req.url}.br`;
res.set("Content-Encoding", "br");
res.set("Content-Type", "application/javascript; charset=UTF-8");
} else if (req.header("Accept-Encoding")?.includes("gzip")) {
req.url = `${req.url}.gz`;
res.set("Content-Encoding", "gzip");
res.set("Content-Type", "application/javascript; charset=UTF-8");
}
next();
});
} else {
app.use(express.static(path.join(__dirname, "public")));
}
// API rate limiting
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 1500,
standardHeaders: false,
legacyHeaders: false,
});
// Routes
app.use("/api/v1", apiLimiter, isLoggedIn, apiRouter);
app.use("/secure", authRouter);
app.use("/public", public_router);
if (isInternalServer()) {
app.use("/email-templates", emailTemplatesRouter);
}
// CSRF error handler
app.use((err: any, req: Request, res: Response, next: NextFunction) => {
if (err === invalidCsrfTokenError) {
return res.status(403).json({
done: false,
message: "Invalid CSRF token",
body: null
});
}
next(err);
});
// React app handling - serve index.html for all non-API routes
app.get("*", (req: Request, res: Response, next: NextFunction) => {
if (req.path.startsWith("/api/")) return next();
res.sendFile(path.join(__dirname, isProduction() ? "build" : "public", "index.html"));
});
// Global error handler
app.use((err: any, _req: Request, res: Response, _next: NextFunction) => {
const status = err.status || 500;
if (res.headersSent) {
return;
}
res.status(status);
// Send structured error response
res.json({
done: false,
message: isProduction() ? "Internal Server Error" : err.message,
body: null,
...(process.env.NODE_ENV === "development" ? { stack: err.stack } : {})
});
});
export default app;