"use server"; import { db } from "@/db"; import { forms, submissions } from "@/db/schema"; import { and, eq } from "drizzle-orm"; import { revalidatePath } from "next/cache"; import { redirect } from "next/navigation"; import { requireAdmin } from "@/lib/auth/auth"; import { sanitizeHtml } from "@/lib/sanitize-html"; import { sse } from "@/lib/realtime/sse"; import { isValidDiscordWebhookUrl } from "@/lib/discord/discord-webhook-settings"; export async function createForm(formData: FormData) { await requireAdmin(); const title = formData.get("title") as string; const description = formData.get("description") as string | null; const [form] = await db .insert(forms) .values({ title, description: description ? sanitizeHtml(description) : null }) .returning(); void sse.forms.pub("update", { formId: form.id, entity: "form", action: "created", }); revalidatePath("/admin/form"); redirect(`/admin/form/${form.id}`); } export async function createDefaultForm() { await requireAdmin(); const [form] = await db .insert(forms) .values({ title: "New Form", description: "" }) .returning(); void sse.forms.pub("update", { formId: form.id, entity: "form", action: "created", }); revalidatePath("/admin/form"); redirect(`/admin/form/${form.id}`); } export async function updateForm(id: string, data: { title?: string; description?: string | null; isOpen?: boolean; closesAt?: string | null; allowMultipleSubmissions?: boolean; discordWebhookUrl?: string | null; discordWebhookTemplate?: string | null; discordWebhookUpdateTemplate?: string | null; allowedRoles?: string[]; deniedRoles?: string[] }) { await requireAdmin(); if (data.title !== undefined && !data.title.trim()) { throw new Error("Form title is required."); } if ( data.discordWebhookUrl && !isValidDiscordWebhookUrl(data.discordWebhookUrl) ) { throw new Error("Enter a valid Discord webhook URL."); } const closesAt = data.closesAt === undefined ? undefined : data.closesAt === null ? null : new Date(data.closesAt); if (closesAt instanceof Date && Number.isNaN(closesAt.getTime())) { throw new Error("Enter a valid closing date and time."); } const nextData = { ...data, closesAt, description: data.description === undefined ? undefined : data.description ? sanitizeHtml(data.description) : data.description, }; await db.update(forms).set(nextData).where(eq(forms.id, id)); void sse.forms.pub("update", { formId: id, entity: "form", action: "updated", }); revalidatePath("/form"); revalidatePath("/admin/form"); } export async function updateSubmissionAdminNote( formId: string, submissionId: string, note: string ) { await requireAdmin(); if ( typeof formId !== "string" || !formId || typeof submissionId !== "string" || !submissionId || typeof note !== "string" || note.length > 10_000 ) { throw new Error("Invalid admin note."); } const [updatedSubmission] = await db .update(submissions) .set({ adminNote: note.trim() || null }) .where( and( eq(submissions.id, submissionId), eq(submissions.formId, formId) ) ) .returning({ id: submissions.id }); if (!updatedSubmission) { throw new Error("Submission not found."); } revalidatePath(`/admin/form/${formId}/result`); revalidatePath(`/admin/form/${formId}/result/${submissionId}`); return { ok: true }; } export async function testDiscordWebhook(url: string, content: string) { await requireAdmin(); const trimmedUrl = url.trim(); if (!isValidDiscordWebhookUrl(trimmedUrl)) { throw new Error("Enter a valid Discord webhook URL."); } if (!content.trim()) { throw new Error("The test message is empty."); } const response = await fetch(trimmedUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ content: content.slice(0, 2000) }), }); if (!response.ok) { throw new Error(`Discord rejected the test message (${response.status}).`); } return { ok: true }; } export async function deleteForm(id: string) { await requireAdmin(); await db.delete(forms).where(eq(forms.id, id)); void sse.forms.pub("update", { formId: id, entity: "form", action: "deleted", }); revalidatePath("/admin/form"); }