feat: add auth middleware for write endpoint protection
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
38
src/server/middleware/auth.ts
Normal file
38
src/server/middleware/auth.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import type { Context, Next } from "hono";
|
||||||
|
import { getCookie } from "hono/cookie";
|
||||||
|
import {
|
||||||
|
getSession,
|
||||||
|
getUserCount,
|
||||||
|
refreshSession,
|
||||||
|
verifyApiKey,
|
||||||
|
} from "../services/auth.service";
|
||||||
|
|
||||||
|
export async function requireAuth(c: Context, next: Next) {
|
||||||
|
const db = c.get("db");
|
||||||
|
|
||||||
|
// Check if any users exist at all
|
||||||
|
if (getUserCount(db) === 0) {
|
||||||
|
return c.json({ error: "setup_required" }, 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check API key first
|
||||||
|
const apiKey = c.req.header("X-API-Key");
|
||||||
|
if (apiKey) {
|
||||||
|
const valid = await verifyApiKey(db, apiKey);
|
||||||
|
if (valid) return next();
|
||||||
|
return c.json({ error: "Invalid API key" }, 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check session cookie
|
||||||
|
const sessionId = getCookie(c, "gearbox_session");
|
||||||
|
if (sessionId) {
|
||||||
|
const session = getSession(db, sessionId);
|
||||||
|
if (session) {
|
||||||
|
// Refresh session expiry on use
|
||||||
|
refreshSession(db, sessionId);
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json({ error: "Authentication required" }, 401);
|
||||||
|
}
|
||||||
86
tests/middleware/auth.test.ts
Normal file
86
tests/middleware/auth.test.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { beforeEach, describe, expect, test } from "bun:test";
|
||||||
|
import { Hono } from "hono";
|
||||||
|
import { requireAuth } from "../../src/server/middleware/auth";
|
||||||
|
import {
|
||||||
|
createApiKey,
|
||||||
|
createSession,
|
||||||
|
createUser,
|
||||||
|
} from "../../src/server/services/auth.service";
|
||||||
|
import { createTestDb } from "../helpers/db";
|
||||||
|
|
||||||
|
let db: ReturnType<typeof createTestDb>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
db = createTestDb();
|
||||||
|
});
|
||||||
|
|
||||||
|
function createApp() {
|
||||||
|
const app = new Hono<{ Variables: { db?: any } }>();
|
||||||
|
app.use("*", async (c, next) => {
|
||||||
|
c.set("db", db);
|
||||||
|
await next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Public GET
|
||||||
|
app.get("/items", (c) => c.json({ ok: true }));
|
||||||
|
|
||||||
|
// Protected POST
|
||||||
|
app.post("/items", requireAuth, (c) => c.json({ ok: true }));
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("auth middleware", () => {
|
||||||
|
test("allows GET requests without auth (middleware not applied to GET)", async () => {
|
||||||
|
const app = createApp();
|
||||||
|
const res = await app.request("/items");
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns 403 setup_required when no users exist", async () => {
|
||||||
|
const app = createApp();
|
||||||
|
const res = await app.request("/items", { method: "POST" });
|
||||||
|
expect(res.status).toBe(403);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.error).toBe("setup_required");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects POST without auth when users exist", async () => {
|
||||||
|
const app = createApp();
|
||||||
|
await createUser(db, "admin", "pass");
|
||||||
|
const res = await app.request("/items", { method: "POST" });
|
||||||
|
expect(res.status).toBe(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("allows POST with valid session cookie", async () => {
|
||||||
|
const app = createApp();
|
||||||
|
const user = await createUser(db, "admin", "pass");
|
||||||
|
const session = createSession(db, user.id);
|
||||||
|
const res = await app.request("/items", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { Cookie: `gearbox_session=${session.id}` },
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("allows POST with valid API key", async () => {
|
||||||
|
const app = createApp();
|
||||||
|
await createUser(db, "admin", "pass");
|
||||||
|
const key = await createApiKey(db, "test");
|
||||||
|
const res = await app.request("/items", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "X-API-Key": key.rawKey },
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects POST with invalid API key", async () => {
|
||||||
|
const app = createApp();
|
||||||
|
await createUser(db, "admin", "pass");
|
||||||
|
const res = await app.request("/items", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "X-API-Key": "invalid" },
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(401);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user