Files
lms-sb/src/app/api/questions/route.ts
T
admins e5ba94cb33 Fix security, transaction, and badge issues from final review
- Validate file URLs against S3 prefix in messages route (Fix 1)
- Guard attachment hrefs with https:// check in QuestionThread and QuestionSplitView (Fix 2)
- Wrap message create + updatedAt bump in prisma.$transaction (Fix 3)
- Add questionsBadge count query to curator layout for admin branch (Fix 4)
- Fire-and-forget email sends with void Promise.all (Fix 5)
- Wrap req.json() calls in try/catch returning 400 on parse failure (Fix 6)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 13:56:31 +05:00

98 lines
2.7 KiB
TypeScript

import { NextRequest, NextResponse } from "next/server";
import { headers } from "next/headers";
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { sendQuestionCreatedEmail } from "@/lib/email";
export async function GET() {
const session = await auth.api.getSession({ headers: await headers() });
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const isStaff = session.user.role === "admin" || session.user.role === "curator";
const userSelect = isStaff
? { id: true as const, name: true as const, email: true as const }
: { id: true as const, name: true as const };
const questions = await prisma.studentQuestion.findMany({
where: isStaff ? undefined : { userId: session.user.id },
include: {
user: { select: userSelect },
course: { select: { id: true, title: true } },
_count: {
select: {
messages: {
where: isStaff
? { isRead: false, author: { role: "student" } }
: { isRead: false, NOT: { authorId: session.user.id } },
},
},
},
},
orderBy: { updatedAt: "desc" },
});
return NextResponse.json(
questions.map((q) => ({
id: q.id,
title: q.title,
status: q.status,
createdAt: q.createdAt,
updatedAt: q.updatedAt,
user: q.user,
course: q.course,
unreadCount: q._count.messages,
}))
);
}
export async function POST(req: NextRequest) {
const session = await auth.api.getSession({ headers: await headers() });
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
if (session.user.role !== "student") {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
let body: unknown;
try {
body = await req.json();
} catch {
return NextResponse.json({ error: "Invalid JSON" }, { status: 400 });
}
const { title, text, courseId } = body as {
title: string;
text: string;
courseId?: string;
};
if (!title?.trim() || !text?.trim()) {
return NextResponse.json({ error: "title and text are required" }, { status: 400 });
}
const question = await prisma.studentQuestion.create({
data: {
userId: session.user.id,
courseId: courseId ?? null,
title: title.trim(),
messages: {
create: {
authorId: session.user.id,
text: text.trim(),
},
},
},
});
const staff = await prisma.user.findMany({
where: { role: { in: ["admin", "curator"] } },
select: { email: true, name: true },
});
void Promise.all(
staff.map((s) =>
sendQuestionCreatedEmail(s.email, s.name, session.user.name, title.trim())
)
);
return NextResponse.json(question, { status: 201 });
}