From e6b7cd51e27c648638d35201616be014696b2d7d Mon Sep 17 00:00:00 2001 From: Uyoxy Date: Thu, 27 Feb 2025 03:21:34 +0100 Subject: [PATCH] Implement an update EXAM CRUD ENDPOINT --- config/config.js | 23 +++---- controllers/exam.controller.js | 75 +++++++++++++++++++++++ entities/exam.entity.js | 29 +++++++++ entities/examRegistration.entity.js | 36 +++++++++++ http/exam.http | 52 ++++++++++++++++ middlewares/auth.middleware.js | 38 ++++++++---- routes/exam.routes.js | 35 +++++++++++ routes/examRoutes.js | 0 server.js | 1 + services/examServices.js | 94 +++++++++++++++++++++++++++++ 10 files changed, 360 insertions(+), 23 deletions(-) create mode 100644 entities/exam.entity.js create mode 100644 entities/examRegistration.entity.js create mode 100644 http/exam.http create mode 100644 routes/exam.routes.js delete mode 100644 routes/examRoutes.js diff --git a/config/config.js b/config/config.js index 8f74b2e..2c7d2f0 100644 --- a/config/config.js +++ b/config/config.js @@ -1,27 +1,28 @@ -import dotenv from "dotenv" +import dotenv from "dotenv"; import { DataSource } from "typeorm"; -import { Course } from "../entities/course.entity.js" -import { UserCourse } from "../entities/userCourse.entity.js" -import UserEntity from "../entities/user.entity.js"; -import Job from "../entities/job.entity.js"; +import { Course } from "../entities/course.entity.js"; +import { UserCourse } from "../entities/userCourse.entity.js"; +import UserEntity from "../entities/user.entity.js"; +import Job from "../entities/job.entity.js"; import Notification from "../entities/notification.entity.js"; import { Payment } from "../entities/payment.entity.js"; +import { Exam } from "../entities/exam.entity.js"; +import { ExamRegistration } from "../entities/examRegistration.entity.js"; -dotenv.config() +dotenv.config(); const AppDataSource = new DataSource({ type: "postgres", - host: process.env.DB_HOST, - port: Number.parseInt(process.env.DB_PORT || '5432'), + host: process.env.DB_HOST, + port: Number(process.env.DB_PORT), username: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, synchronize: true, logging: true, - entities: [UserEntity, Course, UserCourse, Job, Notification, Payment], + entities: [UserEntity, Course, UserCourse, Job, Notification, Payment, Exam, ExamRegistration], subscribers: [], migrations: [], -}) +}); export default AppDataSource; - diff --git a/controllers/exam.controller.js b/controllers/exam.controller.js index e69de29..4ab5488 100644 --- a/controllers/exam.controller.js +++ b/controllers/exam.controller.js @@ -0,0 +1,75 @@ +import { ExamService } from "../services/exam.service.js"; + +class ExamController { + async createExam(req, res) { + try { + const examData = req.body; + const exam = await ExamService.createExam(examData); + res.status(201).json({ success: true, exam }); + } catch (error) { + res.status(error.statusCode || 400).json({ success: false, error: error.message }); + } + } + + async getExams(req, res) { + try { + const exams = await ExamService.getAllExams(); + res.json({ success: true, exams }); + } catch (error) { + res.status(error.statusCode || 400).json({ success: false, error: error.message }); + } + } + + async getExamById(req, res) { + try { + const exam = await ExamService.getExamById(req.params.id); + res.json({ success: true, exam }); + } catch (error) { + res.status(error.statusCode || 400).json({ success: false, error: error.message }); + } + } + + async updateExam(req, res) { + try { + const exam = await ExamService.updateExam(req.params.id, req.body); + res.json({ success: true, exam }); + } catch (error) { + res.status(error.statusCode || 400).json({ success: false, error: error.message }); + } + } + + async deleteExam(req, res) { + try { + await ExamService.deleteExam(req.params.id); + res.json({ success: true, message: "Exam deleted" }); + } catch (error) { + res.status(error.statusCode || 400).json({ success: false, error: error.message }); + } + } + + // Endpoint for students to register and submit answers + async registerExam(req, res) { + try { + const examId = req.params.examId; + const studentId = req.user.id; // set via your authentication middleware + const answers = req.body.answers; // could be an object or array of answers + const registration = await ExamService.registerExam(examId, studentId, answers); + res.status(201).json({ success: true, registration }); + } catch (error) { + res.status(error.statusCode || 400).json({ success: false, error: error.message }); + } + } + + // For admin to fetch all registrations (e.g., registered students and their answers) + async getExamRegistrations(req, res) { + try { + const examId = req.params.examId; + const registrations = await ExamService.getExamRegistrations(examId); + res.json({ success: true, registrations }); + } catch (error) { + res.status(error.statusCode || 400).json({ success: false, error: error.message }); + } + } +} + +export default new ExamController(); diff --git a/entities/exam.entity.js b/entities/exam.entity.js new file mode 100644 index 0000000..2a72a30 --- /dev/null +++ b/entities/exam.entity.js @@ -0,0 +1,29 @@ +import { EntitySchema } from "typeorm"; + +export const Exam = new EntitySchema({ + name: "Exam", + tableName: "exams", + columns: { + id: { + primary: true, + type: "int", + generated: true, + }, + title: { + type: "varchar", + unique: true, // prevents duplicate exam creation + }, + description: { + type: "text", + nullable: true, + }, + createdAt: { + type: "timestamp", + createDate: true, + }, + updatedAt: { + type: "timestamp", + updateDate: true, + }, + }, +}); diff --git a/entities/examRegistration.entity.js b/entities/examRegistration.entity.js new file mode 100644 index 0000000..c375c26 --- /dev/null +++ b/entities/examRegistration.entity.js @@ -0,0 +1,36 @@ +import { EntitySchema } from "typeorm"; + +export const ExamRegistration = new EntitySchema({ + name: "ExamRegistration", + tableName: "exam_registrations", + columns: { + id: { + primary: true, + type: "int", + generated: true, + }, + // Store answers as JSON (or text if you prefer) + answers: { + type: "json", + nullable: true, + }, + registrationDate: { + type: "timestamp", + createDate: true, + }, + }, + relations: { + exam: { + type: "many-to-one", + target: "Exam", + joinColumn: { name: "examId" }, + onDelete: "CASCADE", + }, + student: { + type: "many-to-one", + target: "User", // reusing your User entity + joinColumn: { name: "studentId" }, + onDelete: "CASCADE", + }, + }, +}); diff --git a/http/exam.http b/http/exam.http new file mode 100644 index 0000000..a1894dc --- /dev/null +++ b/http/exam.http @@ -0,0 +1,52 @@ +### Admin - Create New Exam +//kindly fill out afterjwt token has been created +POST http://localhost:5000/api/exams +Authorization: Bearer +Content-Type: application/json + +{ + "title": "Midterm Exam 2025", + "description": "Exam covering chapters 1 to 5." +} + +### Admin - Get All Exams +GET http://localhost:5000/api/exams +Authorization: Bearer +Content-Type: application/json + +### Admin - Get Exam By ID +GET http://localhost:5000/api/exams/1 +Authorization: Bearer +Content-Type: application/json + +### Admin - Update Exam +PUT http://localhost:5000/api/exams/1 +Authorization: Bearer +Content-Type: application/json + +{ + "title": "Midterm Exam 2025 - Updated", + "description": "Updated exam details." +} + +### Admin - Delete Exam +DELETE http://localhost:5000/api/exams/1 +Authorization: Bearer +Content-Type: application/json + +### Student - Register for Exam (Submit Answers) +POST http://localhost:5000/api/exams/1/register +Authorization: Bearer +Content-Type: application/json + +{ + "answers": { + "Q1": "Answer to question 1", + "Q2": "Answer to question 2" + } +} + +### Admin - Get Exam Registrations +GET http://localhost:5000/api/exams/1/registrations +Authorization: Bearer +Content-Type: application/json diff --git a/middlewares/auth.middleware.js b/middlewares/auth.middleware.js index 00440c1..8a2948e 100644 --- a/middlewares/auth.middleware.js +++ b/middlewares/auth.middleware.js @@ -1,19 +1,33 @@ -import jwt from "jsonwebtoken" +import jwt from "jsonwebtoken"; const authenticateToken = (req, res, next) => { - // For now, we'll set a mock user object for all requests + // In production, decode the JWT token here and set req.user accordingly. + // For example: + // const token = req.headers.authorization?.split(" ")[1]; + // jwt.verify(token, process.env.JWT_SECRET, (err, user) => { ... }); + // For now, we use a mock user: req.user = { id: 1, - username: "mockuser", - role: "instructor", - } - next() -} + username: "adminUser", + role: "admin", // change as needed for testing different roles + }; + next(); +}; -const isInstructor = (req, res, next) => { - // For now, we'll assume all users are instructors - next() -} +const isAdmin = (req, res, next) => { + if (req.user && req.user.role === "admin") { + next(); + } else { + return res.status(403).json({ error: "Forbidden: Admins only" }); + } +}; -export { authenticateToken, isInstructor } +const isStudent = (req, res, next) => { + if (req.user && req.user.role === "student") { + next(); + } else { + return res.status(403).json({ error: "Forbidden: Students only" }); + } +}; +export { authenticateToken, isAdmin, isStudent }; diff --git a/routes/exam.routes.js b/routes/exam.routes.js new file mode 100644 index 0000000..aa39bca --- /dev/null +++ b/routes/exam.routes.js @@ -0,0 +1,35 @@ +import express from "express"; +import ExamController from "../controllers/exam.controller.js"; +import { authenticateToken, isAdmin } from "../middlewares/auth.middleware.js"; + +const router = express.Router(); + +// --- Admin routes for exam CRUD --- +router.post("/", authenticateToken, isAdmin, (req, res) => + ExamController.createExam(req, res) +); +router.get("/", authenticateToken, isAdmin, (req, res) => + ExamController.getExams(req, res) +); +router.get("/:id", authenticateToken, isAdmin, (req, res) => + ExamController.getExamById(req, res) +); +router.put("/:id", authenticateToken, isAdmin, (req, res) => + ExamController.updateExam(req, res) +); +router.delete("/:id", authenticateToken, isAdmin, (req, res) => + ExamController.deleteExam(req, res) +); + +// --- Student route for registration/answer submission --- +// (No role middleware so that any authenticated student can register) +router.post("/:examId/register", authenticateToken, (req, res) => + ExamController.registerExam(req, res) +); + +// --- Optional: Admin route to view all registrations --- +router.get("/:examId/registrations", authenticateToken, isAdmin, (req, res) => + ExamController.getExamRegistrations(req, res) +); + +export default router; diff --git a/routes/examRoutes.js b/routes/examRoutes.js deleted file mode 100644 index e69de29..0000000 diff --git a/server.js b/server.js index 2291d05..cc607f2 100644 --- a/server.js +++ b/server.js @@ -24,6 +24,7 @@ AppDataSource.initialize() app.use(json()); // Routes + app.use("/api/exams", examRoutes); app.use('/api/auth', authRouter); app.use('/api/jobs', jobRoutes); app.use("/api/courses", courseRoutes); diff --git a/services/examServices.js b/services/examServices.js index e69de29..5fcdea8 100644 --- a/services/examServices.js +++ b/services/examServices.js @@ -0,0 +1,94 @@ +import AppDataSource from "../config/config.js"; + +const examRepository = AppDataSource.getRepository("Exam"); +const examRegistrationRepository = AppDataSource.getRepository("ExamRegistration"); + +class ExamError extends Error { + constructor(message, statusCode = 400) { + super(message); + this.name = "ExamError"; + this.statusCode = statusCode; + } +} + +export const ExamService = { + async createExam(examData) { + // Prevent duplicate exam creation by checking the title + const existingExam = await examRepository.findOne({ where: { title: examData.title } }); + if (existingExam) { + throw new ExamError("An exam with this title already exists", 409); + } + const exam = examRepository.create(examData); + return await examRepository.save(exam); + }, + + async getAllExams() { + return await examRepository.find(); + }, + + async getExamById(id) { + const exam = await examRepository.findOne({ where: { id } }); + if (!exam) { + throw new ExamError("Exam not found", 404); + } + return exam; + }, + + async updateExam(id, examData) { + const exam = await this.getExamById(id); + if (examData.title && examData.title !== exam.title) { + const duplicateExam = await examRepository.findOne({ where: { title: examData.title } }); + if (duplicateExam) { + throw new ExamError("An exam with this title already exists", 409); + } + } + await examRepository.update(id, examData); + return await this.getExamById(id); + }, + + async deleteExam(id) { + const deleteResult = await examRepository.delete(id); + if (deleteResult.affected === 0) { + throw new ExamError("Exam not found", 404); + } + }, + + async registerExam(examId, studentId, answers) { + // Prevent duplicate registration for the same exam/student + const existingRegistration = await examRegistrationRepository.findOne({ + where: { + exam: { id: examId }, + student: { id: studentId }, + }, + relations: ["exam", "student"], + }); + if (existingRegistration) { + throw new ExamError("Student already registered for this exam", 409); + } + + // Ensure the exam exists + const exam = await examRepository.findOne({ where: { id: examId } }); + if (!exam) { + throw new ExamError("Exam not found", 404); + } + + const registration = examRegistrationRepository.create({ + exam, + student: { id: studentId }, + answers, // This can be empty initially or provided during submission + }); + return await examRegistrationRepository.save(registration); + }, + + async getExamRegistrations(examId) { + // Fetch registrations with related student details + const exam = await examRepository.findOne({ where: { id: examId } }); + if (!exam) { + throw new ExamError("Exam not found", 404); + } + return await examRegistrationRepository.find({ + where: { exam: { id: examId } }, + relations: ["student"], + }); + }, +};