import { Body, Controller, Delete, Get, Param, Post, Put, Req, } from "@nestjs/common"; import { z } from "zod"; import { ExamsService, type CreateExamInput, type UpdateExamInput, } from "./exams.service.js"; import { Permissions, RequirePermission, } from "../middleware/permission.guard.js"; import type { AuthenticatedRequest } from "../middleware/auth.middleware.js"; import { UnauthorizedError, ValidationError, } from "../shared/errors/application-error.js"; const createExamSchema = z.object({ classId: z.string().min(1), subjectId: z.string().min(1), title: z.string().min(1).max(200), description: z.string().optional(), examDate: z.string().min(1), duration: z.number().int().positive(), totalScore: z.string().min(1), schoolId: z.string().min(1), }); const updateExamSchema = z.object({ title: z.string().min(1).max(200).optional(), description: z.string().optional(), examDate: z.string().min(1).optional(), duration: z.number().int().positive().optional(), totalScore: z.string().min(1).optional(), }); const answerInputSchema = z.object({ questionId: z.string().min(1), answer: z.string(), }); const scoreInputSchema = z.object({ questionId: z.string().min(1), score: z.string().min(1), teacherComment: z.string().optional(), }); const submitExamSchema = z.object({ studentId: z.string().min(1), answers: z.array(answerInputSchema).default([]), }); const gradeExamSchema = z.object({ submissionId: z.string().min(1), scores: z.array(scoreInputSchema).min(1), }); @Controller("v1/exams") export class ExamsController { constructor(private readonly examsService: ExamsService) {} @Post() @RequirePermission(Permissions.EXAM_CREATE) async create( @Body() body: unknown, @Req() req: AuthenticatedRequest, ): Promise<{ success: true; data: { id: string } }> { const userId = req.userId; if (!userId) { throw new UnauthorizedError("Missing x-user-id header"); } const parsed = createExamSchema.safeParse(body); if (!parsed.success) { throw new ValidationError("Invalid exam input", parsed.error.flatten()); } const input: CreateExamInput = { ...parsed.data, examDate: new Date(parsed.data.examDate), createdBy: userId, }; const result = await this.examsService.createExam(input, userId); return { success: true, data: result }; } @Get(":id") @RequirePermission(Permissions.EXAM_READ) async findOne(@Param("id") id: string): Promise<{ success: true; data: Awaited>; }> { const data = await this.examsService.getExam(id); return { success: true, data }; } @Get("class/:classId") @RequirePermission(Permissions.EXAM_READ) async listByClass(@Param("classId") classId: string): Promise<{ success: true; data: Awaited>; }> { const data = await this.examsService.listExamsByClass(classId); return { success: true, data }; } @Put(":id") @RequirePermission(Permissions.EXAM_UPDATE) async update( @Param("id") id: string, @Body() body: unknown, ): Promise<{ success: true; data: { success: true } }> { const parsed = updateExamSchema.safeParse(body); if (!parsed.success) { throw new ValidationError( "Invalid exam update input", parsed.error.flatten(), ); } const data: UpdateExamInput = { ...parsed.data, examDate: parsed.data.examDate ? new Date(parsed.data.examDate) : undefined, }; await this.examsService.updateExam(id, data); return { success: true, data: { success: true } }; } @Delete(":id") @RequirePermission(Permissions.EXAM_DELETE) async remove( @Param("id") id: string, ): Promise<{ success: true; data: { success: true } }> { await this.examsService.deleteExam(id); return { success: true, data: { success: true } }; } @Post(":id/publish") @RequirePermission(Permissions.EXAM_PUBLISH) async publish( @Param("id") id: string, @Req() req: AuthenticatedRequest, ): Promise<{ success: true; data: { success: true } }> { const userId = req.userId; if (!userId) { throw new UnauthorizedError("Missing x-user-id header"); } await this.examsService.publishExam(id, userId); return { success: true, data: { success: true } }; } @Post(":id/submit") @RequirePermission(Permissions.EXAM_SUBMIT) async submit( @Param("id") id: string, @Body() body: unknown, ): Promise<{ success: true; data: { submissionId: string } }> { const parsed = submitExamSchema.safeParse(body); if (!parsed.success) { throw new ValidationError("Invalid submit input", parsed.error.flatten()); } const result = await this.examsService.submitExam( id, parsed.data.studentId, parsed.data.answers, ); return { success: true, data: result }; } @Post(":id/grade") @RequirePermission(Permissions.EXAM_GRADE) async grade( @Param("id") id: string, @Body() body: unknown, @Req() req: AuthenticatedRequest, ): Promise<{ success: true; data: { totalScore: string } }> { const userId = req.userId; if (!userId) { throw new UnauthorizedError("Missing x-user-id header"); } const parsed = gradeExamSchema.safeParse(body); if (!parsed.success) { throw new ValidationError("Invalid grade input", parsed.error.flatten()); } const result = await this.examsService.gradeExam( id, parsed.data.submissionId, parsed.data.scores, userId, ); return { success: true, data: result }; } @Post(":id/archive") @RequirePermission(Permissions.EXAM_UPDATE) async archive( @Param("id") id: string, @Req() req: AuthenticatedRequest, ): Promise<{ success: true; data: { success: true } }> { const userId = req.userId; if (!userId) { throw new UnauthorizedError("Missing x-user-id header"); } await this.examsService.archiveExam(id, userId); return { success: true, data: { success: true } }; } }