import { NextRequest, NextResponse } from "next/server"; import { headers } from "next/headers"; import { auth } from "@/lib/auth"; import { prisma } from "@/lib/prisma"; import { sendQuestionFollowUpEmail, sendQuestionReplyEmail } from "@/lib/email"; interface FileAttachment { name: string; url: string; size: number; } function buildS3Prefix(): string { const endpoint = process.env.S3_ENDPOINT ?? ""; const bucket = process.env.S3_BUCKET ?? ""; // e.g. https://fsn1.your-objectstorage.com/lms-uploads/ return `${endpoint}/${bucket}/`; } export async function POST( req: NextRequest, { params }: { params: Promise<{ id: string }> } ) { const session = await auth.api.getSession({ headers: await headers() }); if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); const { id } = await params; const isStaff = session.user.role === "admin" || session.user.role === "curator"; const question = await prisma.studentQuestion.findUnique({ where: { id }, include: { user: { select: { id: true, name: true, email: true } } }, }); if (!question) return NextResponse.json({ error: "Not found" }, { status: 404 }); if (!isStaff && question.userId !== session.user.id) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); } if (question.status === "CLOSED") { return NextResponse.json({ error: "Question is closed" }, { status: 409 }); } let body: unknown; try { body = await req.json(); } catch { return NextResponse.json({ error: "Invalid JSON" }, { status: 400 }); } const { text, files } = body as { text: string; files?: FileAttachment[] }; if (!text?.trim()) { return NextResponse.json({ error: "text is required" }, { status: 400 }); } const s3Prefix = buildS3Prefix(); const safeFiles = files ?.filter( (f) => typeof f.name === "string" && typeof f.url === "string" && f.url.startsWith("https://") && f.url.startsWith(s3Prefix) && typeof f.size === "number" ) .map((f) => ({ name: f.name.slice(0, 255), url: f.url, size: Math.max(0, f.size) })); const [msg] = await prisma.$transaction([ prisma.studentQuestionMessage.create({ data: { questionId: id, authorId: session.user.id, text: text.trim(), files: safeFiles?.length ? (safeFiles as object[]) : undefined, }, include: { author: { select: { id: true, name: true, role: true } } }, }), prisma.studentQuestion.update({ where: { id }, data: { updatedAt: new Date() }, }), ]); // Send notifications (fire-and-forget, outside transaction) if (isStaff) { void sendQuestionReplyEmail( question.user.email, question.user.name, question.title, id, ); } else { const staff = await prisma.user.findMany({ where: { role: { in: ["admin", "curator"] } }, select: { email: true, name: true }, }); void Promise.all( staff.map((s) => sendQuestionFollowUpEmail(s.email, s.name, session.user.name, question.title) ) ); } return NextResponse.json(msg, { status: 201 }); }