← 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 });
}