diff --git a/app/server/index.ts b/app/server/index.ts index a8cc045c..a390a9c6 100644 --- a/app/server/index.ts +++ b/app/server/index.ts @@ -2,6 +2,8 @@ import { createHonoServer } from "react-router-hono-server/bun"; import { Scalar } from "@scalar/hono-api-reference"; import { Hono } from "hono"; import { logger as honoLogger } from "hono/logger"; +import { secureHeaders } from "hono/secure-headers"; +import { rateLimiter } from "hono-rate-limiter"; import { openAPIRouteHandler } from "hono-openapi"; import { runDbMigrations } from "./db/db"; import { authController } from "./modules/auth/auth.controller"; @@ -41,8 +43,16 @@ export const scalarDescriptor = Scalar({ const app = new Hono() .use(honoLogger()) + .use(secureHeaders()) + .use( + rateLimiter({ + windowMs: 15 * 60 * 1000, + limit: 100, + keyGenerator: (c) => c.req.header("x-forwarded-for") ?? "", + }), + ) .get("healthcheck", (c) => c.json({ status: "ok" })) - .route("/api/v1/auth", authController.basePath("/api/v1")) + .route("/api/v1/auth", authController) .use("/api/v1/volumes/*", requireAuth) .use("/api/v1/repositories/*", requireAuth) .use("/api/v1/backups/*", requireAuth) @@ -56,8 +66,9 @@ const app = new Hono() .route("/api/v1/system", systemController) .route("/api/v1/events", eventsController); -app.get("/api/v1/openapi.json", generalDescriptor(app)); -app.get("/api/v1/docs", scalarDescriptor); +// API documentation endpoints require authentication +app.get("/api/v1/openapi.json", requireAuth, generalDescriptor(app)); +app.get("/api/v1/docs", requireAuth, scalarDescriptor); app.onError((err, c) => { logger.error(`${c.req.url}: ${err.message}`); diff --git a/app/server/modules/auth/auth.controller.ts b/app/server/modules/auth/auth.controller.ts index c78d2818..e2aaecea 100644 --- a/app/server/modules/auth/auth.controller.ts +++ b/app/server/modules/auth/auth.controller.ts @@ -1,5 +1,5 @@ import { validator } from "hono-openapi"; - +import { rateLimiter } from "hono-rate-limiter"; import { Hono } from "hono"; import { deleteCookie, getCookie, setCookie } from "hono/cookie"; import { @@ -31,6 +31,13 @@ const COOKIE_OPTIONS = { }; export const authController = new Hono() + .use( + rateLimiter({ + windowMs: 15 * 60 * 1000, + limit: 5, + keyGenerator: (c) => c.req.header("x-forwarded-for") ?? "", + }), + ) .post("/register", registerDto, validator("json", registerBodySchema), async (c) => { const body = c.req.valid("json"); diff --git a/bun.lock b/bun.lock index 1668570b..419e3cac 100644 --- a/bun.lock +++ b/bun.lock @@ -36,6 +36,7 @@ "es-toolkit": "^1.42.0", "hono": "4.10.5", "hono-openapi": "^1.1.1", + "hono-rate-limiter": "^0.5.0", "http-errors-enhanced": "^4.0.2", "isbot": "^5.1.32", "lucide-react": "^0.555.0", @@ -715,6 +716,8 @@ "hono-openapi": ["hono-openapi@1.1.1", "", { "peerDependencies": { "@hono/standard-validator": "^0.1.2", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.8", "@types/json-schema": "^7.0.15", "hono": "^4.8.3", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-AC3HNhZYPHhnZdSy2Je7GDoTTNxPos6rKRQKVDBbSilY3cWJPqsxRnN6zA4pU7tfxmQEMTqkiLXbw6sAaemB8Q=="], + "hono-rate-limiter": ["hono-rate-limiter@0.5.0", "", { "peerDependencies": { "hono": "^4.10.8", "unstorage": "^1.17.3" }, "optionalPeers": ["unstorage"] }, "sha512-Cps4udhDdPQ3O1Dm1fOzunI1iN1fW3TcVj1YvPdIjxHiHRitTsEz05q+BjgnLtcVDaDGbyuYyBaAxIy1DD1bMw=="], + "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], "http-errors-enhanced": ["http-errors-enhanced@4.0.2", "", {}, "sha512-5EXN1gmhJVvuWpNfz+RclWvLnnENEXNMPfww3gm30H9mQzPF4QSBj/MD5FRkVDxGIUhO/cR2GSLCd/6C6xpBcw=="], diff --git a/package.json b/package.json index 697d0fe4..7c70e1d0 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "es-toolkit": "^1.42.0", "hono": "4.10.5", "hono-openapi": "^1.1.1", + "hono-rate-limiter": "^0.5.0", "http-errors-enhanced": "^4.0.2", "isbot": "^5.1.32", "lucide-react": "^0.555.0",