Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement an update EXAM CRUD ENDPOINT #18

Merged
merged 1 commit into from
Feb 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 12 additions & 11 deletions config/config.js
Original file line number Diff line number Diff line change
@@ -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;

75 changes: 75 additions & 0 deletions controllers/exam.controller.js
Original file line number Diff line number Diff line change
@@ -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();
29 changes: 29 additions & 0 deletions entities/exam.entity.js
Original file line number Diff line number Diff line change
@@ -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,
},
},
});
36 changes: 36 additions & 0 deletions entities/examRegistration.entity.js
Original file line number Diff line number Diff line change
@@ -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",
},
},
});
52 changes: 52 additions & 0 deletions http/exam.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
### Admin - Create New Exam
//kindly fill out afterjwt token has been created
POST http://localhost:5000/api/exams
Authorization: Bearer <YOUR_ADMIN_JWT_TOKEN>
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 <YOUR_ADMIN_JWT_TOKEN>
Content-Type: application/json

### Admin - Get Exam By ID
GET http://localhost:5000/api/exams/1
Authorization: Bearer <YOUR_ADMIN_JWT_TOKEN>
Content-Type: application/json

### Admin - Update Exam
PUT http://localhost:5000/api/exams/1
Authorization: Bearer <YOUR_ADMIN_JWT_TOKEN>
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 <YOUR_ADMIN_JWT_TOKEN>
Content-Type: application/json

### Student - Register for Exam (Submit Answers)
POST http://localhost:5000/api/exams/1/register
Authorization: Bearer <YOUR_STUDENT_JWT_TOKEN>
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 <YOUR_ADMIN_JWT_TOKEN>
Content-Type: application/json
38 changes: 26 additions & 12 deletions middlewares/auth.middleware.js
Original file line number Diff line number Diff line change
@@ -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 };
35 changes: 35 additions & 0 deletions routes/exam.routes.js
Original file line number Diff line number Diff line change
@@ -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;
Empty file removed routes/examRoutes.js
Empty file.
1 change: 1 addition & 0 deletions server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading