← back to Govarbitrage
src/app/api/credentials/route.ts
45 lines
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/db";
import { encryptSecret } from "@/lib/crypto";
import { requireAdmin } from "@/lib/auth";
import { z } from "zod";
export const dynamic = "force-dynamic";
const Schema = z.object({ provider: z.string().min(1), secret: z.string().min(1) });
// Store a provider API key encrypted at rest (AES-256-GCM). The plaintext is
// never persisted and never returned. Attaches to the seeded admin user.
export async function POST(req: NextRequest) {
const denied = await requireAdmin(req);
if (denied) return denied;
const parsed = Schema.safeParse(await req.json().catch(() => ({})));
if (!parsed.success) return NextResponse.json({ error: "Invalid payload" }, { status: 400 });
const user = await prisma.user.findFirst({ where: { role: "ADMIN" } });
if (!user) return NextResponse.json({ error: "No admin user" }, { status: 400 });
const enc = encryptSecret(parsed.data.secret);
const cred = await prisma.apiCredential.upsert({
where: { userId_provider: { userId: user.id, provider: parsed.data.provider } },
create: { userId: user.id, provider: parsed.data.provider, ...enc },
update: enc,
});
await prisma.auditLog.create({
data: { userId: user.id, action: "credential.upsert", entity: "ApiCredential", entityId: cred.id },
});
// Never return ciphertext to the client.
return NextResponse.json({ id: cred.id, provider: cred.provider, stored: true });
}
// List which providers have a stored credential (no secrets). ADMIN-only —
// previously this had no auth check of its own (it relied solely on middleware).
export async function GET(req: NextRequest) {
const denied = await requireAdmin(req);
if (denied) return denied;
const creds = await prisma.apiCredential.findMany({ select: { id: true, provider: true, createdAt: true } });
return NextResponse.json({ credentials: creds });
}