"use server"; import { randomBytes } from "node:crypto"; import { revalidatePath } from "next/cache"; import { redirect } from "next/navigation"; import { requirePermission } from "@/lib/admin/guard"; import { PERMS } from "@/lib/permissions"; import { prisma } from "@/lib/prisma"; import { logStaffActivity } from "@/lib/services/staff-activity"; // Radio API keys (radio_api_keys). External integrations (AzureCast bridges, // widgets, bots) authenticate with a server-generated key. The key itself is // minted here with crypto.randomBytes — never accepted from the form — and the // `permissions` JSON column is intentionally left untouched by this CMS slice. function str(raw: FormDataEntryValue | null): string { return typeof raw === "string" ? raw : ""; } /** Parse a BigInt id from a form value, or null when blank/invalid. */ function parseId(raw: FormDataEntryValue | null): bigint | null { const s = str(raw).trim(); if (!s) return null; try { return BigInt(s); } catch { return null; } } /** Clamp a form value to a non-negative integer (defaulting to `fallback`). */ function intOr(raw: FormDataEntryValue | null, fallback: number): number { const n = Number(str(raw).trim()); if (!Number.isFinite(n) || n < 0) return fallback; return Math.floor(n); } export async function createApiKey(formData: FormData): Promise { const staff = await requirePermission(PERMS.RADIO_EDIT); const name = str(formData.get("name")).trim().slice(0, 255); if (!name) return; const rateLimit = intOr(formData.get("rateLimit"), 300); const allowedIps = str(formData.get("allowedIps")).trim().slice(0, 255) || null; // Server-side key generation — 24 random bytes → 48 hex chars (fits VarChar(64)). const key = randomBytes(24).toString("hex"); const now = new Date(); try { const created = await prisma.radioApiKeys.create({ data: { name, key, allowedIps, rateLimit, isActive: true, createdAt: now, updatedAt: now, }, }); await logStaffActivity({ staffId: staff.id, action: "radio_api_key_create", description: `Created radio API key "${name}" (#${created.id}, rate limit ${rateLimit})`, targetType: "radio_api_key", targetId: Number(created.id), }); } catch { // Unique-key collision (astronomically unlikely) or DB down — fail soft. return; } revalidatePath("/admin/radio/api-keys"); redirect("/admin/radio/api-keys?created=1"); } export async function toggleApiKey(formData: FormData): Promise { const staff = await requirePermission(PERMS.RADIO_EDIT); const id = parseId(formData.get("id")); if (id == null) return; try { const existing = await prisma.radioApiKeys.findUnique({ where: { id }, select: { name: true, isActive: true }, }); if (!existing) return; const next = !existing.isActive; await prisma.radioApiKeys.update({ where: { id }, data: { isActive: next, updatedAt: new Date() }, }); await logStaffActivity({ staffId: staff.id, action: "radio_api_key_toggle", description: `${next ? "Activated" : "Deactivated"} radio API key "${existing.name}" (#${id})`, targetType: "radio_api_key", targetId: Number(id), }); } catch { return; } revalidatePath("/admin/radio/api-keys"); } export async function deleteApiKey(formData: FormData): Promise { const staff = await requirePermission(PERMS.RADIO_EDIT); const id = parseId(formData.get("id")); if (id == null) return; try { await prisma.radioApiKeys.delete({ where: { id } }); await logStaffActivity({ staffId: staff.id, action: "radio_api_key_delete", description: `Deleted radio API key #${id}`, targetType: "radio_api_key", targetId: Number(id), }); } catch { return; } revalidatePath("/admin/radio/api-keys"); }