146 lines
3.5 KiB
TypeScript
146 lines
3.5 KiB
TypeScript
"use server";
|
|
|
|
import { db } from "@/db";
|
|
import { unlink } from "fs/promises";
|
|
import path from "path";
|
|
import { questions } from "@/db/schema";
|
|
import type { QuestionType } 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";
|
|
|
|
async function assertAdmin() {
|
|
const session = await getServerSession(authOptions);
|
|
const adminIds = (process.env.ADMIN_DISCORD_IDS ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
const discordId = (session?.user as { discordId?: string } | undefined)?.discordId;
|
|
if (!discordId || !adminIds.includes(discordId)) {
|
|
throw new Error("Unauthorized");
|
|
}
|
|
}
|
|
|
|
export async function createQuestion(
|
|
formId: string,
|
|
data: {
|
|
type: QuestionType;
|
|
label: string;
|
|
required: boolean;
|
|
displayOrder: number;
|
|
options?: string[];
|
|
}
|
|
) {
|
|
await assertAdmin();
|
|
await db.insert(questions).values({
|
|
formId,
|
|
type: data.type,
|
|
label: data.label,
|
|
required: data.required,
|
|
displayOrder: data.displayOrder,
|
|
options: data.options ?? [],
|
|
});
|
|
revalidatePath("/form");
|
|
revalidatePath("/form/admin");
|
|
}
|
|
|
|
export async function updateQuestion(
|
|
id: string,
|
|
formId: string,
|
|
data: Partial<{
|
|
label: string;
|
|
type: QuestionType;
|
|
required: boolean;
|
|
displayOrder: number;
|
|
options: string[];
|
|
imageUrl: string | null;
|
|
}>
|
|
) {
|
|
await assertAdmin();
|
|
if (data.imageUrl !== undefined) {
|
|
const q = await db.query.questions.findFirst({
|
|
where: (q, { eq }) => eq(q.id, id),
|
|
});
|
|
if (q?.imageUrl && q.imageUrl !== data.imageUrl) {
|
|
await deleteImageFile(q.imageUrl);
|
|
}
|
|
}
|
|
await db.update(questions).set(data).where(eq(questions.id, id));
|
|
revalidatePath("/form");
|
|
revalidatePath("/form/admin");
|
|
}
|
|
|
|
export async function bulkUpdateQuestions(
|
|
formId: string,
|
|
updates: {
|
|
id: string;
|
|
label: string;
|
|
type: QuestionType;
|
|
required: boolean;
|
|
displayOrder: number;
|
|
options: string[];
|
|
}[]
|
|
) {
|
|
await assertAdmin();
|
|
|
|
// Use Promise.all to update all questions concurrently
|
|
await Promise.all(
|
|
updates.map((u) =>
|
|
db
|
|
.update(questions)
|
|
.set({
|
|
label: u.label,
|
|
type: u.type,
|
|
required: u.required,
|
|
displayOrder: u.displayOrder,
|
|
options: u.options ?? [],
|
|
})
|
|
.where(eq(questions.id, u.id))
|
|
)
|
|
);
|
|
|
|
revalidatePath("/form");
|
|
revalidatePath("/form/admin");
|
|
}
|
|
|
|
async function deleteImageFile(imageUrl: string | null) {
|
|
if (!imageUrl) return;
|
|
try {
|
|
const filename = imageUrl.split("/").pop();
|
|
if (filename) {
|
|
const filepath = path.join(process.cwd(), "public", "form", filename);
|
|
await unlink(filepath);
|
|
}
|
|
} catch (err) {
|
|
console.error("Failed to delete image file", err);
|
|
}
|
|
}
|
|
|
|
export async function deleteQuestion(id: string, formId: string) {
|
|
await assertAdmin();
|
|
const q = await db.query.questions.findFirst({
|
|
where: (q, { eq }) => eq(q.id, id),
|
|
});
|
|
if (q?.imageUrl) {
|
|
await deleteImageFile(q.imageUrl);
|
|
}
|
|
await db.delete(questions).where(eq(questions.id, id));
|
|
revalidatePath("/form");
|
|
revalidatePath("/form/admin");
|
|
}
|
|
|
|
export async function reorderQuestions(
|
|
formId: string,
|
|
orderedIds: string[]
|
|
) {
|
|
await assertAdmin();
|
|
await Promise.all(
|
|
orderedIds.map((id, index) =>
|
|
db
|
|
.update(questions)
|
|
.set({ displayOrder: index })
|
|
.where(eq(questions.id, id))
|
|
)
|
|
);
|
|
revalidatePath("/form");
|
|
revalidatePath("/form/admin");
|
|
}
|