"use server"; import { db } from "@/db"; import { submissions } from "@/db/schema"; import { and, eq } from "drizzle-orm"; import { revalidatePath } from "next/cache"; import { getServerSession } from "next-auth"; import { authOptions } from "@/lib/auth/auth-options"; import { requireAdmin, requireDiscordId, requireFormAccess } from "@/lib/auth/auth"; import { sse } from "@/lib/realtime/sse"; import type { FormAnswerPayload } from "@/lib/form/form-answer-values"; import { normalizeSubmissionAnswers, } from "@/lib/form/submission-intake"; import { writeSubmission } from "@/lib/form/submission-write"; import { submissionWebhookMessage } from "@/lib/form/submission-webhook"; import { isFormAcceptingSubmissions } from "@/lib/form/form-availability"; import { getCanonicalUrl } from "@/lib/config/server"; function getSubmissionResultLink(formId: string, submissionId: string) { return new URL( `/admin/form/${formId}/result/${submissionId}`, getCanonicalUrl(), ).toString(); } export async function submitForm( formId: string, answersList: FormAnswerPayload[], targetSubmissionId?: string | null ) { const session = await getServerSession(authOptions); if (!session?.user) throw new Error("Not authenticated"); const discordId = await requireDiscordId(); const userName = session.user.name ?? null; const form = await requireFormAccess(formId, discordId); if (!isFormAcceptingSubmissions(form)) { throw new Error("This form is closed for submissions"); } const questionsList = await db.query.questions.findMany({ where: (question, { eq }) => eq(question.formId, formId), orderBy: (question, { asc }) => asc(question.displayOrder), }); if (questionsList.length === 0) { throw new Error("แบบฟอร์มนี้ยังไม่มีคำถาม"); } const normalizedAnswers = normalizeSubmissionAnswers(questionsList, answersList); const imageCdnIds = [ ...new Set(normalizedAnswers.map((answer) => answer.imageCdnId).filter(Boolean)), ] as string[]; if (imageCdnIds.length > 0) { const ownedImages = await db.query.cdn.findMany({ where: (record, { and, eq, inArray }) => and( inArray(record.id, imageCdnIds), eq(record.ownerDiscordId, discordId) ), columns: { id: true }, }); if (ownedImages.length !== imageCdnIds.length) { throw new Error("ข้อมูลรูปภาพไม่ถูกต้อง"); } } const result = await writeSubmission({ formId, discordId, userName, allowMultipleSubmissions: form.allowMultipleSubmissions, normalizedAnswers, targetSubmissionId, }); // Send Discord Webhook const templateToUse = result.isUpdate ? form.discordWebhookUpdateTemplate : form.discordWebhookTemplate; if (form.discordWebhookUrl) { try { const submissionLink = getSubmissionResultLink(formId, result.submissionId); const message = submissionWebhookMessage({ template: templateToUse, isUpdate: result.isUpdate, userName: session.user.name, discordId, submissionLink, questions: questionsList, answers: normalizedAnswers, oldAnswersByQuestionId: result.oldAnswersDict, }); const response = await fetch(form.discordWebhookUrl, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ content: message, }), }); if (!response.ok) { const responseBody = await response.text(); console.error("Discord webhook rejected submission notification", { status: response.status, statusText: response.statusText, response: responseBody.slice(0, 500), }); } } catch (e) { console.error("Failed to send webhook", e); } } void sse.submissions.pub("update", { formId, submissionId: result.submissionId, action: result.isUpdate ? "updated" : "created", }); revalidatePath(`/admin/form/${formId}/result`); return { ok: true, totalAnswers: result.totalAnswers, queueNumber: result.queueNumber, }; } export async function deleteSubmission(id: string) { await requireAdmin(); const submission = await db.query.submissions.findFirst({ where: (submission, { eq }) => eq(submission.id, id), columns: { formId: true }, }); await db.delete(submissions).where(eq(submissions.id, id)); if (submission) { void sse.submissions.pub("update", { formId: submission.formId, submissionId: id, action: "deleted", }); } revalidatePath("/admin/form"); return { ok: true }; } export async function deleteAllSubmissions(formId: string) { await requireAdmin(); await db.delete(submissions).where(eq(submissions.formId, formId)); void sse.submissions.pub("update", { formId, action: "deleted", }); revalidatePath(`/admin/form/${formId}/result`); return { ok: true }; } export async function deleteOwnSubmission(formId: string, submissionId?: string | null) { const discordId = await requireDiscordId(); const form = await requireFormAccess(formId, discordId); if (form.allowMultipleSubmissions && !submissionId) { throw new Error("ไม่พบคำตอบที่ต้องการลบ"); } const where = form.allowMultipleSubmissions ? and( eq(submissions.id, submissionId!), eq(submissions.formId, formId), eq(submissions.userDiscordId, discordId) ) : and( eq(submissions.formId, formId), eq(submissions.userDiscordId, discordId) ); const deleted = await db.delete(submissions).where(where).returning({ id: submissions.id }); if (form.allowMultipleSubmissions && deleted.length === 0) { throw new Error("ไม่พบคำตอบที่ต้องการลบ"); } void sse.submissions.pub("update", { formId, submissionId: submissionId ?? undefined, action: "deleted", }); revalidatePath(`/form`); revalidatePath(`/admin/form/${formId}/result`); return { ok: true }; }