201 lines
4.7 KiB
TypeScript
201 lines
4.7 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import { db } from "@/db";
|
|
import { quizSessions } from "@/db/schema";
|
|
import { eq, and } from "drizzle-orm";
|
|
import { getSession } from "@/lib/auth";
|
|
import {
|
|
drawQuizQuestions,
|
|
QUIZ_QUESTIONS,
|
|
type QuizQuestion,
|
|
} from "@/lib/quiz-questions";
|
|
|
|
const QUIZ_SESSION_EXPIRY_MS = 2 * 60 * 60 * 1000; // 2 hours (for in-progress quiz attempts only)
|
|
|
|
interface StoredAnswer {
|
|
questionId: string;
|
|
answer: string;
|
|
correct: boolean;
|
|
}
|
|
|
|
export async function GET() {
|
|
const session = await getSession();
|
|
if (!session) {
|
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
}
|
|
|
|
// Quiz only needs to be passed once — no time-based expiry
|
|
const [passedQuiz] = await db
|
|
.select()
|
|
.from(quizSessions)
|
|
.where(
|
|
and(
|
|
eq(quizSessions.annotatorId, session.annotatorId),
|
|
eq(quizSessions.passed, true),
|
|
),
|
|
)
|
|
.orderBy(quizSessions.startedAt)
|
|
.limit(1);
|
|
|
|
if (passedQuiz) {
|
|
return NextResponse.json({
|
|
hasPassedQuiz: true,
|
|
quizSessionId: passedQuiz.id,
|
|
});
|
|
}
|
|
|
|
return NextResponse.json({ hasPassedQuiz: false });
|
|
}
|
|
|
|
export async function POST(request: Request) {
|
|
const session = await getSession();
|
|
if (!session) {
|
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
}
|
|
|
|
const body = await request.json();
|
|
const { action } = body as { action: string };
|
|
|
|
if (action === "start") {
|
|
return handleStart(session.annotatorId);
|
|
}
|
|
|
|
if (action === "answer") {
|
|
const { quizSessionId, questionId, answer } = body as {
|
|
quizSessionId: string;
|
|
questionId: string;
|
|
answer: string;
|
|
};
|
|
|
|
if (!quizSessionId || !questionId || !answer) {
|
|
return NextResponse.json(
|
|
{ error: "Missing required fields" },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
return handleAnswer(
|
|
session.annotatorId,
|
|
quizSessionId,
|
|
questionId,
|
|
answer,
|
|
);
|
|
}
|
|
|
|
return NextResponse.json({ error: "Invalid action" }, { status: 400 });
|
|
}
|
|
|
|
async function handleStart(annotatorId: string) {
|
|
const questions = drawQuizQuestions(8);
|
|
const quizSessionId = crypto.randomUUID();
|
|
|
|
await db.insert(quizSessions).values({
|
|
id: quizSessionId,
|
|
annotatorId,
|
|
startedAt: new Date(),
|
|
totalQuestions: 8,
|
|
answers: "[]",
|
|
});
|
|
|
|
return NextResponse.json({ quizSessionId, questions });
|
|
}
|
|
|
|
async function handleAnswer(
|
|
annotatorId: string,
|
|
quizSessionId: string,
|
|
questionId: string,
|
|
answer: string,
|
|
) {
|
|
const [quizSession] = await db
|
|
.select()
|
|
.from(quizSessions)
|
|
.where(eq(quizSessions.id, quizSessionId))
|
|
.limit(1);
|
|
|
|
if (!quizSession) {
|
|
return NextResponse.json(
|
|
{ error: "Quiz session not found" },
|
|
{ status: 404 },
|
|
);
|
|
}
|
|
|
|
if (quizSession.annotatorId !== annotatorId) {
|
|
return NextResponse.json({ error: "Unauthorized" }, { status: 403 });
|
|
}
|
|
|
|
if (quizSession.completedAt) {
|
|
return NextResponse.json(
|
|
{ error: "Quiz already completed" },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
const elapsed = Date.now() - quizSession.startedAt.getTime();
|
|
if (elapsed > QUIZ_SESSION_EXPIRY_MS) {
|
|
return NextResponse.json({ error: "Quiz session expired" }, { status: 400 });
|
|
}
|
|
|
|
const question = QUIZ_QUESTIONS.find(
|
|
(q: QuizQuestion) => q.id === questionId,
|
|
);
|
|
if (!question) {
|
|
return NextResponse.json({ error: "Question not found" }, { status: 404 });
|
|
}
|
|
|
|
const existingAnswers: StoredAnswer[] = JSON.parse(quizSession.answers);
|
|
|
|
const alreadyAnswered = existingAnswers.some(
|
|
(a) => a.questionId === questionId,
|
|
);
|
|
if (alreadyAnswered) {
|
|
return NextResponse.json(
|
|
{ error: "Question already answered" },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
const correct = answer === question.correctAnswer;
|
|
|
|
const updatedAnswers: StoredAnswer[] = [
|
|
...existingAnswers,
|
|
{ questionId, answer, correct },
|
|
];
|
|
|
|
const allAnswered = updatedAnswers.length >= quizSession.totalQuestions;
|
|
|
|
if (allAnswered) {
|
|
const score = updatedAnswers.filter((a) => a.correct).length;
|
|
const passed = score >= 7;
|
|
|
|
await db
|
|
.update(quizSessions)
|
|
.set({
|
|
answers: JSON.stringify(updatedAnswers),
|
|
completedAt: new Date(),
|
|
score,
|
|
passed,
|
|
})
|
|
.where(eq(quizSessions.id, quizSessionId));
|
|
|
|
return NextResponse.json({
|
|
correct,
|
|
correctAnswer: question.correctAnswer,
|
|
explanation: question.explanation,
|
|
score,
|
|
passed,
|
|
completed: true,
|
|
});
|
|
}
|
|
|
|
await db
|
|
.update(quizSessions)
|
|
.set({ answers: JSON.stringify(updatedAnswers) })
|
|
.where(eq(quizSessions.id, quizSessionId));
|
|
|
|
return NextResponse.json({
|
|
correct,
|
|
correctAnswer: question.correctAnswer,
|
|
explanation: question.explanation,
|
|
completed: false,
|
|
});
|
|
}
|