diff --git a/src/server/index.ts b/src/server/index.ts index df56a5b..c7b5b15 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -10,6 +10,7 @@ import { db as prodDb } from "../db/index.ts"; import { seedDefaults } from "../db/seed.ts"; import { mcpRoutes } from "./mcp/index.ts"; import { requireAuth } from "./middleware/auth.ts"; +import { createRateLimit } from "./middleware/rateLimit.ts"; import { authRoutes } from "./routes/auth.ts"; import { categoryRoutes } from "./routes/categories.ts"; import { globalItemRoutes } from "./routes/global-items.ts"; @@ -117,6 +118,35 @@ app.use("/api/*", async (c, next) => { return next(); }); +// Rate limiting for public endpoints (per D-07, D-08) +const browseTier = createRateLimit(120, 60_000); +const detailTier = createRateLimit(60, 60_000); + +// Browse endpoints — higher limit for list/search +app.use("/api/global-items", async (c, next) => { + if (c.req.method === "GET" && !c.req.path.match(/^\/api\/global-items\/\d+$/)) + return browseTier(c, next); + return next(); +}); +app.use("/api/tags", async (c, next) => { + if (c.req.method === "GET") return browseTier(c, next); + return next(); +}); + +// Detail endpoints — moderate limit for individual resources +app.use("/api/global-items/:id", async (c, next) => { + if (c.req.method === "GET") return detailTier(c, next); + return next(); +}); +app.use("/api/setups/:id/public", async (c, next) => { + if (c.req.method === "GET") return detailTier(c, next); + return next(); +}); +app.use("/api/users/:id/profile", async (c, next) => { + if (c.req.method === "GET") return detailTier(c, next); + return next(); +}); + // Auth middleware for all data routes (userId must be available for per-user scoping) app.use("/api/*", async (c, next) => { // Skip auth routes — they handle their own auth