diff --git a/src/app/api/agent-runs/route.test.ts b/src/app/api/agent-runs/route.test.ts index 3b45da6d..849b94da 100644 --- a/src/app/api/agent-runs/route.test.ts +++ b/src/app/api/agent-runs/route.test.ts @@ -56,12 +56,17 @@ describe("GET /api/agent-runs", () => { mocks.agentRunFindMany.mockResolvedValue([]); }); - it("returns agent runs without authentication", async () => { + it("401s an unauthenticated request", async () => { + const res = await GET(getRequest("http://localhost/api/agent-runs", false)); + expect(res.status).toBe(401); + }); + + it("returns agent runs to an authenticated caller", async () => { mocks.agentRunFindMany.mockResolvedValue([ { id: "run-1", agentName: "saffron", status: "completed" }, ]); - const res = await GET(getRequest("http://localhost/api/agent-runs", false)); + const res = await GET(getRequest("http://localhost/api/agent-runs")); expect(res.status).toBe(200); const body = await res.json(); diff --git a/src/app/api/agent-runs/route.ts b/src/app/api/agent-runs/route.ts index c94f79db..50432f56 100644 --- a/src/app/api/agent-runs/route.ts +++ b/src/app/api/agent-runs/route.ts @@ -4,6 +4,9 @@ import { isValidEscalatedOutcome, VALID_ESCALATED_OUTCOMES } from "@/types"; import { authorizeRequest } from "@/lib/auth"; export async function GET(request: Request) { + if (!(await authorizeRequest(request)).authorized) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } const { searchParams } = new URL(request.url); const limit = parseInt(searchParams.get("limit") || "50"); diff --git a/src/app/api/audit/route.test.ts b/src/app/api/audit/route.test.ts index 457f89e3..6f588737 100644 --- a/src/app/api/audit/route.test.ts +++ b/src/app/api/audit/route.test.ts @@ -24,20 +24,24 @@ vi.mock("@/lib/prisma", () => ({ import { GET } from "./route"; -function request(urlString: string) { - return new Request(urlString, { headers: {} }); +function request(urlString: string, includeAuth = true) { + const headers: Record = {}; + if (includeAuth) headers.Authorization = `Bearer ${mockToken}`; + return new Request(urlString, { headers }); } describe("GET /api/audit", () => { - // NOTE: This route is intentionally unauthenticated. It returns all AuditLog - // rows to any caller. In production deployments behind a firewall or auth - // gateway this is acceptable; in open deployments consider adding auth. beforeEach(() => { vi.clearAllMocks(); mocks.auditLogFindMany.mockResolvedValue([]); }); - it("returns audit logs without authentication", async () => { + it("401s an unauthenticated request", async () => { + const res = await GET(request("http://localhost/api/audit", false)); + expect(res.status).toBe(401); + }); + + it("returns audit logs to an authenticated caller", async () => { mocks.auditLogFindMany.mockResolvedValue([ { id: "log-1", actor: "agent", action: "move_issue", createdAt: new Date() }, ]); diff --git a/src/app/api/audit/route.ts b/src/app/api/audit/route.ts index 3559d99e..2823cce6 100644 --- a/src/app/api/audit/route.ts +++ b/src/app/api/audit/route.ts @@ -1,7 +1,11 @@ import { NextResponse } from "next/server"; import { prisma } from "@/lib/prisma"; +import { authorizeRequest } from "@/lib/auth"; export async function GET(request: Request) { + if (!(await authorizeRequest(request)).authorized) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } const { searchParams } = new URL(request.url); const limit = parseInt(searchParams.get("limit") || "50"); const repo = searchParams.get("repo"); diff --git a/src/app/api/automation/repos/route.ts b/src/app/api/automation/repos/route.ts index 04e896ec..24760bd9 100644 --- a/src/app/api/automation/repos/route.ts +++ b/src/app/api/automation/repos/route.ts @@ -6,7 +6,10 @@ import { isValidRepoName } from "@/lib/config"; import { auditTrackedRepoCreateFailure, createTrackedRepo } from "@/lib/tracked-repos"; import { authorizeRequest } from "@/lib/auth"; -export async function GET() { +export async function GET(request: Request) { + if (!(await authorizeRequest(request)).authorized) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } try { const repos = await prisma.automationRepo.findMany({ orderBy: { fullName: "asc" }, diff --git a/src/app/api/automation/sync/route.ts b/src/app/api/automation/sync/route.ts index 7580ec86..cee05e6b 100644 --- a/src/app/api/automation/sync/route.ts +++ b/src/app/api/automation/sync/route.ts @@ -408,7 +408,10 @@ async function syncRepo(repoFullName: string) { } } -export async function GET() { +export async function GET(request: Request) { + if (!(await authorizeRequest(request)).authorized) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } const repos = await prisma.automationRepo.findMany({ orderBy: { fullName: "asc" }, include: { diff --git a/src/app/api/repos/route.test.ts b/src/app/api/repos/route.test.ts index b3ee24c8..9474dc8d 100644 --- a/src/app/api/repos/route.test.ts +++ b/src/app/api/repos/route.test.ts @@ -25,21 +25,26 @@ vi.mock("@/lib/prisma", () => ({ import { GET } from "./route"; describe("GET /api/repos", () => { - // NOTE: This route is intentionally unauthenticated. It returns all tracked - // repositories to any caller. In production deployments behind a firewall or - // auth gateway this is acceptable; in open deployments consider adding auth. + const authed = () => + new Request("http://localhost/api/repos", { headers: { Authorization: `Bearer ${mockToken}` } }); + beforeEach(() => { vi.clearAllMocks(); mocks.repositoryFindMany.mockResolvedValue([]); }); - it("returns repos without authentication", async () => { + it("401s an unauthenticated request", async () => { + const res = await GET(new Request("http://localhost/api/repos")); + expect(res.status).toBe(401); + }); + + it("returns repos to an authenticated caller", async () => { mocks.repositoryFindMany.mockResolvedValue([ { id: "r1", fullName: "org/repo1", enabled: true }, { id: "r2", fullName: "org/repo2", enabled: true }, ]); - const res = await GET(); + const res = await GET(authed()); expect(res.status).toBe(200); const body = await res.json(); @@ -51,7 +56,7 @@ describe("GET /api/repos", () => { it("returns empty array when no repos exist", async () => { mocks.repositoryFindMany.mockResolvedValue([]); - const res = await GET(); + const res = await GET(authed()); expect(res.status).toBe(200); const body = await res.json(); @@ -59,7 +64,7 @@ describe("GET /api/repos", () => { }); it("orders by fullName ascending", async () => { - await GET(); + await GET(authed()); const call = mocks.repositoryFindMany.mock.calls[0][0]; expect(call.orderBy).toEqual({ fullName: "asc" }); @@ -68,7 +73,7 @@ describe("GET /api/repos", () => { it("returns 500 on database error", async () => { mocks.repositoryFindMany.mockRejectedValue(new Error("db connection lost")); - const res = await GET(); + const res = await GET(authed()); expect(res.status).toBe(500); const body = await res.json(); diff --git a/src/app/api/repos/route.ts b/src/app/api/repos/route.ts index 8bb2cafa..b2d67b04 100644 --- a/src/app/api/repos/route.ts +++ b/src/app/api/repos/route.ts @@ -5,7 +5,10 @@ import { isValidRepoName } from "@/lib/config"; import { auditTrackedRepoCreateFailure, createTrackedRepo } from "@/lib/tracked-repos"; import { authorizeRequest } from "@/lib/auth"; -export async function GET() { +export async function GET(request: Request) { + if (!(await authorizeRequest(request)).authorized) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } try { const repos = await prisma.repository.findMany({ orderBy: { fullName: "asc" },