Fix API routes: closed-question guard, file validation, files sanitization, follow-up email

- Add CLOSED status guard in messages POST (returns 409)
- Add extension allowlist check in upload route + text/x-markdown MIME type
- Sanitize files JSON array before DB write
- Add sendQuestionFollowUpEmail helper and use it for student follow-up replies
- Scope email field to staff only in questions list query

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-19 13:28:08 +05:00
parent f2946db57a
commit a9e6272d2d
4 changed files with 47 additions and 5 deletions
+15 -3
View File
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
import { headers } from "next/headers";
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { sendQuestionCreatedEmail, sendQuestionReplyEmail } from "@/lib/email";
import { sendQuestionFollowUpEmail, sendQuestionReplyEmail } from "@/lib/email";
interface FileAttachment {
name: string;
@@ -29,6 +29,9 @@ export async function POST(
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 });
}
const body = await req.json();
const { text, files } = body as { text: string; files?: FileAttachment[] };
@@ -37,12 +40,21 @@ export async function POST(
return NextResponse.json({ error: "text is required" }, { status: 400 });
}
const safeFiles = files
?.filter(
(f) =>
typeof f.name === "string" &&
typeof f.url === "string" &&
typeof f.size === "number"
)
.map((f) => ({ name: f.name.slice(0, 255), url: f.url, size: Math.max(0, f.size) }));
const message = await prisma.studentQuestionMessage.create({
data: {
questionId: id,
authorId: session.user.id,
text: text.trim(),
files: files?.length ? (files as object[]) : undefined,
files: safeFiles?.length ? (safeFiles as object[]) : undefined,
},
include: { author: { select: { id: true, name: true, role: true } } },
});
@@ -68,7 +80,7 @@ export async function POST(
});
await Promise.all(
staff.map((s) =>
sendQuestionCreatedEmail(s.email, s.name, session.user.name, question.title)
sendQuestionFollowUpEmail(s.email, s.name, session.user.name, question.title)
)
);
}