Files
erika/app/admin/form/actions.ts
T

151 lines
4.0 KiB
TypeScript

"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";
import { sanitizeHtml } from "@/lib/sanitize-html";
import { sse } from "@/lib/sse";
import { isValidDiscordWebhookUrl } from "@/lib/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; 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 nextData = {
...data,
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");
}