186 lines
5.5 KiB
TypeScript
186 lines
5.5 KiB
TypeScript
"use server";
|
|
|
|
import { db } from "@/db";
|
|
import { submissions, answers } from "@/db/schema";
|
|
import { eq } from "drizzle-orm";
|
|
import { revalidatePath } from "next/cache";
|
|
import { getServerSession } from "next-auth";
|
|
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
|
|
import { requireAdmin, requireDiscordId, requireFormAccess } from "@/lib/auth";
|
|
|
|
export async function submitForm(
|
|
formId: string,
|
|
answersList: { questionId: string; value: string }[]
|
|
) {
|
|
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 (!form.isOpen) throw new Error("This form is disabled for now");
|
|
|
|
const result = await db.transaction(async (tx) => {
|
|
const existingSubmission = await tx.query.submissions.findFirst({
|
|
where: (submission, { eq, and }) =>
|
|
and(eq(submission.formId, formId), eq(submission.userDiscordId, discordId)),
|
|
});
|
|
|
|
let submissionId = "";
|
|
const isUpdate = Boolean(existingSubmission);
|
|
let oldAnswersDict: Record<string, string> = {};
|
|
|
|
if (existingSubmission) {
|
|
submissionId = existingSubmission.id;
|
|
|
|
const oldAnswersList = await tx.query.answers.findMany({
|
|
where: (answer, { eq }) => eq(answer.submissionId, submissionId),
|
|
});
|
|
|
|
oldAnswersDict = oldAnswersList.reduce<Record<string, string>>((acc, curr) => {
|
|
acc[curr.questionId] = curr.value;
|
|
return acc;
|
|
}, {});
|
|
|
|
const history = Array.isArray(existingSubmission.editHistory)
|
|
? [...existingSubmission.editHistory]
|
|
: [];
|
|
history.push({
|
|
editedAt: new Date().toISOString(),
|
|
oldAnswers: oldAnswersDict,
|
|
});
|
|
|
|
await tx
|
|
.update(submissions)
|
|
.set({ editHistory: history })
|
|
.where(eq(submissions.id, submissionId));
|
|
|
|
await tx.delete(answers).where(eq(answers.submissionId, submissionId));
|
|
} else {
|
|
const [inserted] = await tx
|
|
.insert(submissions)
|
|
.values({
|
|
formId,
|
|
userDiscordId: discordId,
|
|
userName,
|
|
})
|
|
.returning();
|
|
submissionId = inserted.id;
|
|
}
|
|
|
|
if (answersList.length > 0) {
|
|
await tx.insert(answers).values(
|
|
answersList.map((answer) => ({
|
|
submissionId,
|
|
questionId: answer.questionId,
|
|
value: answer.value,
|
|
}))
|
|
);
|
|
}
|
|
|
|
return { submissionId, isUpdate, oldAnswersDict };
|
|
});
|
|
|
|
// Send Discord Webhook
|
|
const templateToUse = result.isUpdate
|
|
? form.discordWebhookUpdateTemplate
|
|
: form.discordWebhookTemplate;
|
|
|
|
if (form.discordWebhookUrl && templateToUse) {
|
|
try {
|
|
const questionsList = await db.query.questions.findMany({
|
|
where: (q, { eq }) => eq(q.formId, formId),
|
|
orderBy: (q, { asc }) => asc(q.displayOrder)
|
|
});
|
|
const questionMap = questionsList.reduce((acc, q, index) => {
|
|
acc[q.id] = String(index + 1);
|
|
return acc;
|
|
}, {} as Record<string, string>);
|
|
|
|
const answersDict = answersList.reduce((acc, a) => {
|
|
const idx = questionMap[a.questionId];
|
|
if (idx) {
|
|
acc[idx] = a.value;
|
|
}
|
|
return acc;
|
|
}, {} as Record<string, string>);
|
|
|
|
let updatesText = "";
|
|
if (result.isUpdate) {
|
|
const changed: string[] = [];
|
|
for (const ans of answersList) {
|
|
const oldVal = result.oldAnswersDict[ans.questionId];
|
|
if (oldVal !== ans.value) {
|
|
const qTitle = questionsList.find(q => q.id === ans.questionId)?.label || `Question`;
|
|
changed.push(`**${qTitle}**: \`${oldVal || "Empty"}\` ➡️ \`${ans.value || "Empty"}\``);
|
|
}
|
|
}
|
|
updatesText = changed.length > 0 ? changed.join("\n") : "No changes made.";
|
|
}
|
|
|
|
let message = templateToUse;
|
|
// Replace {1}, {2}, etc. with value, and new variables
|
|
message = message.replace(/\{([^}]+)\}/g, (match: string, p1: string) => {
|
|
const key = p1.toLowerCase();
|
|
if (key === "username" || key === "displayname") {
|
|
return session.user?.name || "Unknown User";
|
|
}
|
|
if (key === "id") {
|
|
return discordId || "Unknown ID";
|
|
}
|
|
if (key === "update") {
|
|
return updatesText;
|
|
}
|
|
return answersDict[p1] || "Not Provided";
|
|
});
|
|
|
|
await fetch(form.discordWebhookUrl, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
content: message,
|
|
}),
|
|
});
|
|
} catch (e) {
|
|
console.error("Failed to send webhook", e);
|
|
}
|
|
}
|
|
|
|
revalidatePath(`/admin/form/${formId}/result`);
|
|
return { ok: true };
|
|
}
|
|
|
|
export async function deleteSubmission(id: string) {
|
|
await requireAdmin();
|
|
|
|
await db.delete(submissions).where(eq(submissions.id, id));
|
|
revalidatePath("/admin/form");
|
|
return { ok: true };
|
|
}
|
|
|
|
export async function deleteAllSubmissions(formId: string) {
|
|
await requireAdmin();
|
|
|
|
await db.delete(submissions).where(eq(submissions.formId, formId));
|
|
revalidatePath(`/admin/form/${formId}/result`);
|
|
return { ok: true };
|
|
}
|
|
|
|
export async function deleteOwnSubmission(formId: string) {
|
|
const discordId = await requireDiscordId();
|
|
|
|
const { and } = await import("drizzle-orm");
|
|
|
|
await db.delete(submissions).where(
|
|
and(
|
|
eq(submissions.formId, formId),
|
|
eq(submissions.userDiscordId, discordId)
|
|
)
|
|
);
|
|
revalidatePath(`/form`);
|
|
revalidatePath(`/admin/form/${formId}/result`);
|
|
return { ok: true };
|
|
}
|