diff --git a/apps/api/package.json b/apps/api/package.json index 25fa0e90e39..0a8f2abbd0d 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -5,7 +5,7 @@ "scripts": { "dev": "node src/server.js", "start": "node src/server.js", - "test": "node --test src/tests" + "test": "node --test src/tests/**/*.js" }, "dependencies": { "cors": "^2.8.5", diff --git a/apps/api/src/middleware/requireAdmin.js b/apps/api/src/middleware/requireAdmin.js new file mode 100644 index 00000000000..3c3ef779264 --- /dev/null +++ b/apps/api/src/middleware/requireAdmin.js @@ -0,0 +1,8 @@ +import { fail } from "../utils/response.js"; + +export function requireAdmin(req, res, next) { + if (req.user?.role !== "admin") { + return fail(res, "Forbidden", 403); + } + return next(); +} diff --git a/apps/api/src/routes/adminRoutes.js b/apps/api/src/routes/adminRoutes.js index 4c1da76f9ad..2291abdc57e 100644 --- a/apps/api/src/routes/adminRoutes.js +++ b/apps/api/src/routes/adminRoutes.js @@ -1,8 +1,10 @@ import { Router } from "express"; import { metrics } from "../controllers/adminController.js"; import { authMiddleware } from "../middleware/auth.js"; +import { requireAdmin } from "../middleware/requireAdmin.js"; export const adminRoutes = Router(); adminRoutes.use(authMiddleware); +adminRoutes.use(requireAdmin); adminRoutes.get("/metrics", metrics); diff --git a/apps/api/src/tests/admin-role.test.js b/apps/api/src/tests/admin-role.test.js new file mode 100644 index 00000000000..4994c561c77 --- /dev/null +++ b/apps/api/src/tests/admin-role.test.js @@ -0,0 +1,48 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { createApp } from "../app.js"; +import { signAccessToken } from "../utils/jwt.js"; + +async function withServer(run) { + const app = createApp(); + const server = app.listen(0); + await new Promise((resolve, reject) => { + server.once("listening", resolve); + server.once("error", reject); + }); + try { await run(server.address().port); } + finally { + await new Promise((resolve, reject) => { + server.close((error) => (error ? reject(error) : resolve())); + }); + } +} + +test("GET /api/admin/metrics without auth returns 401", async () => { + await withServer(async (port) => { + const response = await fetch(`http://127.0.0.1:${port}/api/admin/metrics`); + assert.equal(response.status, 401); + }); +}); + +test("GET /api/admin/metrics as client returns 403", async () => { + await withServer(async (port) => { + const token = signAccessToken({ sub: "usr_c", role: "client" }); + const response = await fetch(`http://127.0.0.1:${port}/api/admin/metrics`, { + headers: { Authorization: `Bearer ${token}` } + }); + assert.equal(response.status, 403); + }); +}); + +test("GET /api/admin/metrics as admin returns 200", async () => { + await withServer(async (port) => { + const token = signAccessToken({ sub: "usr_a", role: "admin" }); + const response = await fetch(`http://127.0.0.1:${port}/api/admin/metrics`, { + headers: { Authorization: `Bearer ${token}` } + }); + const payload = await response.json(); + assert.equal(response.status, 200); + assert.equal(payload.success, true); + }); +});