Skip to content

Commit de6f864

Browse files
authored
Merge pull request #97 from bocan:just-a-thursday
Implement rate limiting for various endpoints and update dependencies
2 parents c123397 + 137c688 commit de6f864

6 files changed

Lines changed: 163 additions & 122 deletions

File tree

package-lock.json

Lines changed: 110 additions & 110 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

server/src/index.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import searchRoutes from "./routes/search";
1111
import attachmentRoutes from "./routes/attachments";
1212
import templatesRoutes from "./routes/templates";
1313
import { requireAuth } from "./middleware/auth";
14+
import { healthCheckLimiter, staticFileLimiter } from "./middleware/rateLimiters";
1415
import { GitService } from "./services/gitService";
1516
import { DATA_DIR as DEFAULT_DATA_DIR, FileSystemService } from "./services/fileSystem";
1617

@@ -258,25 +259,25 @@ app.use("/api/search", requireAuth, searchRoutes); // Protected
258259
app.use("/api/attachments", requireAuth, attachmentRoutes); // Protected
259260

260261
// Health check
261-
app.get("/api/health", (req: Request, res: Response) => {
262+
app.get("/api/health", healthCheckLimiter, (req: Request, res: Response) => {
262263
res.json({ status: "ok" });
263264
});
264265

265266
// Handle .well-known/* routes that aren't supported (for MCP clients doing OAuth discovery)
266-
app.get("/.well-known/oauth-authorization-server", (req: Request, res: Response) => {
267+
app.get("/.well-known/oauth-authorization-server", healthCheckLimiter, (req: Request, res: Response) => {
267268
res.status(404).json({ error: "OAuth not supported", message: "This server uses API key authentication" });
268269
});
269-
app.get("/.well-known/openid-configuration", (req: Request, res: Response) => {
270+
app.get("/.well-known/openid-configuration", healthCheckLimiter, (req: Request, res: Response) => {
270271
res.status(404).json({ error: "OIDC not supported", message: "This server uses API key authentication" });
271272
});
272273

273274
// Serve static files in production
274275
if (process.env.NODE_ENV === "production") {
275276
const clientDistPath = path.join(__dirname, "../../client/dist");
276-
app.use(express.static(clientDistPath));
277+
app.use(staticFileLimiter, express.static(clientDistPath));
277278

278279
// Handle client-side routing - serve index.html for all non-API routes
279-
app.get(/.*/, (req: Request, res: Response) => {
280+
app.get(/.*/, staticFileLimiter, (req: Request, res: Response) => {
280281
res.sendFile(path.join(clientDistPath, "index.html"));
281282
});
282283
}

server/src/middleware/rateLimiters.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,44 @@
11
import rateLimit from "express-rate-limit";
22

3+
/**
4+
* Rate limiter for read operations (get pages, list folders, view history)
5+
* These read from the file system but don't modify it
6+
* Limit: 120 requests per minute per IP
7+
*/
8+
export const readLimiter = rateLimit({
9+
windowMs: 60 * 1000, // 1 minute
10+
max: 120,
11+
message: { error: "Too many read requests, please slow down" },
12+
standardHeaders: true,
13+
legacyHeaders: false,
14+
});
15+
16+
/**
17+
* Rate limiter for static file serving
18+
* Used for serving the SPA and static assets
19+
* Limit: 200 requests per minute per IP (generous for page loads with many assets)
20+
*/
21+
export const staticFileLimiter = rateLimit({
22+
windowMs: 60 * 1000, // 1 minute
23+
max: 200,
24+
message: { error: "Too many requests, please slow down" },
25+
standardHeaders: true,
26+
legacyHeaders: false,
27+
});
28+
29+
/**
30+
* Rate limiter for health check and metadata endpoints
31+
* These are lightweight but should still be protected
32+
* Limit: 60 requests per minute per IP
33+
*/
34+
export const healthCheckLimiter = rateLimit({
35+
windowMs: 60 * 1000, // 1 minute
36+
max: 60,
37+
message: { error: "Too many health check requests" },
38+
standardHeaders: true,
39+
legacyHeaders: false,
40+
});
41+
342
/**
443
* Rate limiter for file operations (create, update, delete, rename, move)
544
* These are expensive operations that modify the file system and git history

server/src/routes/attachments.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ router.post("/", fileTransferLimiter, upload.single("file"), async (req: Request
103103
});
104104

105105
// List attachments for a folder
106-
router.get("/", async (req: Request, res: Response) => {
106+
router.get("/", fileTransferLimiter, async (req: Request, res: Response) => {
107107
try {
108108
const folderPath = getFolderQueryParam(req);
109109

server/src/routes/pages.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
11
import { Router } from "express";
22
import * as pageController from "../controllers/pageController";
3-
import { fileOperationLimiter } from "../middleware/rateLimiters";
3+
import { fileOperationLimiter, readLimiter } from "../middleware/rateLimiters";
44

55
const router = Router();
66

77
// Specific routes must come before wildcard routes
8-
router.get("/", pageController.getPages);
8+
router.get("/", readLimiter, pageController.getPages);
99
router.post("/", fileOperationLimiter, pageController.createPage);
1010
router.put("/move", fileOperationLimiter, pageController.movePage);
1111
router.put("/rename/file", fileOperationLimiter, pageController.renamePage);
1212

1313
// Wildcard routes (path-to-regexp v8): use `/*name` for "rest of path"
1414
// Order matters: more specific routes must come before the generic page handler.
15-
router.get("/*path/history", pageController.getPageHistory);
16-
router.get("/*path/versions/:hash", pageController.getPageVersion);
15+
router.get("/*path/history", readLimiter, pageController.getPageHistory);
16+
router.get("/*path/versions/:hash", readLimiter, pageController.getPageVersion);
1717
router.post(
1818
"/*path/restore/:hash",
1919
fileOperationLimiter,
2020
pageController.restorePageVersion,
2121
);
22-
router.get("/*path", pageController.getPage);
22+
router.get("/*path", readLimiter, pageController.getPage);
2323
router.put("/*path", fileOperationLimiter, pageController.updatePage);
2424
router.delete("/*path", fileOperationLimiter, pageController.deletePage);
2525

server/src/routes/templates.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { Router } from "express";
22
import * as templatesController from "../controllers/templatesController";
3+
import { readLimiter } from "../middleware/rateLimiters";
34

45
const router = Router();
56

6-
router.get("/", templatesController.getTemplates);
7+
router.get("/", readLimiter, templatesController.getTemplates);
78

89
export default router;

0 commit comments

Comments
 (0)