65 lines
2.0 KiB
TypeScript
65 lines
2.0 KiB
TypeScript
import express, { Request, Response, NextFunction } from "express";
|
|
import cors from "cors";
|
|
import cookieParser from "cookie-parser";
|
|
import helmet from "helmet";
|
|
import { config } from "./config";
|
|
import { loadAuth, requireCsrf } from "./authMiddleware";
|
|
import authRouter from "./routes/auth";
|
|
import healthRouter from "./routes/health";
|
|
import racesRouter from "./routes/races";
|
|
|
|
export function createApp(): express.Express {
|
|
const app = express();
|
|
|
|
app.use(
|
|
helmet({
|
|
contentSecurityPolicy:
|
|
config.securityProfile === "production"
|
|
? {
|
|
directives: {
|
|
defaultSrc: ["'self'"],
|
|
scriptSrc: ["'self'"],
|
|
styleSrc: ["'self'"],
|
|
imgSrc: ["'self'", "data:", "https:"],
|
|
connectSrc: ["'self'"],
|
|
objectSrc: ["'none'"],
|
|
frameAncestors: ["'none'"],
|
|
},
|
|
}
|
|
: false,
|
|
hsts:
|
|
config.securityProfile === "production"
|
|
? { maxAge: 31_536_000, includeSubDomains: true }
|
|
: false,
|
|
referrerPolicy: { policy: "strict-origin-when-cross-origin" },
|
|
}),
|
|
);
|
|
app.use(
|
|
cors({
|
|
origin: config.corsOrigin,
|
|
credentials: true,
|
|
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
|
allowedHeaders: ["Content-Type", "X-CSRF-Token"],
|
|
}),
|
|
);
|
|
app.use(express.json());
|
|
app.use(cookieParser(config.session.secret));
|
|
app.use(loadAuth);
|
|
app.use(requireCsrf);
|
|
|
|
app.use("/api", healthRouter);
|
|
app.use("/api", authRouter);
|
|
app.use("/api", racesRouter);
|
|
|
|
app.use((err: unknown, _req: Request, res: Response, _next: NextFunction) => {
|
|
if (err instanceof SyntaxError && "body" in err) {
|
|
res.status(400).json({ error: "validation_error", details: ["Invalid JSON in request body"] });
|
|
return;
|
|
}
|
|
console.error("[app] Unhandled error:", err);
|
|
res.status(500).json({ error: "unknown_error", details: ["Internal server error"] });
|
|
});
|
|
|
|
return app;
|
|
}
|