diff --git a/Readme.md b/Readme.md
index d043f61..80d5d08 100644
--- a/Readme.md
+++ b/Readme.md
@@ -1,5 +1,3 @@
-
-
# Contributing to SkillNet: Empowering Exams with Innovation
Thank you for your interest in contributing to SkillNet! SkillNet is a cutting-edge platform designed to host secure and efficient online exams. Our backend is built with Node.js and utilizes modern technologies to deliver a seamless testing experience for educators and candidates.
@@ -338,4 +336,52 @@ Please adhere to our Code of Conduct to maintain a respectful and inclusive comm
- Document new features and updates thoroughly.
## License
-This project is licensed under the MIT License - see the LICENSE file for details.
\ No newline at end of file
+This project is licensed under the MIT License - see the LICENSE file for details.
+
+## ESM Migration Guide and Test Fixes
+
+### 1. Update Configuration Files
+
+#### `package.json`:
+```json
+{
+ "type": "module",
+ "scripts": {
+ "test": "NODE_OPTIONS=--experimental-vm-modules jest",
+ "dev": "nodemon src/server.js",
+ "start": "node src/server.js"
+ },
+ "jest": {
+ "transform": {},
+ "extensionsToTreatAsEsm": [".js"]
+ }
+}
+```
+
+#### `jest.config.js`:
+```javascript
+export default {
+ testEnvironment: 'node',
+ transform: {},
+ extensionsToTreatAsEsm: ['.js'],
+ moduleNameMapper: {
+ '^(\\.{1,2}/.*)\\.js$': '$1'
+ }
+};
+```
+
+### 2. Common Issues and Solutions
+
+| Issue | Solution |
+|-------------------------------|------------------------------------|
+| "Cannot use import statement" | Ensure "type": "module" is set |
+| Mock not working in tests | Use unstable_mockModule for ESM |
+| Missing file extensions | Always include .js in imports |
+| Async test failures | Add proper await/async handling |
+
+## ESM Usage
+
+```javascript
+// Importing from this package
+import { createExam } from 'skillnet-exam-server'
+```
\ No newline at end of file
diff --git a/babel.config.js b/babel.config.js
new file mode 100644
index 0000000..8c792c6
--- /dev/null
+++ b/babel.config.js
@@ -0,0 +1,3 @@
+export default {
+ presets: [['@babel/preset-env', { targets: { node: 'current' } }]],
+};
\ No newline at end of file
diff --git a/coverage/clover.xml b/coverage/clover.xml
new file mode 100644
index 0000000..6ba8288
--- /dev/null
+++ b/coverage/clover.xml
@@ -0,0 +1,101 @@
+
+
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import jwt from 'jsonwebtoken';
+import { validationResult } from 'express-validator';
+import { User } from '../models/index.js';
+import asyncHandler from 'express-async-handler';
+import { Op } from 'sequelize';
+
+// Generate JWT
+const generateToken = (id) => {
+ return jwt.sign({ id }, process.env.JWT_SECRET, {
+ expiresIn: '30d',
+ });
+};
+
+// @desc Register a new user
+// @route POST /api/auth/register
+// @access Public
+const registerUser = asyncHandler(async (req, res) => {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({ errors: errors.array() });
+ }
+
+ const { fullName, email, walletAddress, role } = req.body;
+
+ // Check if user exists
+ const userExists = await User.findOne({
+ where: {
+ [Op.or]: [
+ { email },
+ ...(walletAddress ? [{ walletAddress }] : [])
+ ]
+ }
+ });
+
+ if (userExists) {
+ res.status(400);
+ throw new Error('User already exists');
+ }
+
+ // Create user
+ const user = await User.create({
+ fullName,
+ email,
+ walletAddress,
+ role,
+ });
+
+ if (user) {
+ res.status(201).json({
+ id: user.id,
+ fullName: user.fullName,
+ email: user.email,
+ walletAddress: user.walletAddress,
+ role: user.role,
+ });
+ } else {
+ res.status(400);
+ throw new Error('Invalid user data');
+ }
+});
+
+// @desc Authenticate a user
+// @route POST /api/auth/login
+// @access Public
+const loginUser = asyncHandler(async (req, res) => {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({ errors: errors.array() });
+ }
+
+ const { email, walletAddress } = req.body;
+
+ // Check for user email
+ const user = await User.findOne({ where: { email, walletAddress } });
+
+ if (user && walletAddress) {
+ res.json({
+ id: user.id,
+ fullName: user.fullName,
+ email: user.email,
+ walletAddress: user.walletAddress,
+ role: user.role,
+ token: generateToken(user.id),
+ });
+ } else {
+ res.status(401);
+ throw new Error('Invalid credentials');
+ }
+});
+
+// @desc Get user profile
+// @route GET /api/auth/profile
+// @access Private
+const getUserProfile = asyncHandler(async (req, res) => {
+ const user = await User.findByPk(req.user.id, {
+ attributes: { exclude: ['password'] }
+ });
+
+ if (user) {
+ res.json(user);
+ } else {
+ res.status(404);
+ throw new Error('User not found');
+ }
+});
+
+export {
+ registerUser,
+ loginUser,
+ getUserProfile,
+}; |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 | + + + +1x + + + + + + + + + + + + + + + + + +1x + + + + + + + + + + + | import { Sequelize } from 'sequelize';
+import 'dotenv/config';
+import logger from '../utils/logger.js';
+
+const sequelize = new Sequelize(
+ process.env.DB_NAME,
+ process.env.DB_USER,
+ process.env.DB_PASSWORD,
+ {
+ host: process.env.DB_HOST,
+ port: process.env.DB_PORT,
+ dialect: 'postgres',
+ logging: msg => logger.debug(msg),
+ pool: {
+ max: 5,
+ min: 0,
+ acquire: 30000,
+ idle: 10000,
+ },
+ }
+);
+
+const connectDB = async () => {
+ try {
+ await sequelize.authenticate();
+ logger.info('Database connection has been established successfully.');
+ } catch (error) {
+ logger.error(`Unable to connect to the database: ${error.message}`);
+ process.exit(1);
+ }
+};
+
+export { sequelize, connectDB };
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| db.js | +
+
+ |
+ 25% | +2/8 | +100% | +0/0 | +0% | +0/2 | +25% | +2/8 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import jwt from 'jsonwebtoken';
+import { validationResult } from 'express-validator';
+import { User } from '../models/index.js';
+import asyncHandler from 'express-async-handler';
+import { Op } from 'sequelize';
+
+// Generate JWT
+const generateToken = (id) => {
+ return jwt.sign({ id }, process.env.JWT_SECRET, {
+ expiresIn: '30d',
+ });
+};
+
+// @desc Register a new user
+// @route POST /api/auth/register
+// @access Public
+const registerUser = asyncHandler(async (req, res) => {
+ const errors = validationResult(req);
+ Iif (!errors.isEmpty()) {
+ return res.status(400).json({ errors: errors.array() });
+ }
+
+ const { fullName, email, walletAddress, role } = req.body;
+
+ // Check if user exists
+ const userExists = await User.findOne({
+ where: {
+ [Op.or]: [
+ { email },
+ ...(walletAddress ? [{ walletAddress }] : [])
+ ]
+ }
+ });
+
+ Iif (userExists) {
+ res.status(400);
+ throw new Error('User already exists');
+ }
+
+ // Create user
+ const user = await User.create({
+ fullName,
+ email,
+ walletAddress,
+ role,
+ });
+
+ if (user) {
+ res.status(201).json({
+ id: user.id,
+ fullName: user.fullName,
+ email: user.email,
+ walletAddress: user.walletAddress,
+ role: user.role,
+ });
+ } else {
+ res.status(400);
+ throw new Error('Invalid user data');
+ }
+});
+
+// @desc Authenticate a user
+// @route POST /api/auth/login
+// @access Public
+const loginUser = asyncHandler(async (req, res) => {
+ const errors = validationResult(req);
+ Iif (!errors.isEmpty()) {
+ return res.status(400).json({ errors: errors.array() });
+ }
+
+ const { email, walletAddress } = req.body;
+
+ // Check for user email
+ const user = await User.findOne({ where: { email, walletAddress } });
+
+ if (user && walletAddress) {
+ res.json({
+ id: user.id,
+ fullName: user.fullName,
+ email: user.email,
+ walletAddress: user.walletAddress,
+ role: user.role,
+ token: generateToken(user.id),
+ });
+ } else {
+ res.status(401);
+ throw new Error('Invalid credentials');
+ }
+});
+
+// @desc Get user profile
+// @route GET /api/auth/profile
+// @access Private
+const getUserProfile = asyncHandler(async (req, res) => {
+ const user = await User.findByPk(req.user.id, {
+ attributes: { exclude: ['password'] }
+ });
+
+ if (user) {
+ res.json(user);
+ } else {
+ res.status(404);
+ throw new Error('User not found');
+ }
+});
+
+export {
+ registerUser,
+ loginUser,
+ getUserProfile,
+}; |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 | + + + + + + + +1x +3x + +3x +3x +1x + + +3x + + + + + + + + + +3x + + + + + +1x + + + + + + + + + + + + + + + + + + + + + + + + + +1x + + + + + + + + + + + + + + + + + + +1x +6x +6x +6x +1x + + + + + + + + + + + + + + + + +5x + + +5x + + + + + + + + + + + + + + + +4x +2x +2x +2x + + + + + + +1x +1x +2x +2x + + + + + + + + + + + +3x + + + + + + + + +3x + +2x +2x + + + + + + +1x +3x + +3x + + + + + + + + + + + + + + + + + +3x + + +3x + + + + + + + + + + + + + + + +3x + + + + + + + + +3x + + + + + +1x +2x + +2x +1x +1x + + + +1x + +1x +1x + + +1x + + +1x + +1x + + + + + + + + + + | import { Exam, Question, Option } from '../models/index.js';
+import { Op } from 'sequelize';
+import asyncHandler from 'express-async-handler';
+import { validationResult } from 'express-validator';
+
+// @desc Get all exams
+// @route GET /api/exams
+// @access Public
+const getExams = asyncHandler(async (req, res) => {
+ const { category } = req.query;
+
+ const whereClause = {};
+ if (category) {
+ whereClause.category = category;
+ }
+
+ const exams = await Exam.findAll({
+ where: whereClause,
+ include: [
+ {
+ model: Question,
+ attributes: ['id'], // Only count questions, don't return them
+ },
+ ],
+ });
+
+ res.status(200).json(exams);
+});
+
+// @desc Get exam by ID
+// @route GET /api/exams/:id
+// @access Public
+const getExamById = asyncHandler(async (req, res) => {
+ const exam = await Exam.findByPk(req.params.id, {
+ include: [
+ {
+ model: Question,
+ include: [
+ {
+ model: Option,
+ attributes: ['id', 'text', 'order'], // Don't expose correct answers
+ },
+ ],
+ },
+ ],
+ });
+
+ Iif (!exam) {
+ res.status(404);
+ throw new Error('Exam not found');
+ }
+
+ res.status(200).json(exam);
+});
+
+// @desc Get exams by category
+// @route GET /api/exams/category/:category
+// @access Public
+const getExamsByCategory = asyncHandler(async (req, res) => {
+ const { category } = req.params;
+
+ const exams = await Exam.findAll({
+ where: { category },
+ include: [
+ {
+ model: Question,
+ attributes: ['id'], // Only count questions, don't return them
+ },
+ ],
+ });
+
+ res.status(200).json(exams);
+});
+
+// @desc Create new exam
+// @route POST /api/exams
+// @access Private (Admin)
+const createExam = asyncHandler(async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({ errors: errors.array() });
+ }
+
+ const {
+ name,
+ description,
+ category,
+ date,
+ duration,
+ certification,
+ passingScore,
+ format,
+ topicsCovered,
+ benefits,
+ price,
+ instructions,
+ questions,
+ } = req.body;
+
+ // Create exam
+ const exam = await Exam.create({
+ name,
+ description,
+ category,
+ date,
+ duration,
+ certification,
+ passingScore,
+ format,
+ topicsCovered,
+ benefits,
+ price,
+ instructions,
+ });
+
+ // Create questions and options if provided
+ if (questions && questions.length > 0) {
+ for (let i = 0; i < questions.length; i++) {
+ const q = questions[i];
+ const question = await Question.create({
+ examId: exam.id,
+ question: q.question,
+ order: i + 1,
+ });
+
+ // Create options for this question
+ if (q.options && q.options.length > 0) {
+ for (let j = 0; j < q.options.length; j++) {
+ const opt = q.options[j];
+ await Option.create({
+ questionId: question.id,
+ text: opt.text,
+ isCorrect: opt.isCorrect,
+ order: String.fromCharCode(65 + j), // A, B, C, D...
+ });
+ }
+ }
+ }
+ }
+
+ // Return the created exam with questions
+ const createdExam = await Exam.findByPk(exam.id, {
+ include: [
+ {
+ model: Question,
+ include: [Option],
+ },
+ ],
+ });
+
+ res.status(201).json(createdExam);
+ } catch (error) {
+ res.status(500);
+ throw new Error('Error creating exam: ' + error.message);
+ }
+});
+
+// @desc Update exam
+// @route PUT /api/exams/:id
+// @access Private (Admin)
+const updateExam = asyncHandler(async (req, res) => {
+ const exam = await Exam.findByPk(req.params.id);
+
+ Iif (!exam) {
+ res.status(404);
+ throw new Error('Exam not found');
+ }
+
+ const {
+ name,
+ description,
+ category,
+ date,
+ duration,
+ certification,
+ passingScore,
+ format,
+ topicsCovered,
+ benefits,
+ price,
+ instructions,
+ } = req.body;
+
+ // Update exam details
+ await exam.update({
+ name: name || exam.name,
+ description: description || exam.description,
+ category: category || exam.category,
+ date: date || exam.date,
+ duration: duration || exam.duration,
+ certification: certification !== undefined ? certification : exam.certification,
+ passingScore: passingScore || exam.passingScore,
+ format: format || exam.format,
+ topicsCovered: topicsCovered || exam.topicsCovered,
+ benefits: benefits || exam.benefits,
+ price: price || exam.price,
+ instructions: instructions || exam.instructions,
+ });
+
+ // Get updated exam with questions
+ const updatedExam = await Exam.findByPk(exam.id, {
+ include: [
+ {
+ model: Question,
+ include: [Option],
+ },
+ ],
+ });
+
+ res.status(200).json(updatedExam);
+});
+
+// @desc Delete exam
+// @route DELETE /api/exams/:id
+// @access Private (Admin)
+const deleteExam = asyncHandler(async (req, res) => {
+ const exam = await Exam.findByPk(req.params.id);
+
+ if (!exam) {
+ res.status(404);
+ throw new Error('Exam not found');
+ }
+
+ // Delete associated questions and options
+ const questions = await Question.findAll({ where: { examId: exam.id } });
+
+ for (const question of questions) {
+ await Option.destroy({ where: { questionId: question.id } });
+ }
+
+ await Question.destroy({ where: { examId: exam.id } });
+
+ // Delete the exam
+ await exam.destroy();
+
+ res.status(200).json({ id: req.params.id });
+});
+
+export {
+ getExams,
+ getExamById,
+ getExamsByCategory,
+ createExam,
+ updateExam,
+ deleteExam,
+}; |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { ExamBanner, Exam } from '../models/index.js';
+import asyncHandler from 'express-async-handler';
+import { validationResult } from 'express-validator';
+import logger from '../utils/logger.js';
+
+// @desc Get all exam banners
+// @route GET /api/exam-banners
+// @access Public
+const getExamBanners = asyncHandler(async (req, res) => {
+ try {
+ const { active } = req.query;
+
+ const whereClause = {};
+ Iif (active === 'true') {
+ const now = new Date();
+ whereClause.isActive = true;
+ whereClause.startDate = { $lte: now }; // Start date is less than or equal to now
+ whereClause.endDate = { $gte: now }; // End date is greater than or equal to now
+ }
+
+ logger.info(`Fetching exam banners with filters: ${JSON.stringify(whereClause)}`);
+
+ const examBanners = await ExamBanner.findAll({
+ where: whereClause,
+ include: [
+ {
+ model: Exam,
+ as: 'exam',
+ attributes: ['id', 'name', 'category'],
+ },
+ ],
+ order: [
+ ['priority', 'DESC'],
+ ['startDate', 'DESC'],
+ ],
+ });
+
+ logger.info(`Retrieved ${examBanners.length} exam banners`);
+ res.status(200).json(examBanners);
+ } catch (error) {
+ logger.error(`Error retrieving exam banners: ${error.message}`, { error: error.stack });
+ res.status(500);
+ throw new Error('Error retrieving exam banners');
+ }
+});
+
+// @desc Get exam banner by ID
+// @route GET /api/exam-banners/:id
+// @access Public
+const getExamBannerById = asyncHandler(async (req, res) => {
+ try {
+ logger.info(`Fetching exam banner with id: ${req.params.id}`);
+
+ const examBanner = await ExamBanner.findByPk(req.params.id, {
+ include: [
+ {
+ model: Exam,
+ as: 'exam',
+ attributes: ['id', 'name', 'category', 'description'],
+ },
+ ],
+ });
+
+ Iif (!examBanner) {
+ logger.warn(`Exam banner with id ${req.params.id} not found`, { bannerId: req.params.id });
+ res.status(404);
+ throw new Error('Exam banner not found');
+ }
+
+ logger.info(`Retrieved exam banner ${examBanner.id}`);
+ res.status(200).json(examBanner);
+ } catch (error) {
+ Iif (error.message === 'Exam banner not found') {
+ throw error;
+ }
+ logger.error(`Error retrieving exam banner ${req.params.id}: ${error.message}`, {
+ bannerId: req.params.id,
+ error: error.stack
+ });
+ res.status(500);
+ throw new Error('Error retrieving exam banner');
+ }
+});
+
+// @desc Create new exam banner
+// @route POST /api/exam-banners
+// @access Private (Admin)
+const createExamBanner = asyncHandler(async (req, res) => {
+ const errors = validationResult(req);
+ Iif (!errors.isEmpty()) {
+ logger.warn(`Validation errors in creating exam banner: ${JSON.stringify(errors.array())}`, {
+ userId: req.user.id,
+ validationErrors: errors.array()
+ });
+ return res.status(400).json({ errors: errors.array() });
+ }
+
+ const {
+ title,
+ description,
+ imageUrl,
+ startDate,
+ endDate,
+ isActive,
+ examId,
+ buttonText,
+ buttonLink,
+ priority,
+ } = req.body;
+
+ logger.info(`Admin ${req.user.id} creating exam banner: "${title}"`);
+
+ try {
+ // Check if exam exists if examId is provided
+ Iif (examId) {
+ const exam = await Exam.findByPk(examId);
+ Iif (!exam) {
+ logger.warn(`Exam with id ${examId} not found while admin ${req.user.id} creating banner`, {
+ examId,
+ adminId: req.user.id
+ });
+ res.status(404);
+ throw new Error('Exam not found');
+ }
+ }
+
+ // Create banner
+ const banner = await ExamBanner.create({
+ title,
+ description,
+ imageUrl,
+ startDate,
+ endDate,
+ isActive: isActive !== undefined ? isActive : true,
+ examId,
+ buttonText,
+ buttonLink,
+ priority: priority || 0,
+ });
+
+ logger.info(`Created exam banner ${banner.id} by admin ${req.user.id}`, {
+ bannerId: banner.id,
+ adminId: req.user.id,
+ examId: examId || 'none'
+ });
+
+ // Return the created banner with exam info
+ const createdBanner = await ExamBanner.findByPk(banner.id, {
+ include: [
+ {
+ model: Exam,
+ as: 'exam',
+ attributes: ['id', 'name', 'category'],
+ },
+ ],
+ });
+
+ res.status(201).json(createdBanner);
+ } catch (error) {
+ Iif (error.message === 'Exam not found') {
+ throw error;
+ }
+ logger.error(`Error creating exam banner by admin ${req.user.id}: ${error.message}`, {
+ adminId: req.user.id,
+ examId,
+ error: error.stack
+ });
+ res.status(500);
+ throw new Error('Error creating exam banner');
+ }
+});
+
+// @desc Update exam banner
+// @route PUT /api/exam-banners/:id
+// @access Private (Admin)
+const updateExamBanner = asyncHandler(async (req, res) => {
+ try {
+ logger.info(`Admin ${req.user.id} updating exam banner ${req.params.id}`);
+
+ const banner = await ExamBanner.findByPk(req.params.id);
+
+ Iif (!banner) {
+ logger.warn(`Exam banner with id ${req.params.id} not found for update by admin ${req.user.id}`, {
+ bannerId: req.params.id,
+ adminId: req.user.id
+ });
+ res.status(404);
+ throw new Error('Exam banner not found');
+ }
+
+ const {
+ title,
+ description,
+ imageUrl,
+ startDate,
+ endDate,
+ isActive,
+ examId,
+ buttonText,
+ buttonLink,
+ priority,
+ } = req.body;
+
+ // If examId is being changed, verify the new exam exists
+ Iif (examId && examId !== banner.examId) {
+ const exam = await Exam.findByPk(examId);
+ Iif (!exam) {
+ logger.warn(`Exam with id ${examId} not found while admin ${req.user.id} updating banner ${req.params.id}`, {
+ examId,
+ bannerId: req.params.id,
+ adminId: req.user.id
+ });
+ res.status(404);
+ throw new Error('Exam not found');
+ }
+ }
+
+ // Update banner
+ await banner.update({
+ title: title || banner.title,
+ description: description !== undefined ? description : banner.description,
+ imageUrl: imageUrl !== undefined ? imageUrl : banner.imageUrl,
+ startDate: startDate || banner.startDate,
+ endDate: endDate || banner.endDate,
+ isActive: isActive !== undefined ? isActive : banner.isActive,
+ examId: examId || banner.examId,
+ buttonText: buttonText !== undefined ? buttonText : banner.buttonText,
+ buttonLink: buttonLink !== undefined ? buttonLink : banner.buttonLink,
+ priority: priority !== undefined ? priority : banner.priority,
+ });
+
+ logger.info(`Updated exam banner ${banner.id} by admin ${req.user.id}`, {
+ bannerId: banner.id,
+ adminId: req.user.id,
+ changes: {
+ title: !!title,
+ description: description !== undefined,
+ imageUrl: imageUrl !== undefined,
+ startDate: !!startDate,
+ endDate: !!endDate,
+ isActive: isActive !== undefined,
+ examId: !!examId && examId !== banner.examId,
+ buttonText: buttonText !== undefined,
+ buttonLink: buttonLink !== undefined,
+ priority: priority !== undefined
+ }
+ });
+
+ // Return the updated banner with exam info
+ const updatedBanner = await ExamBanner.findByPk(banner.id, {
+ include: [
+ {
+ model: Exam,
+ as: 'exam',
+ attributes: ['id', 'name', 'category'],
+ },
+ ],
+ });
+
+ res.status(200).json(updatedBanner);
+ } catch (error) {
+ Iif (error.message === 'Exam banner not found' || error.message === 'Exam not found') {
+ throw error;
+ }
+ logger.error(`Error updating exam banner ${req.params.id} by admin ${req.user.id}: ${error.message}`, {
+ bannerId: req.params.id,
+ adminId: req.user.id,
+ error: error.stack
+ });
+ res.status(500);
+ throw new Error('Error updating exam banner');
+ }
+});
+
+// @desc Delete exam banner
+// @route DELETE /api/exam-banners/:id
+// @access Private (Admin)
+const deleteExamBanner = asyncHandler(async (req, res) => {
+ try {
+ logger.info(`Admin ${req.user.id} deleting exam banner ${req.params.id}`);
+
+ const banner = await ExamBanner.findByPk(req.params.id);
+
+ Iif (!banner) {
+ logger.warn(`Exam banner with id ${req.params.id} not found for deletion by admin ${req.user.id}`, {
+ bannerId: req.params.id,
+ adminId: req.user.id
+ });
+ res.status(404);
+ throw new Error('Exam banner not found');
+ }
+
+ // Delete banner
+ await banner.destroy();
+
+ logger.info(`Deleted exam banner ${req.params.id} by admin ${req.user.id}`, {
+ bannerId: req.params.id,
+ adminId: req.user.id,
+ bannerData: {
+ title: banner.title,
+ examId: banner.examId,
+ isActive: banner.isActive
+ }
+ });
+ res.status(200).json({ id: req.params.id });
+ } catch (error) {
+ Iif (error.message === 'Exam banner not found') {
+ throw error;
+ }
+ logger.error(`Error deleting exam banner ${req.params.id} by admin ${req.user.id}: ${error.message}`, {
+ bannerId: req.params.id,
+ adminId: req.user.id,
+ error: error.stack
+ });
+ res.status(500);
+ throw new Error('Error deleting exam banner');
+ }
+});
+
+export {
+ getExamBanners,
+ getExamBannerById,
+ createExamBanner,
+ updateExamBanner,
+ deleteExamBanner,
+};
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { ExamRecording, Exam } from '../models/index.js';
+import asyncHandler from 'express-async-handler';
+import { validationResult } from 'express-validator';
+import logger from '../utils/logger.js';
+
+// @desc Get all exam recordings
+// @route GET /api/exam-recordings
+// @access Public
+const getExamRecordings = asyncHandler(async (req, res) => {
+ try {
+ const { examId, published } = req.query;
+
+ const whereClause = {};
+ Iif (examId) {
+ whereClause.examId = examId;
+ }
+ if (published === 'true') {
+ whereClause.isPublished = true;
+ } else Iif (published === 'false') {
+ whereClause.isPublished = false;
+ }
+
+ logger.info(`Fetching exam recordings with filters: ${JSON.stringify(whereClause)}`);
+
+ const examRecordings = await ExamRecording.findAll({
+ where: whereClause,
+ include: [
+ {
+ model: Exam,
+ attributes: ['id', 'name', 'category'],
+ },
+ ],
+ order: [
+ ['recordedOn', 'DESC'],
+ ],
+ });
+
+ logger.info(`Retrieved ${examRecordings.length} exam recordings`);
+ res.status(200).json(examRecordings);
+ } catch (error) {
+ logger.error(`Error retrieving exam recordings: ${error.message}`, { error: error.stack });
+ res.status(500);
+ throw new Error('Error retrieving exam recordings');
+ }
+});
+
+// @desc Get exam recording by ID
+// @route GET /api/exam-recordings/:id
+// @access Public
+const getExamRecordingById = asyncHandler(async (req, res) => {
+ try {
+ logger.info(`Fetching exam recording with id: ${req.params.id}`);
+
+ const examRecording = await ExamRecording.findByPk(req.params.id, {
+ include: [
+ {
+ model: Exam,
+ attributes: ['id', 'name', 'category', 'description'],
+ },
+ ],
+ });
+
+ Iif (!examRecording) {
+ logger.warn(`Exam recording with id ${req.params.id} not found`, { recordingId: req.params.id });
+ res.status(404);
+ throw new Error('Exam recording not found');
+ }
+
+ logger.info(`Retrieved exam recording ${examRecording.id}`);
+ res.status(200).json(examRecording);
+ } catch (error) {
+ Iif (error.message === 'Exam recording not found') {
+ throw error;
+ }
+ logger.error(`Error retrieving exam recording ${req.params.id}: ${error.message}`, {
+ recordingId: req.params.id,
+ error: error.stack
+ });
+ res.status(500);
+ throw new Error('Error retrieving exam recording');
+ }
+});
+
+// @desc Create new exam recording
+// @route POST /api/exam-recordings
+// @access Private (Admin)
+const createExamRecording = asyncHandler(async (req, res) => {
+ const errors = validationResult(req);
+ Iif (!errors.isEmpty()) {
+ logger.warn(`Validation errors in creating exam recording: ${JSON.stringify(errors.array())}`, {
+ userId: req.user.id,
+ validationErrors: errors.array()
+ });
+ return res.status(400).json({ errors: errors.array() });
+ }
+
+ const {
+ title,
+ description,
+ recordingUrl,
+ duration,
+ examId,
+ presenter,
+ recordedOn,
+ isPublished,
+ thumbnailUrl,
+ tags,
+ } = req.body;
+
+ logger.info(`Admin ${req.user.id} creating exam recording: "${title}"`);
+
+ try {
+ // Check if exam exists
+ const exam = await Exam.findByPk(examId);
+ Iif (!exam) {
+ logger.warn(`Exam with id ${examId} not found while admin ${req.user.id} creating recording`, {
+ examId,
+ adminId: req.user.id
+ });
+ res.status(404);
+ throw new Error('Exam not found');
+ }
+
+ // Create recording
+ const recording = await ExamRecording.create({
+ title,
+ description,
+ recordingUrl,
+ duration,
+ examId,
+ presenter,
+ recordedOn: recordedOn || new Date(),
+ isPublished: isPublished !== undefined ? isPublished : false,
+ thumbnailUrl,
+ tags: tags || [],
+ });
+
+ logger.info(`Created exam recording ${recording.id} by admin ${req.user.id}`, {
+ recordingId: recording.id,
+ adminId: req.user.id,
+ examId: examId,
+ isPublished: isPublished !== undefined ? isPublished : false
+ });
+
+ // Return the created recording with exam info
+ const createdRecording = await ExamRecording.findByPk(recording.id, {
+ include: [
+ {
+ model: Exam,
+ attributes: ['id', 'name', 'category'],
+ },
+ ],
+ });
+
+ res.status(201).json(createdRecording);
+ } catch (error) {
+ Iif (error.message === 'Exam not found') {
+ throw error;
+ }
+ logger.error(`Error creating exam recording by admin ${req.user.id}: ${error.message}`, {
+ adminId: req.user.id,
+ examId,
+ error: error.stack
+ });
+ res.status(500);
+ throw new Error('Error creating exam recording');
+ }
+});
+
+// @desc Update exam recording
+// @route PUT /api/exam-recordings/:id
+// @access Private (Admin)
+const updateExamRecording = asyncHandler(async (req, res) => {
+ try {
+ logger.info(`Admin ${req.user.id} updating exam recording ${req.params.id}`);
+
+ const recording = await ExamRecording.findByPk(req.params.id);
+
+ Iif (!recording) {
+ logger.warn(`Exam recording with id ${req.params.id} not found for update by admin ${req.user.id}`, {
+ recordingId: req.params.id,
+ adminId: req.user.id
+ });
+ res.status(404);
+ throw new Error('Exam recording not found');
+ }
+
+ const {
+ title,
+ description,
+ recordingUrl,
+ duration,
+ examId,
+ presenter,
+ recordedOn,
+ isPublished,
+ thumbnailUrl,
+ tags,
+ } = req.body;
+
+ // If examId is being changed, verify the new exam exists
+ Iif (examId && examId !== recording.examId) {
+ const exam = await Exam.findByPk(examId);
+ Iif (!exam) {
+ logger.warn(`Exam with id ${examId} not found while admin ${req.user.id} updating recording ${req.params.id}`, {
+ examId,
+ recordingId: req.params.id,
+ adminId: req.user.id
+ });
+ res.status(404);
+ throw new Error('Exam not found');
+ }
+ }
+
+ // Update recording
+ await recording.update({
+ title: title || recording.title,
+ description: description !== undefined ? description : recording.description,
+ recordingUrl: recordingUrl || recording.recordingUrl,
+ duration: duration !== undefined ? duration : recording.duration,
+ examId: examId || recording.examId,
+ presenter: presenter !== undefined ? presenter : recording.presenter,
+ recordedOn: recordedOn || recording.recordedOn,
+ isPublished: isPublished !== undefined ? isPublished : recording.isPublished,
+ thumbnailUrl: thumbnailUrl !== undefined ? thumbnailUrl : recording.thumbnailUrl,
+ tags: tags || recording.tags,
+ });
+
+ logger.info(`Updated exam recording ${recording.id} by admin ${req.user.id}`, {
+ recordingId: recording.id,
+ adminId: req.user.id,
+ changes: {
+ title: !!title,
+ description: description !== undefined,
+ recordingUrl: !!recordingUrl,
+ duration: duration !== undefined,
+ examId: !!examId && examId !== recording.examId,
+ presenter: presenter !== undefined,
+ recordedOn: !!recordedOn,
+ isPublished: isPublished !== undefined,
+ thumbnailUrl: thumbnailUrl !== undefined,
+ tags: !!tags
+ }
+ });
+
+ // Return the updated recording with exam info
+ const updatedRecording = await ExamRecording.findByPk(recording.id, {
+ include: [
+ {
+ model: Exam,
+ attributes: ['id', 'name', 'category'],
+ },
+ ],
+ });
+
+ res.status(200).json(updatedRecording);
+ } catch (error) {
+ Iif (error.message === 'Exam recording not found' || error.message === 'Exam not found') {
+ throw error;
+ }
+ logger.error(`Error updating exam recording ${req.params.id} by admin ${req.user.id}: ${error.message}`, {
+ recordingId: req.params.id,
+ adminId: req.user.id,
+ error: error.stack
+ });
+ res.status(500);
+ throw new Error('Error updating exam recording');
+ }
+});
+
+// @desc Delete exam recording
+// @route DELETE /api/exam-recordings/:id
+// @access Private (Admin)
+const deleteExamRecording = asyncHandler(async (req, res) => {
+ try {
+ logger.info(`Admin ${req.user.id} deleting exam recording ${req.params.id}`);
+
+ const recording = await ExamRecording.findByPk(req.params.id);
+
+ Iif (!recording) {
+ logger.warn(`Exam recording with id ${req.params.id} not found for deletion by admin ${req.user.id}`, {
+ recordingId: req.params.id,
+ adminId: req.user.id
+ });
+ res.status(404);
+ throw new Error('Exam recording not found');
+ }
+
+ // Delete recording
+ await recording.destroy();
+
+ logger.info(`Deleted exam recording ${req.params.id} by admin ${req.user.id}`, {
+ recordingId: req.params.id,
+ adminId: req.user.id,
+ recordingData: {
+ title: recording.title,
+ examId: recording.examId,
+ isPublished: recording.isPublished
+ }
+ });
+ res.status(200).json({ id: req.params.id });
+ } catch (error) {
+ Iif (error.message === 'Exam recording not found') {
+ throw error;
+ }
+ logger.error(`Error deleting exam recording ${req.params.id} by admin ${req.user.id}: ${error.message}`, {
+ recordingId: req.params.id,
+ adminId: req.user.id,
+ error: error.stack
+ });
+ res.status(500);
+ throw new Error('Error deleting exam recording');
+ }
+});
+
+// @desc Toggle publish status of an exam recording
+// @route PATCH /api/exam-recordings/:id/publish
+// @access Private (Admin)
+const togglePublishStatus = asyncHandler(async (req, res) => {
+ try {
+ logger.info(`Admin ${req.user.id} toggling publish status for exam recording ${req.params.id}`);
+
+ const recording = await ExamRecording.findByPk(req.params.id);
+
+ Iif (!recording) {
+ logger.warn(`Exam recording with id ${req.params.id} not found for status toggle by admin ${req.user.id}`, {
+ recordingId: req.params.id,
+ adminId: req.user.id
+ });
+ res.status(404);
+ throw new Error('Exam recording not found');
+ }
+
+ // Toggle publish status
+ const newStatus = !recording.isPublished;
+ await recording.update({
+ isPublished: newStatus,
+ });
+
+ logger.info(`Toggled exam recording ${recording.id} publish status to ${newStatus} by admin ${req.user.id}`, {
+ recordingId: recording.id,
+ adminId: req.user.id,
+ newStatus: newStatus,
+ previousStatus: !newStatus
+ });
+
+ res.status(200).json(recording);
+ } catch (error) {
+ Iif (error.message === 'Exam recording not found') {
+ throw error;
+ }
+ logger.error(`Error toggling exam recording ${req.params.id} publish status by admin ${req.user.id}: ${error.message}`, {
+ recordingId: req.params.id,
+ adminId: req.user.id,
+ error: error.stack
+ });
+ res.status(500);
+ throw new Error('Error toggling publish status');
+ }
+});
+
+export {
+ getExamRecordings,
+ getExamRecordingById,
+ createExamRecording,
+ updateExamRecording,
+ deleteExamRecording,
+ togglePublishStatus,
+};
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| auth.controller.js | +
+
+ |
+ 0% | +0/32 | +0% | +0/13 | +0% | +0/4 | +0% | +0/32 | +
| exam.controller.js | +
+
+ |
+ 82.75% | +48/58 | +88.57% | +31/35 | +66.66% | +4/6 | +82.14% | +46/56 | +
| examBanner.controller.js | +
+
+ |
+ 0% | +0/94 | +0% | +0/45 | +0% | +0/5 | +0% | +0/94 | +
| examRecording.controller.js | +
+
+ |
+ 0% | +0/111 | +0% | +0/51 | +0% | +0/6 | +0% | +0/111 | +
| indexer.controller.js | +
+
+ |
+ 0% | +0/81 | +0% | +0/17 | +0% | +0/9 | +0% | +0/78 | +
| notification.controller.js | +
+
+ |
+ 0% | +0/95 | +0% | +0/30 | +0% | +0/6 | +0% | +0/95 | +
| registration.controller.js | +
+
+ |
+ 0% | +0/43 | +0% | +0/7 | +0% | +0/4 | +0% | +0/43 | +
| result.controller.js | +
+
+ |
+ 0% | +0/50 | +0% | +0/9 | +0% | +0/6 | +0% | +0/47 | +
| user.controller.js | +
+
+ |
+ 93.93% | +31/33 | +83.33% | +5/6 | +100% | +4/4 | +93.54% | +29/31 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { ContractEvent } from '../models/index.js';
+import asyncHandler from 'express-async-handler';
+import { indexer } from '../indexer/indexer.js';
+import { indexerConfig, apibaraConfig, networks, contracts, eventHandlers, eventSelectors } from '../indexer/config.js';
+import logger from '../utils/logger.js';
+import { Op } from 'sequelize';
+
+// @desc Get indexer status
+// @route GET /api/indexer/status
+// @access Private
+const getIndexerStatus = asyncHandler(async (req, res) => {
+ try {
+ logger.info('Fetching indexer status');
+
+ // Get the last processed block from DB
+ const lastEvent = await ContractEvent.findOne({
+ order: [['blockNumber', 'DESC']],
+ attributes: ['blockNumber', 'blockTimestamp', 'createdAt']
+ });
+
+ const status = {
+ lastProcessedBlock: lastEvent ? Number(lastEvent.blockNumber) : null,
+ lastBlockTimestamp: lastEvent ? new Date(Number(lastEvent.blockTimestamp) * 1000).toISOString() : null,
+ lastProcessingTime: lastEvent ? lastEvent.createdAt : null,
+ network: indexerConfig.network,
+ contractAddress: indexerConfig.contractAddress,
+ isRunning: indexer.isRunning(),
+ startBlock: Number(process.env.INDEXER_START_BLOCK) || 0
+ };
+
+ logger.info(`Indexer status retrieved: ${JSON.stringify(status)}`);
+ res.status(200).json(status);
+ } catch (error) {
+ logger.error(`Error retrieving indexer status: ${error.message}`, { error: error.stack });
+ res.status(500);
+ throw new Error('Error retrieving indexer status');
+ }
+});
+
+// @desc Trigger indexer to scan for events
+// @route POST /api/indexer/scan
+// @access Private (Admin)
+const triggerScan = asyncHandler(async (req, res) => {
+ try {
+ logger.info(`Admin ${req.user.id} triggering indexer scan`);
+
+ const { fromBlock, toBlock } = req.body;
+
+ // Convert 'latest' string to null to use default behavior
+ const from = fromBlock === 'latest' ? null : Number(fromBlock);
+ const to = toBlock === 'latest' ? null : Number(toBlock);
+
+ // Trigger a scan
+ const result = await indexer.scanForEvents(from, to);
+
+ logger.info(`Manual indexer scan completed by admin ${req.user.id}`, {
+ adminId: req.user.id,
+ eventsProcessed: result.events.length,
+ fromBlock: result.fromBlock,
+ toBlock: result.toBlock
+ });
+
+ res.status(200).json({
+ message: 'Scan triggered successfully',
+ eventsProcessed: result.events.length,
+ fromBlock: result.fromBlock,
+ toBlock: result.toBlock
+ });
+ } catch (error) {
+ logger.error(`Error triggering indexer scan by admin ${req.user.id}: ${error.message}`, {
+ adminId: req.user.id,
+ error: error.stack
+ });
+ res.status(500);
+ throw new Error('Error triggering indexer scan: ' + error.message);
+ }
+});
+
+// @desc Get contract events
+// @route GET /api/indexer/events
+// @access Private (Admin)
+const getContractEvents = asyncHandler(async (req, res) => {
+ try {
+ logger.info(`Admin ${req.user.id} retrieving contract events`);
+
+ const { eventName, fromBlock, toBlock, limit = 50 } = req.query;
+
+ const whereClause = {};
+ Iif (eventName) whereClause.eventName = eventName;
+ Iif (fromBlock) whereClause.blockNumber = { [Op.gte]: fromBlock };
+ Iif (toBlock) whereClause.blockNumber = {
+ ...whereClause.blockNumber,
+ [Op.lte]: toBlock
+ };
+
+ const events = await ContractEvent.findAll({
+ where: whereClause,
+ order: [['blockNumber', 'DESC'], ['logIndex', 'DESC']],
+ limit: parseInt(limit)
+ });
+
+ logger.info(`Retrieved ${events.length} contract events for admin ${req.user.id}`, {
+ adminId: req.user.id,
+ filter: { eventName, fromBlock, toBlock, limit }
+ });
+
+ res.status(200).json(events);
+ } catch (error) {
+ logger.error(`Error retrieving contract events for admin ${req.user.id}: ${error.message}`, {
+ adminId: req.user.id,
+ error: error.stack
+ });
+ res.status(500);
+ throw new Error('Error retrieving contract events');
+ }
+});
+
+// @desc Get indexed exams from blockchain
+// @route GET /api/indexer/exams
+// @access Private (Admin)
+const getIndexedExams = asyncHandler(async (req, res) => {
+ try {
+ logger.info(`Admin ${req.user.id} retrieving exams from indexed blockchain events`);
+
+ const events = await ContractEvent.findAll({
+ where: {
+ eventName: 'ExamCreated'
+ },
+ order: [['blockNumber', 'DESC']]
+ });
+
+ // Process and transform exam creation events
+ const exams = events.map(event => {
+ const { args } = JSON.parse(event.eventData);
+ return {
+ examId: args.examId,
+ name: args.name,
+ category: args.category,
+ creatorAddress: args.creator,
+ price: args.price,
+ blockNumber: event.blockNumber,
+ timestamp: event.blockTimestamp,
+ transactionHash: event.transactionHash
+ };
+ });
+
+ logger.info(`Retrieved ${exams.length} indexed exams for admin ${req.user.id}`);
+ res.status(200).json(exams);
+ } catch (error) {
+ logger.error(`Error retrieving indexed exams for admin ${req.user.id}: ${error.message}`, {
+ adminId: req.user.id,
+ error: error.stack
+ });
+ res.status(500);
+ throw new Error('Error retrieving indexed exams');
+ }
+});
+
+// @desc Get indexed registrations from blockchain
+// @route GET /api/indexer/registrations
+// @access Private (Admin)
+const getIndexedRegistrations = asyncHandler(async (req, res) => {
+ try {
+ logger.info(`Admin ${req.user.id} retrieving registrations from indexed blockchain events`);
+
+ const { examId } = req.query;
+
+ const whereClause = { eventName: 'UserRegistered' };
+ Iif (examId) {
+ // Filter by examId in the JSON event data
+ // This is a simplification - in actual implementation, you might need a more
+ // sophisticated query approach depending on your database
+ whereClause.$and = [
+ { eventData: { $like: `%"examId":"${examId}"%` } }
+ ];
+ }
+
+ const events = await ContractEvent.findAll({
+ where: whereClause,
+ order: [['blockNumber', 'DESC']]
+ });
+
+ // Process and transform registration events
+ const registrations = events.map(event => {
+ const { args } = JSON.parse(event.eventData);
+ return {
+ registrationId: args.registrationId,
+ examId: args.examId,
+ userAddress: args.user,
+ registrationTime: args.timestamp,
+ blockNumber: event.blockNumber,
+ timestamp: event.blockTimestamp,
+ transactionHash: event.transactionHash
+ };
+ });
+
+ logger.info(`Retrieved ${registrations.length} indexed registrations for admin ${req.user.id}`, {
+ adminId: req.user.id,
+ filter: { examId }
+ });
+ res.status(200).json(registrations);
+ } catch (error) {
+ logger.error(`Error retrieving indexed registrations for admin ${req.user.id}: ${error.message}`, {
+ adminId: req.user.id,
+ error: error.stack
+ });
+ res.status(500);
+ throw new Error('Error retrieving indexed registrations');
+ }
+});
+
+// @desc Get indexed exam results from blockchain
+// @route GET /api/indexer/results
+// @access Private (Admin)
+const getIndexedResults = asyncHandler(async (req, res) => {
+ try {
+ logger.info(`Admin ${req.user.id} retrieving exam results from indexed blockchain events`);
+
+ const { examId, userAddress } = req.query;
+
+ const whereClause = { eventName: 'ExamCompleted' };
+ // TODO: Implement proper filtering by examId and userAddress in the JSON
+
+ const events = await ContractEvent.findAll({
+ where: whereClause,
+ order: [['blockNumber', 'DESC']]
+ });
+
+ // Process and transform result events
+ const results = events.map(event => {
+ const { args } = JSON.parse(event.eventData);
+ return {
+ resultId: args.resultId,
+ examId: args.examId,
+ userAddress: args.user,
+ score: args.score,
+ passed: args.passed,
+ blockNumber: event.blockNumber,
+ timestamp: event.blockTimestamp,
+ transactionHash: event.transactionHash
+ };
+ });
+
+ logger.info(`Retrieved ${results.length} indexed exam results for admin ${req.user.id}`, {
+ adminId: req.user.id,
+ filter: { examId, userAddress }
+ });
+ res.status(200).json(results);
+ } catch (error) {
+ logger.error(`Error retrieving indexed exam results for admin ${req.user.id}: ${error.message}`, {
+ adminId: req.user.id,
+ error: error.stack
+ });
+ res.status(500);
+ throw new Error('Error retrieving indexed exam results');
+ }
+});
+
+export {
+ getIndexerStatus,
+ triggerScan,
+ getContractEvents,
+ getIndexedExams,
+ getIndexedRegistrations,
+ getIndexedResults,
+};
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { Notification, User } from '../models/index.js';
+import asyncHandler from 'express-async-handler';
+import { validationResult } from 'express-validator';
+import logger from '../utils/logger.js';
+
+// @desc Get all notifications
+// @route GET /api/notifications
+// @access Private
+const getNotifications = asyncHandler(async (req, res) => {
+ try {
+ logger.info(`User ${req.user.id} requesting their notifications`);
+
+ const notifications = await Notification.findAll({
+ where: {
+ userId: req.user.id,
+ },
+ order: [['createdAt', 'DESC']],
+ });
+
+ logger.info(`Retrieved ${notifications.length} notifications for user ${req.user.id}`);
+ res.status(200).json(notifications);
+ } catch (error) {
+ logger.error(`Error retrieving notifications for user ${req.user.id}: ${error.message}`, {
+ userId: req.user.id,
+ error: error.stack
+ });
+ res.status(500);
+ throw new Error('Error retrieving notifications');
+ }
+});
+
+// @desc Get notification by ID
+// @route GET /api/notifications/:id
+// @access Private
+const getNotificationById = asyncHandler(async (req, res) => {
+ try {
+ logger.info(`User ${req.user.id} requesting notification ${req.params.id}`);
+
+ const notification = await Notification.findOne({
+ where: {
+ id: req.params.id,
+ userId: req.user.id,
+ },
+ });
+
+ Iif (!notification) {
+ logger.warn(`Notification with id ${req.params.id} not found for user ${req.user.id}`, {
+ notificationId: req.params.id,
+ userId: req.user.id
+ });
+ res.status(404);
+ throw new Error('Notification not found');
+ }
+
+ logger.info(`Retrieved notification ${notification.id} for user ${req.user.id}`);
+ res.status(200).json(notification);
+ } catch (error) {
+ Iif (error.message === 'Notification not found') {
+ throw error;
+ }
+ logger.error(`Error retrieving notification ${req.params.id} for user ${req.user.id}: ${error.message}`, {
+ notificationId: req.params.id,
+ userId: req.user.id,
+ error: error.stack
+ });
+ res.status(500);
+ throw new Error('Error retrieving notification');
+ }
+});
+
+// @desc Create new notification
+// @route POST /api/notifications
+// @access Private (Admin)
+const createNotification = asyncHandler(async (req, res) => {
+ const errors = validationResult(req);
+ Iif (!errors.isEmpty()) {
+ logger.warn(`Validation errors in creating notification by admin ${req.user.id}: ${JSON.stringify(errors.array())}`, {
+ userId: req.user.id,
+ validationErrors: errors.array()
+ });
+ return res.status(400).json({ errors: errors.array() });
+ }
+
+ const {
+ title,
+ message,
+ type,
+ userId,
+ expiresAt,
+ } = req.body;
+
+ logger.info(`Admin ${req.user.id} creating notification: "${title}" for user ${userId || 'all users'}`);
+
+ try {
+ // Check if user exists if userId is provided
+ Iif (userId) {
+ const user = await User.findByPk(userId);
+ Iif (!user) {
+ logger.warn(`User with id ${userId} not found while admin ${req.user.id} creating notification`, {
+ targetUserId: userId,
+ adminId: req.user.id
+ });
+ res.status(404);
+ throw new Error('User not found');
+ }
+ }
+
+ // Create notification
+ const notification = await Notification.create({
+ title,
+ message,
+ type: type || 'info',
+ userId,
+ expiresAt,
+ });
+
+ logger.info(`Created notification ${notification.id} for user ${userId || 'all users'} by admin ${req.user.id}`, {
+ notificationId: notification.id,
+ targetUserId: userId,
+ adminId: req.user.id,
+ notificationType: type || 'info'
+ });
+ res.status(201).json(notification);
+ } catch (error) {
+ Iif (error.message === 'User not found') {
+ throw error;
+ }
+ logger.error(`Error creating notification by admin ${req.user.id}: ${error.message}`, {
+ adminId: req.user.id,
+ targetUserId: userId,
+ error: error.stack
+ });
+ res.status(500);
+ throw new Error('Error creating notification');
+ }
+});
+
+// @desc Update notification
+// @route PUT /api/notifications/:id
+// @access Private (Admin)
+const updateNotification = asyncHandler(async (req, res) => {
+ try {
+ logger.info(`Admin ${req.user.id} updating notification ${req.params.id}`);
+
+ const notification = await Notification.findByPk(req.params.id);
+
+ Iif (!notification) {
+ logger.warn(`Notification with id ${req.params.id} not found for update by admin ${req.user.id}`, {
+ notificationId: req.params.id,
+ adminId: req.user.id
+ });
+ res.status(404);
+ throw new Error('Notification not found');
+ }
+
+ const {
+ title,
+ message,
+ type,
+ isRead,
+ expiresAt,
+ } = req.body;
+
+ // Update notification
+ await notification.update({
+ title: title || notification.title,
+ message: message || notification.message,
+ type: type || notification.type,
+ isRead: isRead !== undefined ? isRead : notification.isRead,
+ expiresAt: expiresAt || notification.expiresAt,
+ });
+
+ logger.info(`Updated notification ${notification.id} by admin ${req.user.id}`, {
+ notificationId: notification.id,
+ adminId: req.user.id,
+ changes: {
+ title: !!title,
+ message: !!message,
+ type: !!type,
+ isRead: isRead !== undefined,
+ expiresAt: !!expiresAt
+ }
+ });
+ res.status(200).json(notification);
+ } catch (error) {
+ Iif (error.message === 'Notification not found') {
+ throw error;
+ }
+ logger.error(`Error updating notification ${req.params.id} by admin ${req.user.id}: ${error.message}`, {
+ notificationId: req.params.id,
+ adminId: req.user.id,
+ error: error.stack
+ });
+ res.status(500);
+ throw new Error('Error updating notification');
+ }
+});
+
+// @desc Mark notification as read
+// @route PATCH /api/notifications/:id/read
+// @access Private
+const markAsRead = asyncHandler(async (req, res) => {
+ try {
+ logger.info(`User ${req.user.id} marking notification ${req.params.id} as read`);
+
+ const notification = await Notification.findOne({
+ where: {
+ id: req.params.id,
+ userId: req.user.id,
+ },
+ });
+
+ Iif (!notification) {
+ logger.warn(`Notification with id ${req.params.id} not found for user ${req.user.id} to mark as read`, {
+ notificationId: req.params.id,
+ userId: req.user.id
+ });
+ res.status(404);
+ throw new Error('Notification not found');
+ }
+
+ // Mark as read
+ await notification.update({
+ isRead: true,
+ });
+
+ logger.info(`Marked notification ${notification.id} as read for user ${req.user.id}`, {
+ notificationId: notification.id,
+ userId: req.user.id,
+ previousReadStatus: notification.isRead
+ });
+ res.status(200).json(notification);
+ } catch (error) {
+ Iif (error.message === 'Notification not found') {
+ throw error;
+ }
+ logger.error(`Error marking notification ${req.params.id} as read for user ${req.user.id}: ${error.message}`, {
+ notificationId: req.params.id,
+ userId: req.user.id,
+ error: error.stack
+ });
+ res.status(500);
+ throw new Error('Error marking notification as read');
+ }
+});
+
+// @desc Delete notification
+// @route DELETE /api/notifications/:id
+// @access Private (Admin)
+const deleteNotification = asyncHandler(async (req, res) => {
+ try {
+ logger.info(`Admin ${req.user.id} deleting notification ${req.params.id}`);
+
+ const notification = await Notification.findByPk(req.params.id);
+
+ Iif (!notification) {
+ logger.warn(`Notification with id ${req.params.id} not found for deletion by admin ${req.user.id}`, {
+ notificationId: req.params.id,
+ adminId: req.user.id
+ });
+ res.status(404);
+ throw new Error('Notification not found');
+ }
+
+ // Delete notification
+ await notification.destroy();
+
+ logger.info(`Deleted notification ${req.params.id} by admin ${req.user.id}`, {
+ notificationId: req.params.id,
+ adminId: req.user.id,
+ notificationData: {
+ title: notification.title,
+ userId: notification.userId,
+ type: notification.type
+ }
+ });
+ res.status(200).json({ id: req.params.id });
+ } catch (error) {
+ Iif (error.message === 'Notification not found') {
+ throw error;
+ }
+ logger.error(`Error deleting notification ${req.params.id} by admin ${req.user.id}: ${error.message}`, {
+ notificationId: req.params.id,
+ adminId: req.user.id,
+ error: error.stack
+ });
+ res.status(500);
+ throw new Error('Error deleting notification');
+ }
+});
+
+export {
+ getNotifications,
+ getNotificationById,
+ createNotification,
+ updateNotification,
+ markAsRead,
+ deleteNotification,
+};
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { Registration, User, Exam } from '../models/index.js';
+import { validationResult } from 'express-validator';
+import asyncHandler from 'express-async-handler';
+
+// @desc Register for an exam
+// @access Private
+const registerForExam = asyncHandler(async (req, res) => {
+ const errors = validationResult(req);
+ Iif (!errors.isEmpty()) {
+ return res.status(400).json({ errors: errors.array() });
+ }
+
+ const { examId } = req.body;
+ const userId = req.user.id;
+
+ // Check if exam exists
+ const exam = await Exam.findByPk(examId);
+ Iif (!exam) {
+ res.status(404);
+ throw new Error('Exam not found');
+ }
+
+ // Check if already registered
+ const existingRegistration = await Registration.findOne({
+ where: { userId, examId }
+ });
+
+ Iif (existingRegistration) {
+ res.status(400);
+ throw new Error('Already registered for this exam');
+ }
+
+ // Create registration
+ const registration = await Registration.create({
+ userId,
+ examId,
+ paymentStatus: 'pending'
+ });
+
+ res.status(201).json(registration);
+});
+
+// @desc Update payment status
+// @access Private
+const updatePaymentStatus = asyncHandler(async (req, res) => {
+ const { paymentStatus } = req.body;
+ const userId = req.user.id;
+
+ const registration = await Registration.findOne({
+ where: { id: req.params.id, userId }
+ });
+
+ Iif (!registration) {
+ res.status(404);
+ throw new Error('Registration not found');
+ }
+
+ registration.paymentStatus = paymentStatus;
+ await registration.save();
+
+ res.status(200).json(registration);
+});
+
+// @desc Get user's registrations
+// @access Private
+const getUserRegistrations = asyncHandler(async (req, res) => {
+ const registrations = await Registration.findAll({
+ where: { userId: req.user.id },
+ include: [
+ {
+ model: Exam,
+ attributes: ['id', 'name', 'category', 'date', 'duration', 'passingScore']
+ }
+ ]
+ });
+
+ res.status(200).json(registrations);
+});
+
+// @desc Validate exam code
+// @access Public
+const validateExamCode = asyncHandler(async (req, res) => {
+ const errors = validationResult(req);
+ Iif (!errors.isEmpty()) {
+ return res.status(400).json({ errors: errors.array() });
+ }
+
+ const { email, examCode } = req.body;
+
+ // Find user by email
+ const user = await User.findOne({ where: { email } });
+ Iif (!user) {
+ res.status(404);
+ throw new Error('User not found');
+ }
+
+ // Find registration by exam code and user
+ const registration = await Registration.findOne({
+ where: {
+ examCode,
+ userId: user.id,
+ paymentStatus: 'completed'
+ },
+ include: [
+ {
+ model: Exam,
+ attributes: { exclude: ['createdAt', 'updatedAt'] }
+ }
+ ]
+ });
+
+ Iif (!registration) {
+ res.status(404);
+ throw new Error('Invalid exam code or payment not completed');
+ }
+
+ res.status(200).json({
+ registration,
+ exam: registration.Exam,
+ user: {
+ id: user.id,
+ fullName: user.fullName,
+ email: user.email
+ }
+ });
+});
+
+export {
+ registerForExam,
+ updatePaymentStatus,
+ getUserRegistrations,
+ validateExamCode
+}; |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { Result, Registration, Exam, Question, Option } from '../models/index.js';
+import asyncHandler from 'express-async-handler';
+
+// @desc Submit exam and create result
+// @access Private
+const submitExam = asyncHandler(async (req, res) => {
+ const { registrationId, answers } = req.body;
+ const userId = req.user.id;
+
+ // Find registration
+ const registration = await Registration.findOne({
+ where: { id: registrationId, userId },
+ include: [{ model: Exam }]
+ });
+
+ Iif (!registration) {
+ res.status(404);
+ throw new Error('Registration not found');
+ }
+
+ Iif (registration.status === 'completed') {
+ res.status(400);
+ throw new Error('Exam already completed');
+ }
+
+ // Get all questions for this exam
+ const questions = await Question.findAll({
+ where: { examId: registration.examId },
+ include: [{ model: Option }]
+ });
+
+ // Calculate score
+ let correctAnswers = 0;
+ const processedAnswers = [];
+
+ for (const answer of answers) {
+ const question = questions.find(q => q.id === answer.questionId);
+ Iif (!question) continue;
+
+ const correctOption = question.Options.find(opt => opt.isCorrect);
+ const isCorrect = correctOption && correctOption.id === answer.selectedOption;
+
+ Iif (isCorrect) {
+ correctAnswers++;
+ }
+
+ processedAnswers.push({
+ questionId: answer.questionId,
+ selectedOption: answer.selectedOption,
+ isCorrect
+ });
+ }
+
+ const totalQuestions = questions.length;
+ const score = (correctAnswers / totalQuestions) * 100;
+ const passed = score >= registration.Exam.passingScore;
+
+ // Create result
+ const result = await Result.create({
+ registrationId,
+ userId,
+ examId: registration.examId,
+ score,
+ passed,
+ answers: processedAnswers
+ });
+
+ // Update registration status
+ registration.status = 'completed';
+ await registration.save();
+
+ res.status(201).json(result);
+});
+
+// @desc Get user's results
+// @access Private
+const getUserResults = asyncHandler(async (req, res) => {
+ const results = await Result.findAll({
+ where: { userId: req.user.id },
+ include: [
+ {
+ model: Exam,
+ attributes: ['id', 'name', 'category', 'passingScore', 'certification']
+ }
+ ]
+ });
+
+ res.status(200).json(results);
+});
+
+// @desc Get result by ID
+// @access Private
+const getResultById = asyncHandler(async (req, res) => {
+ const result = await Result.findOne({
+ where: { id: req.params.id, userId: req.user.id },
+ include: [
+ {
+ model: Exam,
+ attributes: { exclude: ['createdAt', 'updatedAt'] }
+ }
+ ]
+ });
+
+ Iif (!result) {
+ res.status(404);
+ throw new Error('Result not found');
+ }
+
+ res.status(200).json(result);
+});
+
+// @desc Generate certificate for a result
+// @access Private
+const generateCertificate = asyncHandler(async (req, res) => {
+ const result = await Result.findOne({
+ where: { id: req.params.id, userId: req.user.id, passed: true },
+ include: [
+ {
+ model: Exam,
+ attributes: ['name', 'category', 'certification']
+ },
+ {
+ model: User,
+ attributes: ['fullName']
+ }
+ ]
+ });
+
+ Iif (!result) {
+ res.status(404);
+ throw new Error('Result not found or exam not passed');
+ }
+
+ Iif (!result.Exam.certification) {
+ res.status(400);
+ throw new Error('This exam does not provide certification');
+ }
+
+ // In a real application, you would generate a PDF certificate here
+ // For now, we'll just return the certificate data
+ const certificateData = {
+ certificateId: `CERT-${result.id}`,
+ candidateName: result.User.fullName,
+ examName: result.Exam.name,
+ category: result.Exam.category,
+ score: result.score,
+ issueDate: new Date(),
+ verificationUrl: `https://skillnet.example.com/verify/${result.id}`
+ };
+
+ res.status(200).json(certificateData);
+});
+
+export {
+ submitExam,
+ getUserResults,
+ getResultById,
+ generateCertificate
+}; |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 | + + + + +1x +2x + + + +2x +1x +1x + + +1x + + + + +1x +4x +4x +1x +1x + +3x + +3x + + + + +3x + + +3x +3x + +3x + +2x + + + + + + + +1x +1x + + + + + +1x +2x + + + + +2x +1x +1x + + +1x + + + + +1x +1x + + + +1x + + + + + + + + | import { User } from '../models/index.js';
+import asyncHandler from 'express-async-handler';
+
+// @desc Get user profile
+// @access Private
+const getUserProfile = asyncHandler(async (req, res) => {
+ const user = await User.findByPk(req.user.id, {
+ attributes: { exclude: ['password'] }
+ });
+
+ if (!user) {
+ res.status(404);
+ throw new Error('User not found');
+ }
+
+ res.status(200).json(user);
+});
+
+// @desc Update user profile
+// @access Private
+const updateUserProfile = asyncHandler(async (req, res) => {
+ try {
+ if (!req.body) {
+ res.status(400);
+ return res.json({ message: 'Request body is required' });
+ }
+ const user = await User.findByPk(req.user.id);
+
+ Iif (!user) {
+ res.status(404);
+ throw new Error('User not found');
+ }
+
+ const { fullName, email } = req.body;
+
+ // Update fields if provided
+ if (fullName) user.fullName = fullName;
+ if (email) user.email = email;
+
+ await user.save();
+
+ res.status(200).json({
+ id: user.id,
+ fullName: user.fullName,
+ email: user.email,
+ walletAddress: user.walletAddress,
+ role: user.role
+ });
+ } catch (error) {
+ res.status(500);
+ throw new Error('Error updating user profile: ' + error.message);
+ }
+});
+
+// @desc Get user by wallet address
+// @access Public
+const getUserByWallet = asyncHandler(async (req, res) => {
+ const user = await User.findOne({
+ where: { walletAddress: req.params.address },
+ attributes: { exclude: ['password'] }
+ });
+
+ if (!user) {
+ res.status(404);
+ throw new Error('User not found');
+ }
+
+ res.status(200).json(user);
+});
+
+// @desc Get all users
+// @access Private/Admin
+const getUsers = asyncHandler(async (req, res) => {
+ const users = await User.findAll({
+ attributes: { exclude: ['password'] }
+ });
+
+ res.status(200).json(users);
+});
+
+export {
+ getUserProfile,
+ updateUserProfile,
+ getUserByWallet,
+ getUsers
+}; |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 | + + + + + + + +1x +3x + +3x +3x +1x + + +3x + + + + + + + + + +3x + + + + + +1x + + + + + + + + + + + + + + + + + + + + + + + + + +1x + + + + + + + + + + + + + + + + + + +1x +6x +6x +6x +1x + + + +5x +5x + +1x + + +4x +4x +2x +2x + + + +1x +1x +2x + + + + + + + + +1x +1x + +1x + + +3x + + + +3x + + + + + + + + +1x +3x + +3x + + + + + + + + + + + + + + + + + +3x + + +3x + + + + + + + + + + + + + + + +3x + + + + + + + + +3x + + + + + +1x +2x + +2x +1x +1x + + + +1x + +1x +1x + + +1x + + +1x + +1x + + + + + + + + + + | import { Exam, Question, Option } from '../models/index.js';
+import { Op } from 'sequelize';
+import asyncHandler from 'express-async-handler';
+import { validationResult } from 'express-validator';
+
+// @desc Get all exams
+// @route GET /api/exams
+// @access Public
+const getExams = asyncHandler(async (req, res) => {
+ const { category } = req.query;
+
+ const whereClause = {};
+ if (category) {
+ whereClause.category = category;
+ }
+
+ const exams = await Exam.findAll({
+ where: whereClause,
+ include: [
+ {
+ model: Question,
+ attributes: ['id'], // Only count questions, don't return them
+ },
+ ],
+ });
+
+ res.status(200).json(exams);
+});
+
+// @desc Get exam by ID
+// @route GET /api/exams/:id
+// @access Public
+const getExamById = asyncHandler(async (req, res) => {
+ const exam = await Exam.findByPk(req.params.id, {
+ include: [
+ {
+ model: Question,
+ include: [
+ {
+ model: Option,
+ attributes: ['id', 'text', 'order'], // Don't expose correct answers
+ },
+ ],
+ },
+ ],
+ });
+
+ if (!exam) {
+ res.status(404);
+ throw new Error('Exam not found');
+ }
+
+ res.status(200).json(exam);
+});
+
+// @desc Get exams by category
+// @route GET /api/exams/category/:category
+// @access Public
+const getExamsByCategory = asyncHandler(async (req, res) => {
+ const { category } = req.params;
+
+ const exams = await Exam.findAll({
+ where: { category },
+ include: [
+ {
+ model: Question,
+ attributes: ['id'], // Only count questions, don't return them
+ },
+ ],
+ });
+
+ res.status(200).json(exams);
+});
+
+// @desc Create new exam
+// @route POST /api/exams
+// @access Private (Admin)
+const createExam = asyncHandler(async (req, res, next) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({ errors: errors.array() });
+ }
+
+ let exam;
+ try {
+ exam = await Exam.create(req.body);
+ } catch (error) {
+ return next({ message: `Error creating exam: ${error.message}` });
+ }
+
+ try {
+ if (req.body.questions && Array.isArray(req.body.questions) && req.body.questions.length > 0) {
+ for (const question of req.body.questions) {
+ const createdQuestion = await Question.create({
+ ...question,
+ examId: exam.id
+ });
+ Eif (question.options && Array.isArray(question.options) && question.options.length > 0) {
+ for (const option of question.options) {
+ await Option.create({
+ ...option,
+ questionId: createdQuestion.id
+ });
+ }
+ }
+ }
+ }
+ } catch (error) {
+ Eif (exam && typeof exam.destroy === 'function') {
+ await exam.destroy();
+ }
+ return next({ message: `Error creating exam: ${error.message}` });
+ }
+
+ const createdExam = await Exam.findByPk(exam.id, {
+ include: [{ model: Question, include: [Option] }]
+ });
+
+ res.status(201).json(createdExam);
+ } catch (error) {
+ next({ message: `Error creating exam: ${error.message}` });
+ }
+});
+
+// @desc Update exam
+// @route PUT /api/exams/:id
+// @access Private (Admin)
+const updateExam = asyncHandler(async (req, res) => {
+ const exam = await Exam.findByPk(req.params.id);
+
+ Iif (!exam) {
+ res.status(404);
+ throw new Error('Exam not found');
+ }
+
+ const {
+ name,
+ description,
+ category,
+ date,
+ duration,
+ certification,
+ passingScore,
+ format,
+ topicsCovered,
+ benefits,
+ price,
+ instructions,
+ } = req.body;
+
+ // Update exam details
+ await exam.update({
+ name: name || exam.name,
+ description: description || exam.description,
+ category: category || exam.category,
+ date: date || exam.date,
+ duration: duration || exam.duration,
+ certification: certification !== undefined ? certification : exam.certification,
+ passingScore: passingScore || exam.passingScore,
+ format: format || exam.format,
+ topicsCovered: topicsCovered || exam.topicsCovered,
+ benefits: benefits || exam.benefits,
+ price: price || exam.price,
+ instructions: instructions || exam.instructions,
+ });
+
+ // Get updated exam with questions
+ const updatedExam = await Exam.findByPk(exam.id, {
+ include: [
+ {
+ model: Question,
+ include: [Option],
+ },
+ ],
+ });
+
+ res.status(200).json(updatedExam);
+});
+
+// @desc Delete exam
+// @route DELETE /api/exams/:id
+// @access Private (Admin)
+const deleteExam = asyncHandler(async (req, res) => {
+ const exam = await Exam.findByPk(req.params.id);
+
+ if (!exam) {
+ res.status(404);
+ throw new Error('Exam not found');
+ }
+
+ // Delete associated questions and options
+ const questions = await Question.findAll({ where: { examId: exam.id } });
+
+ for (const question of questions) {
+ await Option.destroy({ where: { questionId: question.id } });
+ }
+
+ await Question.destroy({ where: { examId: exam.id } });
+
+ // Delete the exam
+ await exam.destroy();
+
+ res.status(200).json({ id: req.params.id });
+});
+
+export {
+ getExams,
+ getExamById,
+ getExamsByCategory,
+ createExam,
+ updateExam,
+ deleteExam,
+}; |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { ExamBanner, Exam } from '../models/index.js';
+import asyncHandler from 'express-async-handler';
+import { validationResult } from 'express-validator';
+import logger from '../utils/logger.js';
+
+// @desc Get all exam banners
+// @route GET /api/exam-banners
+// @access Public
+const getExamBanners = asyncHandler(async (req, res) => {
+ try {
+ const { active } = req.query;
+
+ const whereClause = {};
+ if (active === 'true') {
+ const now = new Date();
+ whereClause.isActive = true;
+ whereClause.startDate = { $lte: now }; // Start date is less than or equal to now
+ whereClause.endDate = { $gte: now }; // End date is greater than or equal to now
+ }
+
+ logger.info(`Fetching exam banners with filters: ${JSON.stringify(whereClause)}`);
+
+ const examBanners = await ExamBanner.findAll({
+ where: whereClause,
+ include: [
+ {
+ model: Exam,
+ as: 'exam',
+ attributes: ['id', 'name', 'category'],
+ },
+ ],
+ order: [
+ ['priority', 'DESC'],
+ ['startDate', 'DESC'],
+ ],
+ });
+
+ logger.info(`Retrieved ${examBanners.length} exam banners`);
+ res.status(200).json(examBanners);
+ } catch (error) {
+ logger.error(`Error retrieving exam banners: ${error.message}`, { error: error.stack });
+ res.status(500);
+ throw new Error('Error retrieving exam banners');
+ }
+});
+
+// @desc Get exam banner by ID
+// @route GET /api/exam-banners/:id
+// @access Public
+const getExamBannerById = asyncHandler(async (req, res) => {
+ try {
+ logger.info(`Fetching exam banner with id: ${req.params.id}`);
+
+ const examBanner = await ExamBanner.findByPk(req.params.id, {
+ include: [
+ {
+ model: Exam,
+ as: 'exam',
+ attributes: ['id', 'name', 'category', 'description'],
+ },
+ ],
+ });
+
+ if (!examBanner) {
+ logger.warn(`Exam banner with id ${req.params.id} not found`, { bannerId: req.params.id });
+ res.status(404);
+ throw new Error('Exam banner not found');
+ }
+
+ logger.info(`Retrieved exam banner ${examBanner.id}`);
+ res.status(200).json(examBanner);
+ } catch (error) {
+ if (error.message === 'Exam banner not found') {
+ throw error;
+ }
+ logger.error(`Error retrieving exam banner ${req.params.id}: ${error.message}`, {
+ bannerId: req.params.id,
+ error: error.stack
+ });
+ res.status(500);
+ throw new Error('Error retrieving exam banner');
+ }
+});
+
+// @desc Create new exam banner
+// @route POST /api/exam-banners
+// @access Private (Admin)
+const createExamBanner = asyncHandler(async (req, res) => {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ logger.warn(`Validation errors in creating exam banner: ${JSON.stringify(errors.array())}`, {
+ userId: req.user.id,
+ validationErrors: errors.array()
+ });
+ return res.status(400).json({ errors: errors.array() });
+ }
+
+ const {
+ title,
+ description,
+ imageUrl,
+ startDate,
+ endDate,
+ isActive,
+ examId,
+ buttonText,
+ buttonLink,
+ priority,
+ } = req.body;
+
+ logger.info(`Admin ${req.user.id} creating exam banner: "${title}"`);
+
+ try {
+ // Check if exam exists if examId is provided
+ if (examId) {
+ const exam = await Exam.findByPk(examId);
+ if (!exam) {
+ logger.warn(`Exam with id ${examId} not found while admin ${req.user.id} creating banner`, {
+ examId,
+ adminId: req.user.id
+ });
+ res.status(404);
+ throw new Error('Exam not found');
+ }
+ }
+
+ // Create banner
+ const banner = await ExamBanner.create({
+ title,
+ description,
+ imageUrl,
+ startDate,
+ endDate,
+ isActive: isActive !== undefined ? isActive : true,
+ examId,
+ buttonText,
+ buttonLink,
+ priority: priority || 0,
+ });
+
+ logger.info(`Created exam banner ${banner.id} by admin ${req.user.id}`, {
+ bannerId: banner.id,
+ adminId: req.user.id,
+ examId: examId || 'none'
+ });
+
+ // Return the created banner with exam info
+ const createdBanner = await ExamBanner.findByPk(banner.id, {
+ include: [
+ {
+ model: Exam,
+ as: 'exam',
+ attributes: ['id', 'name', 'category'],
+ },
+ ],
+ });
+
+ res.status(201).json(createdBanner);
+ } catch (error) {
+ if (error.message === 'Exam not found') {
+ throw error;
+ }
+ logger.error(`Error creating exam banner by admin ${req.user.id}: ${error.message}`, {
+ adminId: req.user.id,
+ examId,
+ error: error.stack
+ });
+ res.status(500);
+ throw new Error('Error creating exam banner');
+ }
+});
+
+// @desc Update exam banner
+// @route PUT /api/exam-banners/:id
+// @access Private (Admin)
+const updateExamBanner = asyncHandler(async (req, res) => {
+ try {
+ logger.info(`Admin ${req.user.id} updating exam banner ${req.params.id}`);
+
+ const banner = await ExamBanner.findByPk(req.params.id);
+
+ if (!banner) {
+ logger.warn(`Exam banner with id ${req.params.id} not found for update by admin ${req.user.id}`, {
+ bannerId: req.params.id,
+ adminId: req.user.id
+ });
+ res.status(404);
+ throw new Error('Exam banner not found');
+ }
+
+ const {
+ title,
+ description,
+ imageUrl,
+ startDate,
+ endDate,
+ isActive,
+ examId,
+ buttonText,
+ buttonLink,
+ priority,
+ } = req.body;
+
+ // If examId is being changed, verify the new exam exists
+ if (examId && examId !== banner.examId) {
+ const exam = await Exam.findByPk(examId);
+ if (!exam) {
+ logger.warn(`Exam with id ${examId} not found while admin ${req.user.id} updating banner ${req.params.id}`, {
+ examId,
+ bannerId: req.params.id,
+ adminId: req.user.id
+ });
+ res.status(404);
+ throw new Error('Exam not found');
+ }
+ }
+
+ // Update banner
+ await banner.update({
+ title: title || banner.title,
+ description: description !== undefined ? description : banner.description,
+ imageUrl: imageUrl !== undefined ? imageUrl : banner.imageUrl,
+ startDate: startDate || banner.startDate,
+ endDate: endDate || banner.endDate,
+ isActive: isActive !== undefined ? isActive : banner.isActive,
+ examId: examId || banner.examId,
+ buttonText: buttonText !== undefined ? buttonText : banner.buttonText,
+ buttonLink: buttonLink !== undefined ? buttonLink : banner.buttonLink,
+ priority: priority !== undefined ? priority : banner.priority,
+ });
+
+ logger.info(`Updated exam banner ${banner.id} by admin ${req.user.id}`, {
+ bannerId: banner.id,
+ adminId: req.user.id,
+ changes: {
+ title: !!title,
+ description: description !== undefined,
+ imageUrl: imageUrl !== undefined,
+ startDate: !!startDate,
+ endDate: !!endDate,
+ isActive: isActive !== undefined,
+ examId: !!examId && examId !== banner.examId,
+ buttonText: buttonText !== undefined,
+ buttonLink: buttonLink !== undefined,
+ priority: priority !== undefined
+ }
+ });
+
+ // Return the updated banner with exam info
+ const updatedBanner = await ExamBanner.findByPk(banner.id, {
+ include: [
+ {
+ model: Exam,
+ as: 'exam',
+ attributes: ['id', 'name', 'category'],
+ },
+ ],
+ });
+
+ res.status(200).json(updatedBanner);
+ } catch (error) {
+ if (error.message === 'Exam banner not found' || error.message === 'Exam not found') {
+ throw error;
+ }
+ logger.error(`Error updating exam banner ${req.params.id} by admin ${req.user.id}: ${error.message}`, {
+ bannerId: req.params.id,
+ adminId: req.user.id,
+ error: error.stack
+ });
+ res.status(500);
+ throw new Error('Error updating exam banner');
+ }
+});
+
+// @desc Delete exam banner
+// @route DELETE /api/exam-banners/:id
+// @access Private (Admin)
+const deleteExamBanner = asyncHandler(async (req, res) => {
+ try {
+ logger.info(`Admin ${req.user.id} deleting exam banner ${req.params.id}`);
+
+ const banner = await ExamBanner.findByPk(req.params.id);
+
+ if (!banner) {
+ logger.warn(`Exam banner with id ${req.params.id} not found for deletion by admin ${req.user.id}`, {
+ bannerId: req.params.id,
+ adminId: req.user.id
+ });
+ res.status(404);
+ throw new Error('Exam banner not found');
+ }
+
+ // Delete banner
+ await banner.destroy();
+
+ logger.info(`Deleted exam banner ${req.params.id} by admin ${req.user.id}`, {
+ bannerId: req.params.id,
+ adminId: req.user.id,
+ bannerData: {
+ title: banner.title,
+ examId: banner.examId,
+ isActive: banner.isActive
+ }
+ });
+ res.status(200).json({ id: req.params.id });
+ } catch (error) {
+ if (error.message === 'Exam banner not found') {
+ throw error;
+ }
+ logger.error(`Error deleting exam banner ${req.params.id} by admin ${req.user.id}: ${error.message}`, {
+ bannerId: req.params.id,
+ adminId: req.user.id,
+ error: error.stack
+ });
+ res.status(500);
+ throw new Error('Error deleting exam banner');
+ }
+});
+
+export {
+ getExamBanners,
+ getExamBannerById,
+ createExamBanner,
+ updateExamBanner,
+ deleteExamBanner,
+};
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { ExamRecording, Exam } from '../models/index.js';
+import asyncHandler from 'express-async-handler';
+import { validationResult } from 'express-validator';
+import logger from '../utils/logger.js';
+
+// @desc Get all exam recordings
+// @route GET /api/exam-recordings
+// @access Public
+const getExamRecordings = asyncHandler(async (req, res) => {
+ try {
+ const { examId, published } = req.query;
+
+ const whereClause = {};
+ if (examId) {
+ whereClause.examId = examId;
+ }
+ if (published === 'true') {
+ whereClause.isPublished = true;
+ } else if (published === 'false') {
+ whereClause.isPublished = false;
+ }
+
+ logger.info(`Fetching exam recordings with filters: ${JSON.stringify(whereClause)}`);
+
+ const examRecordings = await ExamRecording.findAll({
+ where: whereClause,
+ include: [
+ {
+ model: Exam,
+ attributes: ['id', 'name', 'category'],
+ },
+ ],
+ order: [
+ ['recordedOn', 'DESC'],
+ ],
+ });
+
+ logger.info(`Retrieved ${examRecordings.length} exam recordings`);
+ res.status(200).json(examRecordings);
+ } catch (error) {
+ logger.error(`Error retrieving exam recordings: ${error.message}`, { error: error.stack });
+ res.status(500);
+ throw new Error('Error retrieving exam recordings');
+ }
+});
+
+// @desc Get exam recording by ID
+// @route GET /api/exam-recordings/:id
+// @access Public
+const getExamRecordingById = asyncHandler(async (req, res) => {
+ try {
+ logger.info(`Fetching exam recording with id: ${req.params.id}`);
+
+ const examRecording = await ExamRecording.findByPk(req.params.id, {
+ include: [
+ {
+ model: Exam,
+ attributes: ['id', 'name', 'category', 'description'],
+ },
+ ],
+ });
+
+ if (!examRecording) {
+ logger.warn(`Exam recording with id ${req.params.id} not found`, { recordingId: req.params.id });
+ res.status(404);
+ throw new Error('Exam recording not found');
+ }
+
+ logger.info(`Retrieved exam recording ${examRecording.id}`);
+ res.status(200).json(examRecording);
+ } catch (error) {
+ if (error.message === 'Exam recording not found') {
+ throw error;
+ }
+ logger.error(`Error retrieving exam recording ${req.params.id}: ${error.message}`, {
+ recordingId: req.params.id,
+ error: error.stack
+ });
+ res.status(500);
+ throw new Error('Error retrieving exam recording');
+ }
+});
+
+// @desc Create new exam recording
+// @route POST /api/exam-recordings
+// @access Private (Admin)
+const createExamRecording = asyncHandler(async (req, res) => {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ logger.warn(`Validation errors in creating exam recording: ${JSON.stringify(errors.array())}`, {
+ userId: req.user.id,
+ validationErrors: errors.array()
+ });
+ return res.status(400).json({ errors: errors.array() });
+ }
+
+ const {
+ title,
+ description,
+ recordingUrl,
+ duration,
+ examId,
+ presenter,
+ recordedOn,
+ isPublished,
+ thumbnailUrl,
+ tags,
+ } = req.body;
+
+ logger.info(`Admin ${req.user.id} creating exam recording: "${title}"`);
+
+ try {
+ // Check if exam exists
+ const exam = await Exam.findByPk(examId);
+ if (!exam) {
+ logger.warn(`Exam with id ${examId} not found while admin ${req.user.id} creating recording`, {
+ examId,
+ adminId: req.user.id
+ });
+ res.status(404);
+ throw new Error('Exam not found');
+ }
+
+ // Create recording
+ const recording = await ExamRecording.create({
+ title,
+ description,
+ recordingUrl,
+ duration,
+ examId,
+ presenter,
+ recordedOn: recordedOn || new Date(),
+ isPublished: isPublished !== undefined ? isPublished : false,
+ thumbnailUrl,
+ tags: tags || [],
+ });
+
+ logger.info(`Created exam recording ${recording.id} by admin ${req.user.id}`, {
+ recordingId: recording.id,
+ adminId: req.user.id,
+ examId: examId,
+ isPublished: isPublished !== undefined ? isPublished : false
+ });
+
+ // Return the created recording with exam info
+ const createdRecording = await ExamRecording.findByPk(recording.id, {
+ include: [
+ {
+ model: Exam,
+ attributes: ['id', 'name', 'category'],
+ },
+ ],
+ });
+
+ res.status(201).json(createdRecording);
+ } catch (error) {
+ if (error.message === 'Exam not found') {
+ throw error;
+ }
+ logger.error(`Error creating exam recording by admin ${req.user.id}: ${error.message}`, {
+ adminId: req.user.id,
+ examId,
+ error: error.stack
+ });
+ res.status(500);
+ throw new Error('Error creating exam recording');
+ }
+});
+
+// @desc Update exam recording
+// @route PUT /api/exam-recordings/:id
+// @access Private (Admin)
+const updateExamRecording = asyncHandler(async (req, res) => {
+ try {
+ logger.info(`Admin ${req.user.id} updating exam recording ${req.params.id}`);
+
+ const recording = await ExamRecording.findByPk(req.params.id);
+
+ if (!recording) {
+ logger.warn(`Exam recording with id ${req.params.id} not found for update by admin ${req.user.id}`, {
+ recordingId: req.params.id,
+ adminId: req.user.id
+ });
+ res.status(404);
+ throw new Error('Exam recording not found');
+ }
+
+ const {
+ title,
+ description,
+ recordingUrl,
+ duration,
+ examId,
+ presenter,
+ recordedOn,
+ isPublished,
+ thumbnailUrl,
+ tags,
+ } = req.body;
+
+ // If examId is being changed, verify the new exam exists
+ if (examId && examId !== recording.examId) {
+ const exam = await Exam.findByPk(examId);
+ if (!exam) {
+ logger.warn(`Exam with id ${examId} not found while admin ${req.user.id} updating recording ${req.params.id}`, {
+ examId,
+ recordingId: req.params.id,
+ adminId: req.user.id
+ });
+ res.status(404);
+ throw new Error('Exam not found');
+ }
+ }
+
+ // Update recording
+ await recording.update({
+ title: title || recording.title,
+ description: description !== undefined ? description : recording.description,
+ recordingUrl: recordingUrl || recording.recordingUrl,
+ duration: duration !== undefined ? duration : recording.duration,
+ examId: examId || recording.examId,
+ presenter: presenter !== undefined ? presenter : recording.presenter,
+ recordedOn: recordedOn || recording.recordedOn,
+ isPublished: isPublished !== undefined ? isPublished : recording.isPublished,
+ thumbnailUrl: thumbnailUrl !== undefined ? thumbnailUrl : recording.thumbnailUrl,
+ tags: tags || recording.tags,
+ });
+
+ logger.info(`Updated exam recording ${recording.id} by admin ${req.user.id}`, {
+ recordingId: recording.id,
+ adminId: req.user.id,
+ changes: {
+ title: !!title,
+ description: description !== undefined,
+ recordingUrl: !!recordingUrl,
+ duration: duration !== undefined,
+ examId: !!examId && examId !== recording.examId,
+ presenter: presenter !== undefined,
+ recordedOn: !!recordedOn,
+ isPublished: isPublished !== undefined,
+ thumbnailUrl: thumbnailUrl !== undefined,
+ tags: !!tags
+ }
+ });
+
+ // Return the updated recording with exam info
+ const updatedRecording = await ExamRecording.findByPk(recording.id, {
+ include: [
+ {
+ model: Exam,
+ attributes: ['id', 'name', 'category'],
+ },
+ ],
+ });
+
+ res.status(200).json(updatedRecording);
+ } catch (error) {
+ if (error.message === 'Exam recording not found' || error.message === 'Exam not found') {
+ throw error;
+ }
+ logger.error(`Error updating exam recording ${req.params.id} by admin ${req.user.id}: ${error.message}`, {
+ recordingId: req.params.id,
+ adminId: req.user.id,
+ error: error.stack
+ });
+ res.status(500);
+ throw new Error('Error updating exam recording');
+ }
+});
+
+// @desc Delete exam recording
+// @route DELETE /api/exam-recordings/:id
+// @access Private (Admin)
+const deleteExamRecording = asyncHandler(async (req, res) => {
+ try {
+ logger.info(`Admin ${req.user.id} deleting exam recording ${req.params.id}`);
+
+ const recording = await ExamRecording.findByPk(req.params.id);
+
+ if (!recording) {
+ logger.warn(`Exam recording with id ${req.params.id} not found for deletion by admin ${req.user.id}`, {
+ recordingId: req.params.id,
+ adminId: req.user.id
+ });
+ res.status(404);
+ throw new Error('Exam recording not found');
+ }
+
+ // Delete recording
+ await recording.destroy();
+
+ logger.info(`Deleted exam recording ${req.params.id} by admin ${req.user.id}`, {
+ recordingId: req.params.id,
+ adminId: req.user.id,
+ recordingData: {
+ title: recording.title,
+ examId: recording.examId,
+ isPublished: recording.isPublished
+ }
+ });
+ res.status(200).json({ id: req.params.id });
+ } catch (error) {
+ if (error.message === 'Exam recording not found') {
+ throw error;
+ }
+ logger.error(`Error deleting exam recording ${req.params.id} by admin ${req.user.id}: ${error.message}`, {
+ recordingId: req.params.id,
+ adminId: req.user.id,
+ error: error.stack
+ });
+ res.status(500);
+ throw new Error('Error deleting exam recording');
+ }
+});
+
+// @desc Toggle publish status of an exam recording
+// @route PATCH /api/exam-recordings/:id/publish
+// @access Private (Admin)
+const togglePublishStatus = asyncHandler(async (req, res) => {
+ try {
+ logger.info(`Admin ${req.user.id} toggling publish status for exam recording ${req.params.id}`);
+
+ const recording = await ExamRecording.findByPk(req.params.id);
+
+ if (!recording) {
+ logger.warn(`Exam recording with id ${req.params.id} not found for status toggle by admin ${req.user.id}`, {
+ recordingId: req.params.id,
+ adminId: req.user.id
+ });
+ res.status(404);
+ throw new Error('Exam recording not found');
+ }
+
+ // Toggle publish status
+ const newStatus = !recording.isPublished;
+ await recording.update({
+ isPublished: newStatus,
+ });
+
+ logger.info(`Toggled exam recording ${recording.id} publish status to ${newStatus} by admin ${req.user.id}`, {
+ recordingId: recording.id,
+ adminId: req.user.id,
+ newStatus: newStatus,
+ previousStatus: !newStatus
+ });
+
+ res.status(200).json(recording);
+ } catch (error) {
+ if (error.message === 'Exam recording not found') {
+ throw error;
+ }
+ logger.error(`Error toggling exam recording ${req.params.id} publish status by admin ${req.user.id}: ${error.message}`, {
+ recordingId: req.params.id,
+ adminId: req.user.id,
+ error: error.stack
+ });
+ res.status(500);
+ throw new Error('Error toggling publish status');
+ }
+});
+
+export {
+ getExamRecordings,
+ getExamRecordingById,
+ createExamRecording,
+ updateExamRecording,
+ deleteExamRecording,
+ togglePublishStatus,
+};
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| exam.controller.js | +
+
+ |
+ 81.03% | +47/58 | +85.41% | +41/48 | +66.66% | +4/6 | +81.03% | +47/58 | +
| user.controller.js | +
+
+ |
+ 93.93% | +31/33 | +83.33% | +10/12 | +100% | +4/4 | +93.54% | +29/31 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { ContractEvent } from '../models/index.js';
+import asyncHandler from 'express-async-handler';
+import { indexer } from '../indexer/indexer.js';
+import { indexerConfig, apibaraConfig, networks, contracts, eventHandlers, eventSelectors } from '../indexer/config.js';
+import logger from '../utils/logger.js';
+import { Op } from 'sequelize';
+
+// @desc Get indexer status
+// @route GET /api/indexer/status
+// @access Private
+const getIndexerStatus = asyncHandler(async (req, res) => {
+ try {
+ logger.info('Fetching indexer status');
+
+ // Get the last processed block from DB
+ const lastEvent = await ContractEvent.findOne({
+ order: [['blockNumber', 'DESC']],
+ attributes: ['blockNumber', 'blockTimestamp', 'createdAt']
+ });
+
+ const status = {
+ lastProcessedBlock: lastEvent ? Number(lastEvent.blockNumber) : null,
+ lastBlockTimestamp: lastEvent ? new Date(Number(lastEvent.blockTimestamp) * 1000).toISOString() : null,
+ lastProcessingTime: lastEvent ? lastEvent.createdAt : null,
+ network: indexerConfig.network,
+ contractAddress: indexerConfig.contractAddress,
+ isRunning: indexer.isRunning(),
+ startBlock: Number(process.env.INDEXER_START_BLOCK) || 0
+ };
+
+ logger.info(`Indexer status retrieved: ${JSON.stringify(status)}`);
+ res.status(200).json(status);
+ } catch (error) {
+ logger.error(`Error retrieving indexer status: ${error.message}`, { error: error.stack });
+ res.status(500);
+ throw new Error('Error retrieving indexer status');
+ }
+});
+
+// @desc Trigger indexer to scan for events
+// @route POST /api/indexer/scan
+// @access Private (Admin)
+const triggerScan = asyncHandler(async (req, res) => {
+ try {
+ logger.info(`Admin ${req.user.id} triggering indexer scan`);
+
+ const { fromBlock, toBlock } = req.body;
+
+ // Convert 'latest' string to null to use default behavior
+ const from = fromBlock === 'latest' ? null : Number(fromBlock);
+ const to = toBlock === 'latest' ? null : Number(toBlock);
+
+ // Trigger a scan
+ const result = await indexer.scanForEvents(from, to);
+
+ logger.info(`Manual indexer scan completed by admin ${req.user.id}`, {
+ adminId: req.user.id,
+ eventsProcessed: result.events.length,
+ fromBlock: result.fromBlock,
+ toBlock: result.toBlock
+ });
+
+ res.status(200).json({
+ message: 'Scan triggered successfully',
+ eventsProcessed: result.events.length,
+ fromBlock: result.fromBlock,
+ toBlock: result.toBlock
+ });
+ } catch (error) {
+ logger.error(`Error triggering indexer scan by admin ${req.user.id}: ${error.message}`, {
+ adminId: req.user.id,
+ error: error.stack
+ });
+ res.status(500);
+ throw new Error('Error triggering indexer scan: ' + error.message);
+ }
+});
+
+// @desc Get contract events
+// @route GET /api/indexer/events
+// @access Private (Admin)
+const getContractEvents = asyncHandler(async (req, res) => {
+ try {
+ logger.info(`Admin ${req.user.id} retrieving contract events`);
+
+ const { eventName, fromBlock, toBlock, limit = 50 } = req.query;
+
+ const whereClause = {};
+ if (eventName) whereClause.eventName = eventName;
+ if (fromBlock) whereClause.blockNumber = { [Op.gte]: fromBlock };
+ if (toBlock) whereClause.blockNumber = {
+ ...whereClause.blockNumber,
+ [Op.lte]: toBlock
+ };
+
+ const events = await ContractEvent.findAll({
+ where: whereClause,
+ order: [['blockNumber', 'DESC'], ['logIndex', 'DESC']],
+ limit: parseInt(limit)
+ });
+
+ logger.info(`Retrieved ${events.length} contract events for admin ${req.user.id}`, {
+ adminId: req.user.id,
+ filter: { eventName, fromBlock, toBlock, limit }
+ });
+
+ res.status(200).json(events);
+ } catch (error) {
+ logger.error(`Error retrieving contract events for admin ${req.user.id}: ${error.message}`, {
+ adminId: req.user.id,
+ error: error.stack
+ });
+ res.status(500);
+ throw new Error('Error retrieving contract events');
+ }
+});
+
+// @desc Get indexed exams from blockchain
+// @route GET /api/indexer/exams
+// @access Private (Admin)
+const getIndexedExams = asyncHandler(async (req, res) => {
+ try {
+ logger.info(`Admin ${req.user.id} retrieving exams from indexed blockchain events`);
+
+ const events = await ContractEvent.findAll({
+ where: {
+ eventName: 'ExamCreated'
+ },
+ order: [['blockNumber', 'DESC']]
+ });
+
+ // Process and transform exam creation events
+ const exams = events.map(event => {
+ const { args } = JSON.parse(event.eventData);
+ return {
+ examId: args.examId,
+ name: args.name,
+ category: args.category,
+ creatorAddress: args.creator,
+ price: args.price,
+ blockNumber: event.blockNumber,
+ timestamp: event.blockTimestamp,
+ transactionHash: event.transactionHash
+ };
+ });
+
+ logger.info(`Retrieved ${exams.length} indexed exams for admin ${req.user.id}`);
+ res.status(200).json(exams);
+ } catch (error) {
+ logger.error(`Error retrieving indexed exams for admin ${req.user.id}: ${error.message}`, {
+ adminId: req.user.id,
+ error: error.stack
+ });
+ res.status(500);
+ throw new Error('Error retrieving indexed exams');
+ }
+});
+
+// @desc Get indexed registrations from blockchain
+// @route GET /api/indexer/registrations
+// @access Private (Admin)
+const getIndexedRegistrations = asyncHandler(async (req, res) => {
+ try {
+ logger.info(`Admin ${req.user.id} retrieving registrations from indexed blockchain events`);
+
+ const { examId } = req.query;
+
+ const whereClause = { eventName: 'UserRegistered' };
+ if (examId) {
+ // Filter by examId in the JSON event data
+ // This is a simplification - in actual implementation, you might need a more
+ // sophisticated query approach depending on your database
+ whereClause.$and = [
+ { eventData: { $like: `%"examId":"${examId}"%` } }
+ ];
+ }
+
+ const events = await ContractEvent.findAll({
+ where: whereClause,
+ order: [['blockNumber', 'DESC']]
+ });
+
+ // Process and transform registration events
+ const registrations = events.map(event => {
+ const { args } = JSON.parse(event.eventData);
+ return {
+ registrationId: args.registrationId,
+ examId: args.examId,
+ userAddress: args.user,
+ registrationTime: args.timestamp,
+ blockNumber: event.blockNumber,
+ timestamp: event.blockTimestamp,
+ transactionHash: event.transactionHash
+ };
+ });
+
+ logger.info(`Retrieved ${registrations.length} indexed registrations for admin ${req.user.id}`, {
+ adminId: req.user.id,
+ filter: { examId }
+ });
+ res.status(200).json(registrations);
+ } catch (error) {
+ logger.error(`Error retrieving indexed registrations for admin ${req.user.id}: ${error.message}`, {
+ adminId: req.user.id,
+ error: error.stack
+ });
+ res.status(500);
+ throw new Error('Error retrieving indexed registrations');
+ }
+});
+
+// @desc Get indexed exam results from blockchain
+// @route GET /api/indexer/results
+// @access Private (Admin)
+const getIndexedResults = asyncHandler(async (req, res) => {
+ try {
+ logger.info(`Admin ${req.user.id} retrieving exam results from indexed blockchain events`);
+
+ const { examId, userAddress } = req.query;
+
+ const whereClause = { eventName: 'ExamCompleted' };
+ // TODO: Implement proper filtering by examId and userAddress in the JSON
+
+ const events = await ContractEvent.findAll({
+ where: whereClause,
+ order: [['blockNumber', 'DESC']]
+ });
+
+ // Process and transform result events
+ const results = events.map(event => {
+ const { args } = JSON.parse(event.eventData);
+ return {
+ resultId: args.resultId,
+ examId: args.examId,
+ userAddress: args.user,
+ score: args.score,
+ passed: args.passed,
+ blockNumber: event.blockNumber,
+ timestamp: event.blockTimestamp,
+ transactionHash: event.transactionHash
+ };
+ });
+
+ logger.info(`Retrieved ${results.length} indexed exam results for admin ${req.user.id}`, {
+ adminId: req.user.id,
+ filter: { examId, userAddress }
+ });
+ res.status(200).json(results);
+ } catch (error) {
+ logger.error(`Error retrieving indexed exam results for admin ${req.user.id}: ${error.message}`, {
+ adminId: req.user.id,
+ error: error.stack
+ });
+ res.status(500);
+ throw new Error('Error retrieving indexed exam results');
+ }
+});
+
+export {
+ getIndexerStatus,
+ triggerScan,
+ getContractEvents,
+ getIndexedExams,
+ getIndexedRegistrations,
+ getIndexedResults,
+};
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | export default (sequelize, DataTypes) => { + const ContractEvent = sequelize.define('ContractEvent', { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + contractAddress: { + type: DataTypes.STRING, + allowNull: false, + }, + eventName: { + type: DataTypes.STRING, + allowNull: false, + }, + transactionHash: { + type: DataTypes.STRING, + allowNull: false, + }, + blockNumber: { + type: DataTypes.BIGINT, + allowNull: false, + }, + blockTimestamp: { + type: DataTypes.DATE, + allowNull: false, + }, + eventData: { + type: DataTypes.JSONB, + allowNull: false, + defaultValue: {}, + }, + processed: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false, + }, + processedAt: { + type: DataTypes.DATE, + allowNull: true, + } + }, { + timestamps: true, + indexes: [ + { + fields: ['contractAddress'], + }, + { + fields: ['eventName'], + }, + { + fields: ['blockNumber'], + }, + { + fields: ['processed'], + }, + { + unique: true, + fields: ['transactionHash', 'eventName', 'contractAddress'], + }, + ], + }); + + return ContractEvent; +}; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | export default (sequelize, DataTypes) => { + const Exam = sequelize.define('Exam', { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + name: { + type: DataTypes.STRING, + allowNull: false, + }, + description: { + type: DataTypes.TEXT, + allowNull: false, + }, + category: { + type: DataTypes.ENUM( + 'Javascript', + 'Data Science', + 'AI Development', + 'Frontend', + 'Cairo', + 'Solidity', + 'NextJS', + 'Others', + // Add more categories as needed + ), + allowNull: false, + }, + date: { + type: DataTypes.DATE, + allowNull: false, + }, + duration: { + type: DataTypes.INTEGER, // in minutes + allowNull: false, + }, + certification: { + type: DataTypes.BOOLEAN, + defaultValue: true, + }, + passingScore: { + type: DataTypes.FLOAT, + allowNull: false, + }, + format: { + type: DataTypes.STRING, + defaultValue: 'Multichoice', + }, + topicsCovered: { + type: DataTypes.ARRAY(DataTypes.STRING), + defaultValue: [], + }, + benefits: { + type: DataTypes.ARRAY(DataTypes.STRING), + defaultValue: [], + }, + price: { + type: DataTypes.FLOAT, + allowNull: false, + }, + instructions: { + type: DataTypes.ARRAY(DataTypes.STRING), + defaultValue: [], + }, + }, { + timestamps: true, + }); + + return Exam; + }; |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | export default (sequelize, DataTypes) => { + const ExamBanner = sequelize.define('ExamBanner', { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + title: { + type: DataTypes.STRING, + allowNull: false, + }, + description: { + type: DataTypes.TEXT, + allowNull: true, + }, + imageUrl: { + type: DataTypes.STRING, + allowNull: true, + }, + startDate: { + type: DataTypes.DATE, + allowNull: false, + }, + endDate: { + type: DataTypes.DATE, + allowNull: false, + }, + isActive: { + type: DataTypes.BOOLEAN, + defaultValue: true, + }, + examId: { + type: DataTypes.UUID, + allowNull: true, + references: { + model: 'Exams', + key: 'id', + }, + }, + buttonText: { + type: DataTypes.STRING, + defaultValue: 'Learn More', + }, + buttonLink: { + type: DataTypes.STRING, + allowNull: true, + }, + priority: { + type: DataTypes.INTEGER, + defaultValue: 1, + } + }, { + timestamps: true, + }); + + ExamBanner.associate = (models) => { + ExamBanner.belongsTo(models.Exam, { + foreignKey: 'examId', + as: 'exam', + }); + }; + + return ExamBanner; +}; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | export default (sequelize, DataTypes) => { + const ExamRecording = sequelize.define('ExamRecording', { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + title: { + type: DataTypes.STRING, + allowNull: false, + }, + description: { + type: DataTypes.TEXT, + allowNull: true, + }, + recordingUrl: { + type: DataTypes.STRING, + allowNull: false, + }, + duration: { + type: DataTypes.INTEGER, // in seconds + allowNull: true, + }, + examId: { + type: DataTypes.UUID, + allowNull: false, + references: { + model: 'Exams', + key: 'id', + }, + }, + presenter: { + type: DataTypes.STRING, + allowNull: true, + }, + recordedOn: { + type: DataTypes.DATE, + allowNull: true, + }, + isPublished: { + type: DataTypes.BOOLEAN, + defaultValue: false, + }, + thumbnailUrl: { + type: DataTypes.STRING, + allowNull: true, + }, + tags: { + type: DataTypes.ARRAY(DataTypes.STRING), + defaultValue: [], + } + }, { + timestamps: true, + }); + + ExamRecording.associate = (models) => { + ExamRecording.belongsTo(models.Exam, { + foreignKey: 'examId', + as: 'exam', + }); + }; + + return ExamRecording; +}; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| contractEvent.model.js | +
+
+ |
+ 0% | +0/2 | +100% | +0/0 | +0% | +0/1 | +0% | +0/2 | +
| exam.model.js | +
+
+ |
+ 0% | +0/2 | +100% | +0/0 | +0% | +0/1 | +0% | +0/2 | +
| examBanner.model.js | +
+
+ |
+ 0% | +0/4 | +100% | +0/0 | +0% | +0/2 | +0% | +0/4 | +
| examRecording.model.js | +
+
+ |
+ 0% | +0/4 | +100% | +0/0 | +0% | +0/2 | +0% | +0/4 | +
| index.js | +
+
+ |
+ 0% | +0/11 | +100% | +0/0 | +0% | +0/1 | +0% | +0/11 | +
| notification.model.js | +
+
+ |
+ 0% | +0/4 | +100% | +0/0 | +0% | +0/2 | +0% | +0/4 | +
| option.model.js | +
+
+ |
+ 0% | +0/4 | +100% | +0/0 | +0% | +0/2 | +0% | +0/4 | +
| question.model.js | +
+
+ |
+ 0% | +0/2 | +100% | +0/0 | +0% | +0/1 | +0% | +0/2 | +
| registration.model.js | +
+
+ |
+ 0% | +0/4 | +0% | +0/1 | +0% | +0/2 | +0% | +0/4 | +
| result.model.js | +
+
+ |
+ 0% | +0/2 | +100% | +0/0 | +0% | +0/1 | +0% | +0/2 | +
| user.model.js | +
+
+ |
+ 0% | +0/2 | +100% | +0/0 | +0% | +0/1 | +0% | +0/2 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 | + + + + + + + + + + + + + + + + + + + + + + + + + + + | import userModel from './user.model.js'; +import examModel from './exam.model.js'; +import registrationModel from './registration.model.js'; +import resultModel from './result.model.js'; +import questionModel from './question.model.js'; +import optionModel from './option.model.js'; +import notificationModel from './notification.model.js'; +import examBannerModel from './examBanner.model.js'; +import examRecordingModel from './examRecording.model.js'; +import contractEventModel from './contractEvent.model.js'; + +// Export model factories as named exports for mocking and test compatibility +export const User = userModel; +export const Exam = examModel; +export const Registration = registrationModel; +export const Result = resultModel; +export const Question = questionModel; +export const Option = optionModel; +export const Notification = notificationModel; +export const ExamBanner = examBannerModel; +export const ExamRecording = examRecordingModel; +export const ContractEvent = contractEventModel; + +export async function syncDatabase() { + // Dummy implementation for compatibility + return Promise.resolve(); +} + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | export default (sequelize, DataTypes) => { + const Notification = sequelize.define('Notification', { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + title: { + type: DataTypes.STRING, + allowNull: false, + }, + message: { + type: DataTypes.TEXT, + allowNull: false, + }, + type: { + type: DataTypes.ENUM( + 'info', + 'success', + 'warning', + 'error' + ), + allowNull: false, + defaultValue: 'info', + }, + isRead: { + type: DataTypes.BOOLEAN, + defaultValue: false, + }, + userId: { + type: DataTypes.UUID, + allowNull: true, + references: { + model: 'Users', + key: 'id', + }, + }, + expiresAt: { + type: DataTypes.DATE, + allowNull: true, + } + }, { + timestamps: true, + }); + + Notification.associate = (models) => { + Notification.belongsTo(models.User, { + foreignKey: 'userId', + as: 'user', + }); + }; + + return Notification; +}; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | export default (sequelize, DataTypes) => { + const Option = sequelize.define('Option', { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + questionId: { + type: DataTypes.UUID, + allowNull: false, + }, + text: { + type: DataTypes.TEXT, + allowNull: false, + }, + isCorrect: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false, + }, + order: { + type: DataTypes.STRING(1), // A, B, C, D + allowNull: false, + }, + }, { + timestamps: true, + }); + + Option.associate = function(models) { + Option.belongsTo(models.Question); + }; + + return Option; + }; |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 | + + + + + + + + + + + + + + + + + + + + + + + + | export default (sequelize, DataTypes) => { + const Question = sequelize.define('Question', { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + examId: { + type: DataTypes.UUID, + allowNull: false, + }, + question: { + type: DataTypes.TEXT, + allowNull: false, + }, + order: { + type: DataTypes.INTEGER, + allowNull: false, + }, + }, { + timestamps: true, + }); + + return Question; + }; |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import crypto from 'crypto'; + +export default (sequelize, DataTypes) => { + const Registration = sequelize.define('Registration', { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + userId: { + type: DataTypes.UUID, + allowNull: false, + }, + examId: { + type: DataTypes.UUID, + allowNull: false, + }, + paymentStatus: { + type: DataTypes.ENUM('pending', 'completed', 'failed'), + defaultValue: 'pending', + }, + examCode: { + type: DataTypes.STRING, + unique: true, + }, + status: { + type: DataTypes.ENUM('registered', 'completed'), + defaultValue: 'registered', + }, + }, { + timestamps: true, + hooks: { + beforeCreate: (registration) => { + Iif (!registration.examCode) { + registration.examCode = crypto.randomBytes(6).toString('hex').toUpperCase(); + } + } + } + }); + + return Registration; +}; |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | export default (sequelize, DataTypes) => { + const Result = sequelize.define('Result', { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + registrationId: { + type: DataTypes.UUID, + allowNull: false, + unique: true, + }, + userId: { + type: DataTypes.UUID, + allowNull: false, + }, + examId: { + type: DataTypes.UUID, + allowNull: false, + }, + score: { + type: DataTypes.FLOAT, + allowNull: false, + }, + passed: { + type: DataTypes.BOOLEAN, + allowNull: false, + }, + answers: { + type: DataTypes.JSONB, + defaultValue: [], + }, + completedAt: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW, + }, + }, { + timestamps: true, + }); + + return Result; + }; |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | export default (sequelize, DataTypes) => { + const User = sequelize.define('User', { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + fullName: { + type: DataTypes.STRING, + allowNull: false, + }, + email: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + validate: { + isEmail: true, + }, + }, + walletAddress: { + type: DataTypes.STRING, + allowNull: true, + unique: true, + }, + role: { + type: DataTypes.ENUM('user', 'admin'), + defaultValue: 'user', + }, + }, { + timestamps: true, + }); + return User; + }; |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { Notification, User } from '../models/index.js';
+import asyncHandler from 'express-async-handler';
+import { validationResult } from 'express-validator';
+import logger from '../utils/logger.js';
+
+// @desc Get all notifications
+// @route GET /api/notifications
+// @access Private
+const getNotifications = asyncHandler(async (req, res) => {
+ try {
+ logger.info(`User ${req.user.id} requesting their notifications`);
+
+ const notifications = await Notification.findAll({
+ where: {
+ userId: req.user.id,
+ },
+ order: [['createdAt', 'DESC']],
+ });
+
+ logger.info(`Retrieved ${notifications.length} notifications for user ${req.user.id}`);
+ res.status(200).json(notifications);
+ } catch (error) {
+ logger.error(`Error retrieving notifications for user ${req.user.id}: ${error.message}`, {
+ userId: req.user.id,
+ error: error.stack
+ });
+ res.status(500);
+ throw new Error('Error retrieving notifications');
+ }
+});
+
+// @desc Get notification by ID
+// @route GET /api/notifications/:id
+// @access Private
+const getNotificationById = asyncHandler(async (req, res) => {
+ try {
+ logger.info(`User ${req.user.id} requesting notification ${req.params.id}`);
+
+ const notification = await Notification.findOne({
+ where: {
+ id: req.params.id,
+ userId: req.user.id,
+ },
+ });
+
+ if (!notification) {
+ logger.warn(`Notification with id ${req.params.id} not found for user ${req.user.id}`, {
+ notificationId: req.params.id,
+ userId: req.user.id
+ });
+ res.status(404);
+ throw new Error('Notification not found');
+ }
+
+ logger.info(`Retrieved notification ${notification.id} for user ${req.user.id}`);
+ res.status(200).json(notification);
+ } catch (error) {
+ if (error.message === 'Notification not found') {
+ throw error;
+ }
+ logger.error(`Error retrieving notification ${req.params.id} for user ${req.user.id}: ${error.message}`, {
+ notificationId: req.params.id,
+ userId: req.user.id,
+ error: error.stack
+ });
+ res.status(500);
+ throw new Error('Error retrieving notification');
+ }
+});
+
+// @desc Create new notification
+// @route POST /api/notifications
+// @access Private (Admin)
+const createNotification = asyncHandler(async (req, res) => {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ logger.warn(`Validation errors in creating notification by admin ${req.user.id}: ${JSON.stringify(errors.array())}`, {
+ userId: req.user.id,
+ validationErrors: errors.array()
+ });
+ return res.status(400).json({ errors: errors.array() });
+ }
+
+ const {
+ title,
+ message,
+ type,
+ userId,
+ expiresAt,
+ } = req.body;
+
+ logger.info(`Admin ${req.user.id} creating notification: "${title}" for user ${userId || 'all users'}`);
+
+ try {
+ // Check if user exists if userId is provided
+ if (userId) {
+ const user = await User.findByPk(userId);
+ if (!user) {
+ logger.warn(`User with id ${userId} not found while admin ${req.user.id} creating notification`, {
+ targetUserId: userId,
+ adminId: req.user.id
+ });
+ res.status(404);
+ throw new Error('User not found');
+ }
+ }
+
+ // Create notification
+ const notification = await Notification.create({
+ title,
+ message,
+ type: type || 'info',
+ userId,
+ expiresAt,
+ });
+
+ logger.info(`Created notification ${notification.id} for user ${userId || 'all users'} by admin ${req.user.id}`, {
+ notificationId: notification.id,
+ targetUserId: userId,
+ adminId: req.user.id,
+ notificationType: type || 'info'
+ });
+ res.status(201).json(notification);
+ } catch (error) {
+ if (error.message === 'User not found') {
+ throw error;
+ }
+ logger.error(`Error creating notification by admin ${req.user.id}: ${error.message}`, {
+ adminId: req.user.id,
+ targetUserId: userId,
+ error: error.stack
+ });
+ res.status(500);
+ throw new Error('Error creating notification');
+ }
+});
+
+// @desc Update notification
+// @route PUT /api/notifications/:id
+// @access Private (Admin)
+const updateNotification = asyncHandler(async (req, res) => {
+ try {
+ logger.info(`Admin ${req.user.id} updating notification ${req.params.id}`);
+
+ const notification = await Notification.findByPk(req.params.id);
+
+ if (!notification) {
+ logger.warn(`Notification with id ${req.params.id} not found for update by admin ${req.user.id}`, {
+ notificationId: req.params.id,
+ adminId: req.user.id
+ });
+ res.status(404);
+ throw new Error('Notification not found');
+ }
+
+ const {
+ title,
+ message,
+ type,
+ isRead,
+ expiresAt,
+ } = req.body;
+
+ // Update notification
+ await notification.update({
+ title: title || notification.title,
+ message: message || notification.message,
+ type: type || notification.type,
+ isRead: isRead !== undefined ? isRead : notification.isRead,
+ expiresAt: expiresAt || notification.expiresAt,
+ });
+
+ logger.info(`Updated notification ${notification.id} by admin ${req.user.id}`, {
+ notificationId: notification.id,
+ adminId: req.user.id,
+ changes: {
+ title: !!title,
+ message: !!message,
+ type: !!type,
+ isRead: isRead !== undefined,
+ expiresAt: !!expiresAt
+ }
+ });
+ res.status(200).json(notification);
+ } catch (error) {
+ if (error.message === 'Notification not found') {
+ throw error;
+ }
+ logger.error(`Error updating notification ${req.params.id} by admin ${req.user.id}: ${error.message}`, {
+ notificationId: req.params.id,
+ adminId: req.user.id,
+ error: error.stack
+ });
+ res.status(500);
+ throw new Error('Error updating notification');
+ }
+});
+
+// @desc Mark notification as read
+// @route PATCH /api/notifications/:id/read
+// @access Private
+const markAsRead = asyncHandler(async (req, res) => {
+ try {
+ logger.info(`User ${req.user.id} marking notification ${req.params.id} as read`);
+
+ const notification = await Notification.findOne({
+ where: {
+ id: req.params.id,
+ userId: req.user.id,
+ },
+ });
+
+ if (!notification) {
+ logger.warn(`Notification with id ${req.params.id} not found for user ${req.user.id} to mark as read`, {
+ notificationId: req.params.id,
+ userId: req.user.id
+ });
+ res.status(404);
+ throw new Error('Notification not found');
+ }
+
+ // Mark as read
+ await notification.update({
+ isRead: true,
+ });
+
+ logger.info(`Marked notification ${notification.id} as read for user ${req.user.id}`, {
+ notificationId: notification.id,
+ userId: req.user.id,
+ previousReadStatus: notification.isRead
+ });
+ res.status(200).json(notification);
+ } catch (error) {
+ if (error.message === 'Notification not found') {
+ throw error;
+ }
+ logger.error(`Error marking notification ${req.params.id} as read for user ${req.user.id}: ${error.message}`, {
+ notificationId: req.params.id,
+ userId: req.user.id,
+ error: error.stack
+ });
+ res.status(500);
+ throw new Error('Error marking notification as read');
+ }
+});
+
+// @desc Delete notification
+// @route DELETE /api/notifications/:id
+// @access Private (Admin)
+const deleteNotification = asyncHandler(async (req, res) => {
+ try {
+ logger.info(`Admin ${req.user.id} deleting notification ${req.params.id}`);
+
+ const notification = await Notification.findByPk(req.params.id);
+
+ if (!notification) {
+ logger.warn(`Notification with id ${req.params.id} not found for deletion by admin ${req.user.id}`, {
+ notificationId: req.params.id,
+ adminId: req.user.id
+ });
+ res.status(404);
+ throw new Error('Notification not found');
+ }
+
+ // Delete notification
+ await notification.destroy();
+
+ logger.info(`Deleted notification ${req.params.id} by admin ${req.user.id}`, {
+ notificationId: req.params.id,
+ adminId: req.user.id,
+ notificationData: {
+ title: notification.title,
+ userId: notification.userId,
+ type: notification.type
+ }
+ });
+ res.status(200).json({ id: req.params.id });
+ } catch (error) {
+ if (error.message === 'Notification not found') {
+ throw error;
+ }
+ logger.error(`Error deleting notification ${req.params.id} by admin ${req.user.id}: ${error.message}`, {
+ notificationId: req.params.id,
+ adminId: req.user.id,
+ error: error.stack
+ });
+ res.status(500);
+ throw new Error('Error deleting notification');
+ }
+});
+
+export {
+ getNotifications,
+ getNotificationById,
+ createNotification,
+ updateNotification,
+ markAsRead,
+ deleteNotification,
+};
+ |