mike/backend/src/index.ts
2026-05-08 20:45:16 +08:00

126 lines
3.6 KiB
TypeScript

import "dotenv/config";
import express from "express";
import cors from "cors";
import helmet from "helmet";
import rateLimit from "express-rate-limit";
import { chatRouter } from "./routes/chat";
import { projectsRouter } from "./routes/projects";
import { projectChatRouter } from "./routes/projectChat";
import { documentsRouter } from "./routes/documents";
import { tabularRouter } from "./routes/tabular";
import { workflowsRouter } from "./routes/workflows";
import { userRouter } from "./routes/user";
import { downloadsRouter } from "./routes/downloads";
const app = express();
const PORT = process.env.PORT ?? 3001;
const isProduction = process.env.NODE_ENV === "production";
function envInt(name: string, fallback: number): number {
const raw = process.env[name];
if (!raw) return fallback;
const parsed = Number.parseInt(raw, 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
}
function minutes(value: number): number {
return value * 60 * 1000;
}
function hours(value: number): number {
return minutes(value * 60);
}
function makeLimiter(options: {
windowMs: number;
max: number;
message?: string;
}) {
return rateLimit({
windowMs: options.windowMs,
max: options.max,
standardHeaders: true,
legacyHeaders: false,
skip: (req) => req.method === "OPTIONS",
message: {
detail:
options.message ?? "Too many requests. Please try again later.",
},
});
}
const generalLimiter = makeLimiter({
windowMs: minutes(envInt("RATE_LIMIT_GENERAL_WINDOW_MINUTES", 15)),
max: envInt("RATE_LIMIT_GENERAL_MAX", 300),
});
const chatLimiter = makeLimiter({
windowMs: minutes(envInt("RATE_LIMIT_CHAT_WINDOW_MINUTES", 15)),
max: envInt("RATE_LIMIT_CHAT_MAX", 30),
message: "Too many chat requests. Please try again later.",
});
const chatCreateLimiter = makeLimiter({
windowMs: minutes(envInt("RATE_LIMIT_CHAT_CREATE_WINDOW_MINUTES", 15)),
max: envInt("RATE_LIMIT_CHAT_CREATE_MAX", 60),
});
const uploadLimiter = makeLimiter({
windowMs: hours(envInt("RATE_LIMIT_UPLOAD_WINDOW_HOURS", 1)),
max: envInt("RATE_LIMIT_UPLOAD_MAX", 50),
message: "Too many upload requests. Please try again later.",
});
app.disable("x-powered-by");
app.set("trust proxy", envInt("TRUST_PROXY_HOPS", 1));
app.use(
helmet({
contentSecurityPolicy: false,
crossOriginEmbedderPolicy: false,
hsts: isProduction
? {
maxAge: 15552000,
includeSubDomains: true,
}
: false,
referrerPolicy: { policy: "no-referrer" },
}),
);
app.use(
cors({
origin: process.env.FRONTEND_URL ?? "http://localhost:3000",
credentials: true,
}),
);
app.use(generalLimiter);
app.use(express.json({ limit: "50mb" }));
app.post("/chat", chatLimiter);
app.post("/projects/:projectId/chat", chatLimiter);
app.post("/tabular-review/:reviewId/chat", chatLimiter);
app.post("/tabular-review/:reviewId/generate", chatLimiter);
app.post("/chat/create", chatCreateLimiter);
app.post("/chat/:chatId/generate-title", chatCreateLimiter);
app.post("/single-documents", uploadLimiter);
app.post("/single-documents/:documentId/versions", uploadLimiter);
app.post("/projects/:projectId/documents", uploadLimiter);
app.use("/chat", chatRouter);
app.use("/projects", projectsRouter);
app.use("/projects/:projectId/chat", projectChatRouter);
app.use("/single-documents", documentsRouter);
app.use("/tabular-review", tabularRouter);
app.use("/workflows", workflowsRouter);
app.use("/user", userRouter);
app.use("/users", userRouter);
app.use("/download", downloadsRouter);
app.get("/health", (_req, res) => res.json({ ok: true }));
app.listen(PORT, () => {
console.log(`Mike backend running on port ${PORT}`);
});