Files
erika/app/actions/submissions.ts
T

195 lines
6.0 KiB
TypeScript

"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 };
}