├── uploads └── test.txt ├── dist ├── utils │ ├── CustomInterfaces │ │ ├── CustomRequest.js │ │ ├── ExamInterface.js │ │ ├── QuestionInterfaces.js │ │ └── ParticipantDataInterfaces.js │ ├── catchAsync.js │ ├── response-handler.js │ ├── cloudinary.js │ ├── multer.js │ ├── questionFunctions.js │ └── examFunctions.js ├── errors │ └── custom-error.js ├── middlewares │ └── auth.js ├── routes │ └── api │ │ ├── utils │ │ └── routes.js │ │ ├── user │ │ └── routes.js │ │ └── exam │ │ └── routes.js ├── server.js ├── controllers │ ├── utilsController.js │ ├── userController.js │ └── examController.js └── database │ └── dbConnection.js ├── prisma ├── migrations │ ├── migration_lock.toml │ ├── 20220406082854_init │ │ └── migration.sql │ ├── 20220406211848_init │ │ └── migration.sql │ ├── 20220406082433_init │ │ └── migration.sql │ └── 20220406091007_init │ │ └── migration.sql └── schema.prisma ├── src ├── utils │ ├── CustomInterfaces │ │ ├── CustomRequest.ts │ │ ├── ExamInterface.ts │ │ ├── QuestionInterfaces.ts │ │ └── ParticipantDataInterfaces.ts │ ├── catchAsync.ts │ ├── response-handler.ts │ ├── multer.ts │ ├── cloudinary.ts │ ├── questionFunctions.ts │ └── examFunctions.ts ├── errors │ └── custom-error.ts ├── routes │ └── api │ │ ├── utils │ │ └── routes.ts │ │ ├── user │ │ └── routes.ts │ │ └── exam │ │ └── routes.ts ├── database │ ├── prisma-client.ts │ └── dbConnection.ts ├── middlewares │ └── auth.ts ├── controllers │ ├── utilsController.ts │ ├── userController.ts │ └── examController.ts └── server.ts ├── .prettierrc ├── .gitignore ├── README.md ├── package.json ├── web.config ├── .github └── workflows │ └── master_exam-simulation.yml └── tsconfig.json /uploads/test.txt: -------------------------------------------------------------------------------- 1 | :) -------------------------------------------------------------------------------- /dist/utils/CustomInterfaces/CustomRequest.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | -------------------------------------------------------------------------------- /dist/utils/CustomInterfaces/ExamInterface.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | -------------------------------------------------------------------------------- /dist/utils/CustomInterfaces/QuestionInterfaces.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "mysql" -------------------------------------------------------------------------------- /src/utils/CustomInterfaces/CustomRequest.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express'; 2 | import { JwtPayload } from 'jsonwebtoken'; 3 | export interface CustomRequest extends Request { 4 | user?: JwtPayload; 5 | } 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "printWidth": 150, 7 | "jsxSingleQuote": true, 8 | "bracketSameLine": true, 9 | "bracketSpacing": true 10 | } 11 | -------------------------------------------------------------------------------- /dist/utils/catchAsync.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.default = (fn) => { 4 | return (req, res, next) => { 5 | fn(req, res, next).catch((err) => next(err)); 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /src/utils/catchAsync.ts: -------------------------------------------------------------------------------- 1 | import { Response, Request, NextFunction } from 'express'; 2 | 3 | export default (fn: Function) => { 4 | return (req: Request, res: Response, next: NextFunction) => { 5 | fn(req, res, next).catch((err: Error) => next(err)); 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # dependencies 3 | node_modules 4 | /.pnp 5 | .pnp.js 6 | 7 | # testing 8 | /coverage 9 | 10 | # production 11 | /build 12 | 13 | # misc 14 | .DS_Store 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | 24 | yarn.lock 25 | package-lock.json 26 | *.env 27 | -------------------------------------------------------------------------------- /src/errors/custom-error.ts: -------------------------------------------------------------------------------- 1 | export default class CustomError extends Error { 2 | statusCode: number; 3 | status: string; 4 | constructor(message: string, statusCode: number) { 5 | super(message); 6 | this.statusCode = statusCode; 7 | this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error'; 8 | Error.captureStackTrace(this, this.constructor); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/routes/api/utils/routes.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import * as utilsController from '../../../controllers/utilsController'; 3 | import auth from '../../../middlewares/auth'; 4 | import upload from '../../../utils/multer'; 5 | 6 | const router = express.Router(); 7 | 8 | router.post('/upload/images', auth, upload.array('image'), utilsController.uploadImages); 9 | 10 | export default router; 11 | -------------------------------------------------------------------------------- /dist/errors/custom-error.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | class CustomError extends Error { 4 | constructor(message, statusCode) { 5 | super(message); 6 | this.statusCode = statusCode; 7 | this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error'; 8 | Error.captureStackTrace(this, this.constructor); 9 | } 10 | } 11 | exports.default = CustomError; 12 | -------------------------------------------------------------------------------- /dist/utils/CustomInterfaces/ParticipantDataInterfaces.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.participantRankingData = void 0; 4 | class participantRankingData { 5 | constructor(id, totalScore, finishTime) { 6 | this.id = id; 7 | this.totalScore = totalScore; 8 | this.finishTime = finishTime; 9 | } 10 | } 11 | exports.participantRankingData = participantRankingData; 12 | -------------------------------------------------------------------------------- /src/utils/response-handler.ts: -------------------------------------------------------------------------------- 1 | import CustomError from '../errors/custom-error'; 2 | 3 | export const SuccessResponse = (data: any, message?: string) => ({ 4 | success: true, 5 | status: 'success', 6 | message: message || 'Success', 7 | data, 8 | }); 9 | 10 | export const ErrorResponse = (err: Error) => ({ 11 | success: false, 12 | status: err instanceof CustomError && `${err.statusCode}`.startsWith('4') ? 'fail' : 'error', 13 | message: err.message, 14 | error: err, 15 | }); 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Quizzio: Backend 2 | 3 | Backend for the Exam Simulation project. 4 | 5 | ## Installation 6 | 7 | Install my-project with npm 8 | 9 | ```bash 10 | npm install 11 | npm run dev 12 | ``` 13 | 14 | ## Environment Variables 15 | 16 | To run this project, you will need to set the following environment variables in a .env file in the root directory: 17 | 18 | `ACCESS_TOKEN_SECRET` 19 | 20 | `UPLOADS_FOLDER_LOCAL` 21 | 22 | `CLOUDINARY_CLOUD_NAME` 23 | 24 | `CLOUDINARY_API_KEY` 25 | 26 | `CLOUDINARY_API_SECRET` 27 | 28 | ## Authors 29 | 30 | - [@Bihan001](https://www.github.com/Bihan001) 31 | -------------------------------------------------------------------------------- /src/utils/CustomInterfaces/ExamInterface.ts: -------------------------------------------------------------------------------- 1 | export interface examInterface { 2 | id: string; 3 | userId: string; 4 | image: string; 5 | tags: string; 6 | questions: any; 7 | startTime: number; 8 | duration: number; 9 | ongoing: boolean; 10 | finished: boolean; 11 | } 12 | 13 | export interface Option { 14 | id: number; 15 | data: string; 16 | } 17 | 18 | export interface RootObject { 19 | question: string; 20 | type: string; 21 | options: Option[]; 22 | givenOption: any; 23 | correctOption: any; 24 | } 25 | 26 | export interface RootQuestionsObject { 27 | [id: string]: RootObject; 28 | } 29 | -------------------------------------------------------------------------------- /src/database/prisma-client.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | 3 | const prisma = new PrismaClient({ 4 | errorFormat: 'pretty', 5 | log: [ 6 | { 7 | emit: 'event', 8 | level: 'query', 9 | }, 10 | { 11 | emit: 'stdout', 12 | level: 'error', 13 | }, 14 | { 15 | emit: 'stdout', 16 | level: 'info', 17 | }, 18 | { 19 | emit: 'stdout', 20 | level: 'warn', 21 | }, 22 | ], 23 | }); 24 | 25 | prisma.$on('query', (e) => { 26 | console.log('Query: ' + e.query); 27 | console.log('Params: ' + e.params); 28 | console.log('Duration: ' + e.duration + 'ms'); 29 | }); 30 | 31 | export default prisma; 32 | -------------------------------------------------------------------------------- /src/utils/multer.ts: -------------------------------------------------------------------------------- 1 | import multer from 'multer'; 2 | require('dotenv').config(); 3 | 4 | const fileTypes = ['image/jpeg', 'image/png']; 5 | 6 | const storage = multer.diskStorage({ 7 | destination: (req, file, cb) => { 8 | cb(null, process.env.UPLOADS_FOLDER_LOCAL as string); 9 | }, 10 | filename: (req, file, cb) => { 11 | cb(null, Date.now().toString() + '-' + file.originalname); 12 | }, 13 | }); 14 | 15 | const fileFilter = (req: any, file: Express.Multer.File, cb: Function) => { 16 | if (fileTypes.includes(file.mimetype)) { 17 | cb(null, true); 18 | } else { 19 | cb({ message: 'File format not supported' }, false); 20 | } 21 | }; 22 | 23 | export default multer({ 24 | storage, 25 | fileFilter, 26 | }); 27 | -------------------------------------------------------------------------------- /src/routes/api/user/routes.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import * as userController from '../../../controllers/userController'; 3 | import auth from '../../../middlewares/auth'; 4 | import upload from '../../../utils/multer'; 5 | 6 | const router = express.Router(); 7 | 8 | router.post('/login', userController.loginUser); 9 | router.post('/register', userController.registerUser); 10 | router.post('/logout', auth, userController.logoutUser); 11 | router.get('/current', auth, userController.getCurrentUser); 12 | router.get('/exams-hosted', auth, userController.getExamHosted); 13 | router.get('/exams-given', auth, userController.getExamGiven); 14 | router.get('/:id', userController.getuser); 15 | router.patch('/', auth, upload.single('image'), userController.editUser); 16 | 17 | export default router; 18 | -------------------------------------------------------------------------------- /src/middlewares/auth.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import { getDb } from '../database/dbConnection'; 3 | import jwt from 'jsonwebtoken'; 4 | import bcrypt from 'bcrypt'; 5 | import CustomError from '../errors/custom-error'; 6 | import { CustomRequest } from '../utils/CustomInterfaces/CustomRequest'; 7 | 8 | export default (req: CustomRequest, res: Response, next: NextFunction) => { 9 | const db = getDb(); 10 | const authHeader = req.headers['authorization'] as string; 11 | const token = authHeader?.split(' ')[1] || ''; 12 | if (!token) throw new CustomError('Token Not Found!', 403); 13 | jwt.verify(token, process.env.ACCESS_TOKEN_SECRET!, (err, user) => { 14 | if (err || !user) throw new CustomError('Token Invalid!', 403); 15 | req.user = user; 16 | next(); 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /src/utils/cloudinary.ts: -------------------------------------------------------------------------------- 1 | import { v2 as cloudinary } from 'cloudinary'; 2 | require('dotenv').config(); 3 | 4 | cloudinary.config({ 5 | cloud_name: process.env.CLOUDINARY_CLOUD_NAME as string, 6 | api_key: process.env.CLOUDINARY_API_KEY as string, 7 | api_secret: process.env.CLOUDINARY_API_SECRET as string, 8 | }); 9 | 10 | export default function uploader(filePath: string, folder = 'exam_simulation_images') { 11 | return new Promise((resolve, reject) => { 12 | cloudinary.uploader.upload( 13 | filePath, 14 | { 15 | folder, 16 | resource_type: 'auto', 17 | }, 18 | (err, result: any) => { 19 | if (err) { 20 | reject(err); 21 | } else { 22 | resolve({ url: result.secure_url, public_id: result.public_id }); 23 | } 24 | } 25 | ); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/CustomInterfaces/QuestionInterfaces.ts: -------------------------------------------------------------------------------- 1 | import { evaluateQuestion } from '../questionFunctions'; 2 | 3 | interface questionInterface { 4 | id: string; 5 | question: string; 6 | marks: number; 7 | negMarks: number; 8 | type: string; 9 | } 10 | 11 | export interface mcqInterface extends questionInterface { 12 | correctOption: number[]; 13 | options: [ 14 | { 15 | id: number; 16 | data: string; 17 | } 18 | ]; 19 | } 20 | 21 | export interface multipleOptionsInterface extends questionInterface { 22 | correctOption: number[]; 23 | options: [ 24 | { 25 | id: number; 26 | data: string; 27 | } 28 | ]; 29 | } 30 | export interface fillInTheBlanksInterface extends questionInterface { 31 | correctOption: string; 32 | } 33 | 34 | export interface evaluateQuestionInterface { 35 | [type: string]: any; 36 | } 37 | -------------------------------------------------------------------------------- /dist/utils/response-handler.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.ErrorResponse = exports.SuccessResponse = void 0; 7 | const custom_error_1 = __importDefault(require("../errors/custom-error")); 8 | const SuccessResponse = (data, message) => ({ 9 | success: true, 10 | status: 'success', 11 | message: message || 'Success', 12 | data, 13 | }); 14 | exports.SuccessResponse = SuccessResponse; 15 | const ErrorResponse = (err) => ({ 16 | success: false, 17 | status: err instanceof custom_error_1.default && `${err.statusCode}`.startsWith('4') ? 'fail' : 'error', 18 | message: err.message, 19 | error: err, 20 | }); 21 | exports.ErrorResponse = ErrorResponse; 22 | -------------------------------------------------------------------------------- /src/database/dbConnection.ts: -------------------------------------------------------------------------------- 1 | import e from 'express'; 2 | import mysql from 'mysql2/promise'; 3 | import { scheduleOnServerRestart } from '../utils/examFunctions'; 4 | 5 | let db: any; 6 | const connectDatabase = async () => { 7 | db = await mysql.createConnection({ 8 | host: 'mydbinstance.cyfjjout6pho.ap-south-1.rds.amazonaws.com', 9 | user: 'subho57', 10 | password: 'adminDBpassword123', 11 | port: 3306, 12 | database: 'examSimulation', 13 | multipleStatements: true, // Prevent nested sql statements 14 | connectTimeout: 60 * 60 * 1000, 15 | // debug: true, 16 | }); 17 | if (db) { 18 | console.log('Database Connected !'); 19 | scheduleOnServerRestart(); 20 | } else console.log('Database Not Connected !'); 21 | }; 22 | 23 | const getDb = () => { 24 | return db; 25 | }; 26 | 27 | export default db; 28 | export { connectDatabase, getDb }; 29 | -------------------------------------------------------------------------------- /dist/utils/cloudinary.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const cloudinary_1 = require("cloudinary"); 4 | require('dotenv').config(); 5 | cloudinary_1.v2.config({ 6 | cloud_name: process.env.CLOUDINARY_CLOUD_NAME, 7 | api_key: process.env.CLOUDINARY_API_KEY, 8 | api_secret: process.env.CLOUDINARY_API_SECRET, 9 | }); 10 | function uploader(filePath, folder = 'exam_simulation_images') { 11 | return new Promise((resolve, reject) => { 12 | cloudinary_1.v2.uploader.upload(filePath, { 13 | folder, 14 | resource_type: 'auto', 15 | }, (err, result) => { 16 | if (err) { 17 | reject(err); 18 | } 19 | else { 20 | resolve({ url: result.secure_url, public_id: result.public_id }); 21 | } 22 | }); 23 | }); 24 | } 25 | exports.default = uploader; 26 | -------------------------------------------------------------------------------- /src/controllers/utilsController.ts: -------------------------------------------------------------------------------- 1 | import catchAsync from '../utils/catchAsync'; 2 | import { SuccessResponse } from '../utils/response-handler'; 3 | import { CustomRequest } from '../utils/CustomInterfaces/CustomRequest'; 4 | import CustomError from '../errors/custom-error'; 5 | import { Response, Request } from 'express'; 6 | import { getDb } from '../database/dbConnection'; 7 | import cloudinaryUploader from '../utils/cloudinary'; 8 | import fs from 'fs'; 9 | 10 | export const uploadImages = catchAsync(async (req: Request, res: Response) => { 11 | const uploader = async (path: string) => await cloudinaryUploader(path); 12 | 13 | const files = req.files as Express.Multer.File[]; 14 | const urls = []; 15 | for (const file of files) { 16 | const { path } = file; 17 | const newPath = await uploader(path); 18 | urls.push(newPath); 19 | fs.unlinkSync(path); 20 | } 21 | return res.status(200).json(SuccessResponse({ urls }, 'Successfully uploaded images')); 22 | }); 23 | -------------------------------------------------------------------------------- /dist/utils/multer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | const multer_1 = __importDefault(require("multer")); 7 | require('dotenv').config(); 8 | const fileTypes = ['image/jpeg', 'image/png']; 9 | const storage = multer_1.default.diskStorage({ 10 | destination: (req, file, cb) => { 11 | cb(null, process.env.UPLOADS_FOLDER_LOCAL); 12 | }, 13 | filename: (req, file, cb) => { 14 | cb(null, Date.now().toString() + '-' + file.originalname); 15 | }, 16 | }); 17 | const fileFilter = (req, file, cb) => { 18 | if (fileTypes.includes(file.mimetype)) { 19 | cb(null, true); 20 | } 21 | else { 22 | cb({ message: 'File format not supported' }, false); 23 | } 24 | }; 25 | exports.default = (0, multer_1.default)({ 26 | storage, 27 | fileFilter, 28 | }); 29 | -------------------------------------------------------------------------------- /src/utils/CustomInterfaces/ParticipantDataInterfaces.ts: -------------------------------------------------------------------------------- 1 | export interface participantDataInterface { 2 | id: number; 3 | examId: string; 4 | participantId: string; 5 | answers: any; 6 | totalScore: number | null; 7 | finishTime: number; 8 | virtual: boolean; 9 | } 10 | 11 | export interface answerInterface { 12 | type: 'mcq' | 'multipleOptions' | 'fillInTheBlanks'; 13 | answer: number[] | string; 14 | } 15 | 16 | export interface answersObjInterface { 17 | [key: string]: answerInterface; 18 | } 19 | 20 | export interface participantRankingInterface { 21 | id: String; 22 | finishTime: number; 23 | totalScore: number; 24 | } 25 | 26 | export class participantRankingData implements participantRankingInterface { 27 | id: String; 28 | finishTime: number; 29 | totalScore: number; 30 | 31 | constructor(id: String, totalScore: number, finishTime: number) { 32 | this.id = id; 33 | this.totalScore = totalScore; 34 | this.finishTime = finishTime; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /dist/middlewares/auth.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | const dbConnection_1 = require("../database/dbConnection"); 7 | const jsonwebtoken_1 = __importDefault(require("jsonwebtoken")); 8 | const custom_error_1 = __importDefault(require("../errors/custom-error")); 9 | exports.default = (req, res, next) => { 10 | const db = (0, dbConnection_1.getDb)(); 11 | const authHeader = req.headers['authorization']; 12 | const token = (authHeader === null || authHeader === void 0 ? void 0 : authHeader.split(' ')[1]) || ''; 13 | if (!token) 14 | throw new custom_error_1.default('Token Not Found!', 403); 15 | jsonwebtoken_1.default.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, user) => { 16 | if (err || !user) 17 | throw new custom_error_1.default('Token Invalid!', 403); 18 | req.user = user; 19 | next(); 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /dist/utils/questionFunctions.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.evaluateQuestion = void 0; 4 | const evaluateMCQ = (question, answer) => { 5 | if (question.correctOption[0] === answer.answer[0]) 6 | return question.marks; 7 | return -question.negMarks; 8 | }; 9 | const evaluateMultipleOptions = (question, answer) => { 10 | let participantAnsOpts = [...answer.answer].sort(); 11 | let correctOptions = [...question.correctOption].sort(); 12 | if (correctOptions.every((val, index) => val === participantAnsOpts[index])) 13 | return question.marks; 14 | return -question.negMarks; 15 | }; 16 | const evaluateFillInTheBlanks = (question, answer) => { 17 | return question.correctOption.toLowerCase() === 18 | answer.answer.toString().toLowerCase() 19 | ? question.marks 20 | : -question.negMarks; 21 | }; 22 | exports.evaluateQuestion = { 23 | mcq: evaluateMCQ, 24 | multipleOptions: evaluateMultipleOptions, 25 | fillInTheBlanks: evaluateFillInTheBlanks, 26 | }; 27 | -------------------------------------------------------------------------------- /src/routes/api/exam/routes.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import * as examController from '../../../controllers/examController'; 3 | import auth from '../../../middlewares/auth'; 4 | 5 | const router = express.Router(); 6 | router.get('/testing-route', examController.testing); 7 | router.get('/tags', examController.getTags); 8 | router.get('/question-types', examController.getQuestionTypes); 9 | router.get('/create-tables', examController.createTables); 10 | router.post('/create', auth, examController.createExam); 11 | router.get('/evaluate/force', examController.forceEvaluateExam); 12 | router.get('/exam-registered', auth, examController.examRegisterStatus); 13 | router.get('/solution', auth, examController.getExamSolution); 14 | router.get('/scores', auth, examController.getExamScores); 15 | router.patch('/:id', auth, examController.editExam); 16 | router.post('/all', examController.getExams); 17 | router.get('/:id', examController.getExamDetails); 18 | router.get('/:id/start', auth, examController.startExam); 19 | router.post('/register', auth, examController.registerInExam); 20 | router.post('/submit', auth, examController.submitExam); 21 | 22 | export default router; 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "./src/server.ts", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "dev": "nodemon src/server.ts", 9 | "start": "node dist/server.js", 10 | "build": "tsc --project .", 11 | "build-prod": "npm install && npm run build", 12 | "heroku-postbuild": "NPM_CONFIG_PRODUCTION=false npm install" 13 | }, 14 | "author": "Ankur Saha", 15 | "license": "ISC", 16 | "dependencies": { 17 | "@prisma/client": "3.12.0", 18 | "bcrypt": "^5.0.1", 19 | "cloudinary": "^1.28.1", 20 | "cors": "^2.8.5", 21 | "dotenv": "^10.0.0", 22 | "express": "^4.17.2", 23 | "jsonwebtoken": "^8.5.1", 24 | "multer": "^1.4.4", 25 | "mysql2": "^2.3.3", 26 | "node-schedule": "^2.1.0", 27 | "uuid": "^8.3.2" 28 | }, 29 | "devDependencies": { 30 | "@types/bcrypt": "^5.0.0", 31 | "@types/cors": "^2.8.12", 32 | "@types/express": "^4.17.13", 33 | "@types/jsonwebtoken": "^8.5.6", 34 | "@types/multer": "^1.4.7", 35 | "@types/mysql": "^2.15.20", 36 | "@types/node": "^17.0.8", 37 | "@types/node-cron": "^3.0.1", 38 | "@types/node-schedule": "^1.3.2", 39 | "@types/uuid": "^8.3.4", 40 | "nodemon": "^2.0.15", 41 | "prisma": "^3.12.0", 42 | "ts-node": "^10.4.0", 43 | "typescript": "^4.5.4" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/utils/questionFunctions.ts: -------------------------------------------------------------------------------- 1 | import { 2 | mcqInterface, 3 | multipleOptionsInterface, 4 | fillInTheBlanksInterface, 5 | evaluateQuestionInterface, 6 | } from './CustomInterfaces/QuestionInterfaces'; 7 | import { examInterface } from './CustomInterfaces/ExamInterface'; 8 | import { answerInterface } from './CustomInterfaces/ParticipantDataInterfaces'; 9 | 10 | const evaluateMCQ = ( 11 | question: mcqInterface, 12 | answer: answerInterface 13 | ): Number => { 14 | if (question.correctOption[0] === answer.answer[0]) return question.marks; 15 | return -question.negMarks; 16 | }; 17 | 18 | const evaluateMultipleOptions = ( 19 | question: multipleOptionsInterface, 20 | answer: answerInterface 21 | ): Number => { 22 | let participantAnsOpts = [...answer.answer].sort(); 23 | let correctOptions = [...question.correctOption].sort(); 24 | if (correctOptions.every((val, index) => val === participantAnsOpts[index])) 25 | return question.marks; 26 | return -question.negMarks; 27 | }; 28 | 29 | const evaluateFillInTheBlanks = ( 30 | question: fillInTheBlanksInterface, 31 | answer: answerInterface 32 | ): Number => { 33 | return question.correctOption.toLowerCase() === 34 | answer.answer.toString().toLowerCase() 35 | ? question.marks 36 | : -question.negMarks; 37 | }; 38 | 39 | export const evaluateQuestion: evaluateQuestionInterface = { 40 | mcq: evaluateMCQ, 41 | multipleOptions: evaluateMultipleOptions, 42 | fillInTheBlanks: evaluateFillInTheBlanks, 43 | }; 44 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response, NextFunction } from "express"; 2 | import cors from "cors"; 3 | import CustomError from "./errors/custom-error"; 4 | import { SuccessResponse, ErrorResponse } from "./utils/response-handler"; 5 | import examRoutes from "./routes/api/exam/routes"; 6 | import userRoutes from "./routes/api/user/routes"; 7 | import utilsRoutes from "./routes/api/utils/routes"; 8 | 9 | import { connectDatabase } from "./database/dbConnection"; 10 | 11 | const app = express(); 12 | 13 | app.use( 14 | cors({ 15 | credentials: true, 16 | origin: (origin, callback) => { 17 | return callback(null, true); 18 | }, 19 | }) 20 | ); 21 | app.use(express.json()); 22 | require("dotenv").config(); 23 | app.use("/exam", examRoutes); 24 | app.use("/user", userRoutes); 25 | app.use("/utils", utilsRoutes); 26 | 27 | //connecting database 28 | connectDatabase(); 29 | //=================== 30 | 31 | // All middlewares goes above this 32 | 33 | app.all("*", (req: Request, res: Response, next: NextFunction) => { 34 | const err = new CustomError("Non-existant route", 404); 35 | next(err); 36 | }); 37 | 38 | app.use((err: Error, req: Request, res: Response, next: NextFunction) => { 39 | if (err instanceof CustomError) { 40 | return res.status(err.statusCode).json(ErrorResponse(err)); 41 | } 42 | return res.status(500).json(ErrorResponse(err)); 43 | }); 44 | 45 | const PORT = process.env.PORT || 5000; 46 | app.listen(PORT, () => { 47 | console.log(`Server started on port ${PORT}`); 48 | }); 49 | -------------------------------------------------------------------------------- /web.config: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /.github/workflows/master_exam-simulation.yml: -------------------------------------------------------------------------------- 1 | # Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy 2 | # More GitHub Actions for Azure: https://github.com/Azure/actions 3 | 4 | name: Build and deploy Node.js app to Azure Web App - exam-simulation 5 | 6 | on: 7 | push: 8 | branches: 9 | - master 10 | workflow_dispatch: 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | 19 | - name: Set up Node.js version 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: '14.x' 23 | 24 | - name: npm install, build 25 | run: npm run build-prod 26 | 27 | - name: Upload artifact for deployment job 28 | uses: actions/upload-artifact@v2 29 | with: 30 | name: node-app 31 | path: . 32 | 33 | deploy: 34 | runs-on: ubuntu-latest 35 | needs: build 36 | environment: 37 | name: 'Production' 38 | url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} 39 | 40 | steps: 41 | - name: Download artifact from build job 42 | uses: actions/download-artifact@v2 43 | with: 44 | name: node-app 45 | 46 | - name: 'Deploy to Azure Web App' 47 | id: deploy-to-webapp 48 | uses: azure/webapps-deploy@v2 49 | with: 50 | app-name: 'exam-simulation' 51 | slot-name: 'Production' 52 | publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_8382F1C3F78845208D2B79C8F0B8FB8B }} 53 | package: . 54 | -------------------------------------------------------------------------------- /dist/routes/api/utils/routes.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 | if (k2 === undefined) k2 = k; 4 | Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); 5 | }) : (function(o, m, k, k2) { 6 | if (k2 === undefined) k2 = k; 7 | o[k2] = m[k]; 8 | })); 9 | var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { 10 | Object.defineProperty(o, "default", { enumerable: true, value: v }); 11 | }) : function(o, v) { 12 | o["default"] = v; 13 | }); 14 | var __importStar = (this && this.__importStar) || function (mod) { 15 | if (mod && mod.__esModule) return mod; 16 | var result = {}; 17 | if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); 18 | __setModuleDefault(result, mod); 19 | return result; 20 | }; 21 | var __importDefault = (this && this.__importDefault) || function (mod) { 22 | return (mod && mod.__esModule) ? mod : { "default": mod }; 23 | }; 24 | Object.defineProperty(exports, "__esModule", { value: true }); 25 | const express_1 = __importDefault(require("express")); 26 | const utilsController = __importStar(require("../../../controllers/utilsController")); 27 | const auth_1 = __importDefault(require("../../../middlewares/auth")); 28 | const multer_1 = __importDefault(require("../../../utils/multer")); 29 | const router = express_1.default.Router(); 30 | router.post('/upload/images', auth_1.default, multer_1.default.array('image'), utilsController.uploadImages); 31 | exports.default = router; 32 | -------------------------------------------------------------------------------- /dist/server.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | const express_1 = __importDefault(require("express")); 7 | const cors_1 = __importDefault(require("cors")); 8 | const custom_error_1 = __importDefault(require("./errors/custom-error")); 9 | const response_handler_1 = require("./utils/response-handler"); 10 | const routes_1 = __importDefault(require("./routes/api/exam/routes")); 11 | const routes_2 = __importDefault(require("./routes/api/user/routes")); 12 | const routes_3 = __importDefault(require("./routes/api/utils/routes")); 13 | const dbConnection_1 = require("./database/dbConnection"); 14 | const app = (0, express_1.default)(); 15 | app.use((0, cors_1.default)({ 16 | credentials: true, 17 | origin: (origin, callback) => { 18 | return callback(null, true); 19 | }, 20 | })); 21 | app.use(express_1.default.json()); 22 | require("dotenv").config(); 23 | app.use("/exam", routes_1.default); 24 | app.use("/user", routes_2.default); 25 | app.use("/utils", routes_3.default); 26 | //connecting database 27 | (0, dbConnection_1.connectDatabase)(); 28 | //=================== 29 | // All middlewares goes above this 30 | app.all("*", (req, res, next) => { 31 | const err = new custom_error_1.default("Non-existant route", 404); 32 | next(err); 33 | }); 34 | app.use((err, req, res, next) => { 35 | if (err instanceof custom_error_1.default) { 36 | return res.status(err.statusCode).json((0, response_handler_1.ErrorResponse)(err)); 37 | } 38 | return res.status(500).json((0, response_handler_1.ErrorResponse)(err)); 39 | }); 40 | const PORT = process.env.PORT || 5000; 41 | app.listen(PORT, () => { 42 | console.log(`Server started on port ${PORT}`); 43 | }); 44 | -------------------------------------------------------------------------------- /dist/controllers/utilsController.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __importDefault = (this && this.__importDefault) || function (mod) { 12 | return (mod && mod.__esModule) ? mod : { "default": mod }; 13 | }; 14 | Object.defineProperty(exports, "__esModule", { value: true }); 15 | exports.uploadImages = void 0; 16 | const catchAsync_1 = __importDefault(require("../utils/catchAsync")); 17 | const response_handler_1 = require("../utils/response-handler"); 18 | const cloudinary_1 = __importDefault(require("../utils/cloudinary")); 19 | const fs_1 = __importDefault(require("fs")); 20 | exports.uploadImages = (0, catchAsync_1.default)((req, res) => __awaiter(void 0, void 0, void 0, function* () { 21 | const uploader = (path) => __awaiter(void 0, void 0, void 0, function* () { return yield (0, cloudinary_1.default)(path); }); 22 | const files = req.files; 23 | const urls = []; 24 | for (const file of files) { 25 | const { path } = file; 26 | const newPath = yield uploader(path); 27 | urls.push(newPath); 28 | fs_1.default.unlinkSync(path); 29 | } 30 | return res.status(200).json((0, response_handler_1.SuccessResponse)({ urls }, 'Successfully uploaded images')); 31 | })); 32 | -------------------------------------------------------------------------------- /dist/database/dbConnection.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __importDefault = (this && this.__importDefault) || function (mod) { 12 | return (mod && mod.__esModule) ? mod : { "default": mod }; 13 | }; 14 | Object.defineProperty(exports, "__esModule", { value: true }); 15 | exports.getDb = exports.connectDatabase = void 0; 16 | const promise_1 = __importDefault(require("mysql2/promise")); 17 | const examFunctions_1 = require("../utils/examFunctions"); 18 | let db; 19 | const connectDatabase = () => __awaiter(void 0, void 0, void 0, function* () { 20 | db = yield promise_1.default.createConnection({ 21 | host: 'mydbinstance.cyfjjout6pho.ap-south-1.rds.amazonaws.com', 22 | user: 'subho57', 23 | password: 'adminDBpassword123', 24 | port: 3306, 25 | database: 'examSimulation', 26 | multipleStatements: true, 27 | connectTimeout: 60 * 60 * 1000, 28 | // debug: true, 29 | }); 30 | if (db) { 31 | console.log('Database Connected !'); 32 | (0, examFunctions_1.scheduleOnServerRestart)(); 33 | } 34 | else 35 | console.log('Database Not Connected !'); 36 | }); 37 | exports.connectDatabase = connectDatabase; 38 | const getDb = () => { 39 | return db; 40 | }; 41 | exports.getDb = getDb; 42 | exports.default = db; 43 | -------------------------------------------------------------------------------- /dist/routes/api/user/routes.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 | if (k2 === undefined) k2 = k; 4 | Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); 5 | }) : (function(o, m, k, k2) { 6 | if (k2 === undefined) k2 = k; 7 | o[k2] = m[k]; 8 | })); 9 | var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { 10 | Object.defineProperty(o, "default", { enumerable: true, value: v }); 11 | }) : function(o, v) { 12 | o["default"] = v; 13 | }); 14 | var __importStar = (this && this.__importStar) || function (mod) { 15 | if (mod && mod.__esModule) return mod; 16 | var result = {}; 17 | if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); 18 | __setModuleDefault(result, mod); 19 | return result; 20 | }; 21 | var __importDefault = (this && this.__importDefault) || function (mod) { 22 | return (mod && mod.__esModule) ? mod : { "default": mod }; 23 | }; 24 | Object.defineProperty(exports, "__esModule", { value: true }); 25 | const express_1 = __importDefault(require("express")); 26 | const userController = __importStar(require("../../../controllers/userController")); 27 | const auth_1 = __importDefault(require("../../../middlewares/auth")); 28 | const multer_1 = __importDefault(require("../../../utils/multer")); 29 | const router = express_1.default.Router(); 30 | router.post('/login', userController.loginUser); 31 | router.post('/register', userController.registerUser); 32 | router.post('/logout', auth_1.default, userController.logoutUser); 33 | router.get('/current', auth_1.default, userController.getCurrentUser); 34 | router.get('/exams-hosted', auth_1.default, userController.getExamHosted); 35 | router.get('/exams-given', auth_1.default, userController.getExamGiven); 36 | router.get('/:id', userController.getuser); 37 | router.patch('/', auth_1.default, multer_1.default.single('image'), userController.editUser); 38 | exports.default = router; 39 | -------------------------------------------------------------------------------- /dist/routes/api/exam/routes.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 | if (k2 === undefined) k2 = k; 4 | Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); 5 | }) : (function(o, m, k, k2) { 6 | if (k2 === undefined) k2 = k; 7 | o[k2] = m[k]; 8 | })); 9 | var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { 10 | Object.defineProperty(o, "default", { enumerable: true, value: v }); 11 | }) : function(o, v) { 12 | o["default"] = v; 13 | }); 14 | var __importStar = (this && this.__importStar) || function (mod) { 15 | if (mod && mod.__esModule) return mod; 16 | var result = {}; 17 | if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); 18 | __setModuleDefault(result, mod); 19 | return result; 20 | }; 21 | var __importDefault = (this && this.__importDefault) || function (mod) { 22 | return (mod && mod.__esModule) ? mod : { "default": mod }; 23 | }; 24 | Object.defineProperty(exports, "__esModule", { value: true }); 25 | const express_1 = __importDefault(require("express")); 26 | const examController = __importStar(require("../../../controllers/examController")); 27 | const auth_1 = __importDefault(require("../../../middlewares/auth")); 28 | const router = express_1.default.Router(); 29 | router.get('/testing-route', examController.testing); 30 | router.get('/tags', examController.getTags); 31 | router.get('/question-types', examController.getQuestionTypes); 32 | router.get('/create-tables', examController.createTables); 33 | router.post('/create', auth_1.default, examController.createExam); 34 | router.get('/evaluate/force', examController.forceEvaluateExam); 35 | router.get('/exam-registered', auth_1.default, examController.examRegisterStatus); 36 | router.get('/solution', auth_1.default, examController.getExamSolution); 37 | router.get('/scores', auth_1.default, examController.getExamScores); 38 | router.patch('/:id', auth_1.default, examController.editExam); 39 | router.post('/all', examController.getExams); 40 | router.get('/:id', examController.getExamDetails); 41 | router.get('/:id/start', auth_1.default, examController.startExam); 42 | router.post('/register', auth_1.default, examController.registerInExam); 43 | router.post('/submit', auth_1.default, examController.submitExam); 44 | exports.default = router; 45 | -------------------------------------------------------------------------------- /prisma/migrations/20220406082854_init/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `created_at` on the `ExamParticipant` table. All the data in the column will be lost. 5 | - You are about to drop the column `updated_at` on the `ExamParticipant` table. All the data in the column will be lost. 6 | - You are about to drop the column `created_at` on the `ExamTag` table. All the data in the column will be lost. 7 | - You are about to drop the column `updated_at` on the `ExamTag` table. All the data in the column will be lost. 8 | - You are about to drop the column `created_at` on the `PrivateExamEmail` table. All the data in the column will be lost. 9 | - You are about to drop the column `updated_at` on the `PrivateExamEmail` table. All the data in the column will be lost. 10 | - The primary key for the `QuestionUserAnswer` table will be changed. If it partially fails, the table could be left without primary key constraint. 11 | - You are about to drop the column `id` on the `QuestionUserAnswer` table. All the data in the column will be lost. 12 | - A unique constraint covering the columns `[exam_id,question_id,user_id]` on the table `QuestionUserAnswer` will be added. If there are existing duplicate values, this will fail. 13 | - Added the required column `updated_at` to the `Question` table without a default value. This is not possible if the table is not empty. 14 | - Added the required column `updated_at` to the `QuestionVariant` table without a default value. This is not possible if the table is not empty. 15 | 16 | */ 17 | -- AlterTable 18 | ALTER TABLE `ExamParticipant` DROP COLUMN `created_at`, 19 | DROP COLUMN `updated_at`; 20 | 21 | -- AlterTable 22 | ALTER TABLE `ExamTag` DROP COLUMN `created_at`, 23 | DROP COLUMN `updated_at`; 24 | 25 | -- AlterTable 26 | ALTER TABLE `PrivateExamEmail` DROP COLUMN `created_at`, 27 | DROP COLUMN `updated_at`; 28 | 29 | -- AlterTable 30 | ALTER TABLE `Question` ADD COLUMN `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 31 | ADD COLUMN `updated_at` DATETIME(3) NOT NULL; 32 | 33 | -- AlterTable 34 | ALTER TABLE `QuestionUserAnswer` DROP PRIMARY KEY, 35 | DROP COLUMN `id`; 36 | 37 | -- AlterTable 38 | ALTER TABLE `QuestionVariant` ADD COLUMN `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 39 | ADD COLUMN `updated_at` DATETIME(3) NOT NULL; 40 | 41 | -- CreateIndex 42 | CREATE UNIQUE INDEX `QuestionUserAnswer_exam_id_question_id_user_id_key` ON `QuestionUserAnswer`(`exam_id`, `question_id`, `user_id`); 43 | -------------------------------------------------------------------------------- /prisma/migrations/20220406211848_init/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `Institution` on the `users` table. All the data in the column will be lost. 5 | - You are about to drop the `question_exam_submissions` table. If the table is not empty, all the data it contains will be lost. 6 | 7 | */ 8 | -- DropForeignKey 9 | ALTER TABLE `question_exam_submissions` DROP FOREIGN KEY `question_exam_submissions_exam_id_fkey`; 10 | 11 | -- DropForeignKey 12 | ALTER TABLE `question_exam_submissions` DROP FOREIGN KEY `question_exam_submissions_question_id_fkey`; 13 | 14 | -- DropForeignKey 15 | ALTER TABLE `question_exam_submissions` DROP FOREIGN KEY `question_exam_submissions_user_id_fkey`; 16 | 17 | -- AlterTable 18 | ALTER TABLE `users` DROP COLUMN `Institution`, 19 | ADD COLUMN `institution` VARCHAR(191) NULL; 20 | 21 | -- DropTable 22 | DROP TABLE `question_exam_submissions`; 23 | 24 | -- CreateTable 25 | CREATE TABLE `question_submissions` ( 26 | `exam_id` INTEGER NOT NULL, 27 | `question_id` INTEGER NOT NULL, 28 | `user_id` INTEGER NOT NULL, 29 | `option` VARCHAR(191) NOT NULL, 30 | 31 | UNIQUE INDEX `question_submissions_exam_id_question_id_user_id_option_key`(`exam_id`, `question_id`, `user_id`, `option`) 32 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 33 | 34 | -- CreateTable 35 | CREATE TABLE `exam_submissions` ( 36 | `exam_id` INTEGER NOT NULL, 37 | `question_id` INTEGER NOT NULL, 38 | `user_id` INTEGER NOT NULL, 39 | `score` INTEGER NOT NULL DEFAULT 0, 40 | 41 | UNIQUE INDEX `exam_submissions_exam_id_key`(`exam_id`), 42 | UNIQUE INDEX `exam_submissions_exam_id_question_id_user_id_key`(`exam_id`, `question_id`, `user_id`) 43 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 44 | 45 | -- CreateTable 46 | CREATE TABLE `question_options` ( 47 | `question_id` INTEGER NOT NULL, 48 | `option` VARCHAR(191) NOT NULL, 49 | 50 | UNIQUE INDEX `question_options_question_id_option_key`(`question_id`, `option`) 51 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 52 | 53 | -- AddForeignKey 54 | ALTER TABLE `question_submissions` ADD CONSTRAINT `question_submissions_exam_id_fkey` FOREIGN KEY (`exam_id`) REFERENCES `exams`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; 55 | 56 | -- AddForeignKey 57 | ALTER TABLE `question_submissions` ADD CONSTRAINT `question_submissions_question_id_fkey` FOREIGN KEY (`question_id`) REFERENCES `questions`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; 58 | 59 | -- AddForeignKey 60 | ALTER TABLE `exam_submissions` ADD CONSTRAINT `exam_submissions_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; 61 | 62 | -- AddForeignKey 63 | ALTER TABLE `exam_submissions` ADD CONSTRAINT `exam_submissions_exam_id_fkey` FOREIGN KEY (`exam_id`) REFERENCES `exams`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; 64 | 65 | -- AddForeignKey 66 | ALTER TABLE `exam_submissions` ADD CONSTRAINT `exam_submissions_question_id_fkey` FOREIGN KEY (`question_id`) REFERENCES `questions`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; 67 | 68 | -- AddForeignKey 69 | ALTER TABLE `question_options` ADD CONSTRAINT `question_options_question_id_fkey` FOREIGN KEY (`question_id`) REFERENCES `questions`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; 70 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "mysql" 10 | url = env("DATABASE_URL") 11 | } 12 | 13 | model User { 14 | id Int @id @default(autoincrement()) 15 | name String 16 | email String @unique 17 | bio String? 18 | dob DateTime 19 | address String? @db.VarChar(255) 20 | country String? 21 | profile_image String? 22 | password String 23 | latest_degree String? 24 | institution String? 25 | gender String? 26 | country_code Int 27 | phone String 28 | exams_created Exam[] 29 | created_at DateTime @default(now()) 30 | updated_at DateTime @updatedAt 31 | exams_participated ExamParticipant[] 32 | exam_question_scores ExamSubmission[] 33 | 34 | @@map("users") 35 | } 36 | 37 | model Exam { 38 | id Int @id @default(autoincrement()) 39 | creator_id Int 40 | creator User @relation(fields: [creator_id], references: [id]) 41 | name String 42 | description String? 43 | banner String? 44 | thumbnail String? 45 | start_time DateTime 46 | duration BigInt 47 | ongoing Boolean @default(false) 48 | is_private Boolean @default(false) 49 | participant_count Int @default(0) 50 | created_at DateTime @default(now()) 51 | updated_at DateTime @updatedAt 52 | exam_participants ExamParticipant[] 53 | private_exam_emails PrivateExamEmail[] 54 | exam_tags ExamTag[] 55 | exam_question_scores ExamSubmission[] 56 | exam_questions ExamQuestion[] 57 | exam_questions_submissions QuestionSubmission[] 58 | 59 | @@map("exams") 60 | } 61 | 62 | model ExamParticipant { 63 | exam_id Int 64 | participant_id Int 65 | exam Exam @relation(fields: [exam_id], references: [id]) 66 | user User @relation(fields: [participant_id], references: [id]) 67 | score Int? 68 | finish_time Int? 69 | rank Int? 70 | 71 | @@unique([exam_id, participant_id]) 72 | @@map("exam_participants") 73 | } 74 | 75 | model PrivateExamEmail { 76 | examId Int 77 | exam Exam @relation(fields: [examId], references: [id]) 78 | email String 79 | 80 | @@unique([examId, email]) 81 | @@map("private_exam_emails") 82 | } 83 | 84 | model Tag { 85 | id Int @id @default(autoincrement()) 86 | tag String @unique 87 | exam_tags ExamTag[] 88 | created_at DateTime @default(now()) 89 | updated_at DateTime @updatedAt 90 | 91 | @@map("tags") 92 | } 93 | 94 | model ExamTag { 95 | exam_id Int 96 | tag_id Int 97 | exam Exam @relation(fields: [exam_id], references: [id]) 98 | tag Tag @relation(fields: [tag_id], references: [id]) 99 | 100 | @@unique([exam_id, tag_id]) 101 | @@map("exam_tags") 102 | } 103 | 104 | model Question { 105 | id Int @id @default(autoincrement()) 106 | question String 107 | variant_id Int 108 | variant QuestionVariant @relation(fields: [variant_id], references: [id]) 109 | solution String 110 | user_submissions QuestionSubmission[] 111 | exam_questions ExamQuestion[] 112 | created_at DateTime @default(now()) 113 | updated_at DateTime @updatedAt 114 | question_options QuestionOption[] 115 | exam_question_scores ExamSubmission[] 116 | 117 | @@map("questions") 118 | } 119 | 120 | model ExamQuestion { 121 | exam_id Int 122 | question_id Int 123 | exam Exam @relation(fields: [exam_id], references: [id]) 124 | question Question @relation(fields: [question_id], references: [id]) 125 | mark Int 126 | negative_mark Int 127 | 128 | @@unique([exam_id, question_id]) 129 | @@map("exam_questions") 130 | } 131 | 132 | model QuestionVariant { 133 | id Int @id @default(autoincrement()) 134 | variant String @unique 135 | questions Question[] 136 | created_at DateTime @default(now()) 137 | updated_at DateTime @updatedAt 138 | 139 | @@map("question_variants") 140 | } 141 | 142 | model QuestionSubmission { 143 | exam_id Int 144 | exam Exam @relation(fields: [exam_id], references: [id]) 145 | question_id Int 146 | question Question @relation(fields: [question_id], references: [id]) 147 | user_id Int 148 | option String 149 | 150 | @@unique([exam_id, question_id, user_id, option]) 151 | @@map("question_submissions") 152 | } 153 | 154 | model ExamSubmission { 155 | exam_id Int @unique 156 | exam Exam @relation(fields: [exam_id], references: [id]) 157 | question_id Int 158 | question Question @relation(fields: [question_id], references: [id]) 159 | user_id Int 160 | user User @relation(fields: [user_id], references: [id]) 161 | score Int @default(0) 162 | 163 | @@unique([exam_id, question_id, user_id]) 164 | @@map("exam_submissions") 165 | } 166 | 167 | model QuestionOption { 168 | question_id Int 169 | question Question @relation(fields: [question_id], references: [id]) 170 | option String 171 | 172 | @@unique([question_id, option]) 173 | @@map("question_options") 174 | } 175 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | /* Basic Options */ 5 | // "incremental": true, /* Enable incremental compilation */ 6 | "target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, 7 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 8 | // "lib": [], /* Specify library files to be included in the compilation. */ 9 | // "allowJs": true, /* Allow javascript files to be compiled. */ 10 | // "checkJs": true, /* Report errors in .js files. */ 11 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 12 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 13 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 14 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 15 | // "outFile": "./", /* Concatenate and emit output to single file. */ 16 | "outDir": "./dist" /* Redirect output structure to the directory. */, 17 | "rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, 18 | // "composite": true, /* Enable project compilation */ 19 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 20 | // "removeComments": true, /* Do not emit comments to output. */ 21 | // "noEmit": true, /* Do not emit outputs. */ 22 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 23 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 24 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 25 | 26 | /* Strict Type-Checking Options */ 27 | "strict": true /* Enable all strict type-checking options. */, 28 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 29 | // "strictNullChecks": true, /* Enable strict null checks. */ 30 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 31 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 32 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 33 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 34 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 35 | 36 | /* Additional Checks */ 37 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 38 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 39 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 40 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 41 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 42 | 43 | /* Module Resolution Options */ 44 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, 45 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 46 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 47 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 48 | // "typeRoots": [], /* List of folders to include type definitions from. */ 49 | // "types": [], /* Type declaration files to be included in compilation. */ 50 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 51 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 52 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 53 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 54 | 55 | /* Source Map Options */ 56 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 59 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 60 | 61 | /* Experimental Options */ 62 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 63 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 64 | 65 | /* Advanced Options */ 66 | "skipLibCheck": true /* Skip type checking of declaration files. */, 67 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/controllers/userController.ts: -------------------------------------------------------------------------------- 1 | import catchAsync from '../utils/catchAsync'; 2 | import { SuccessResponse } from '../utils/response-handler'; 3 | import { CustomRequest } from '../utils/CustomInterfaces/CustomRequest'; 4 | import CustomError from '../errors/custom-error'; 5 | import { Response, Request } from 'express'; 6 | import { getDb } from '../database/dbConnection'; 7 | import { v4 as uuid } from 'uuid'; 8 | import jwt from 'jsonwebtoken'; 9 | import bcrypt from 'bcrypt'; 10 | 11 | const defaultDp = 12 | 'https://www.tenforums.com/geek/gars/images/2/types/thumb_15951118880user.png'; 13 | 14 | export const getCurrentUser = catchAsync( 15 | async (req: CustomRequest, res: Response) => { 16 | return res.status(200).json(SuccessResponse(req.user, 'User found')); 17 | } 18 | ); 19 | 20 | export const getuser = catchAsync(async (req: CustomRequest, res: Response) => { 21 | const db = getDb(); 22 | const id: String = req.params.id || ''; 23 | let query = 'select * from `User` where `id` = ?'; 24 | const [rows, fields] = await db.execute(query, [id]); 25 | if (rows && rows.length > 0) { 26 | res.status(200).json(SuccessResponse(rows[0], 'User Found !')); 27 | //console.log(rows); 28 | } else res.status(500).send('User Not Found !'); 29 | }); 30 | 31 | //Register ===================================== 32 | export const registerUser = catchAsync(async (req: Request, res: Response) => { 33 | const db = getDb(); 34 | const { 35 | name, 36 | email, 37 | bio, 38 | password, 39 | institution, 40 | phoneNumber, 41 | dob, 42 | address, 43 | gender, 44 | } = req.body; 45 | if ( 46 | !name || 47 | !email || 48 | !password || 49 | !institution || 50 | !phoneNumber || 51 | !dob || 52 | !gender 53 | ) 54 | throw new CustomError('Some Fields are missing !', 500); 55 | let findByEmail = 56 | 'select count(*) as userExists from `User` where `email`=? limit 1'; 57 | let [rows] = await db.execute(findByEmail, [email]); 58 | if (rows[0].userExists) 59 | throw new CustomError('User with this email id already exists !', 500); 60 | let registerUser = 61 | 'insert into `User` (`id`,`name`,`email`,`bio`,`image`,`password`,`institution`,`phoneNumber`,`dob`,`address`,`gender`) values(?,?,?,?,?,?,?,?,?,?,?)'; 62 | const salt = await bcrypt.genSalt(); 63 | const passwordHash = await bcrypt.hash(password, salt); 64 | let id = uuid(); 65 | const result = await db.execute(registerUser, [ 66 | id, 67 | name, 68 | email, 69 | bio || ' ', 70 | defaultDp, 71 | passwordHash, 72 | institution, 73 | phoneNumber, 74 | dob, 75 | address || ' ', 76 | gender, 77 | ]); 78 | const user = { 79 | id, 80 | name, 81 | email, 82 | bio, 83 | image: defaultDp, 84 | institution, 85 | phoneNumber, 86 | dob, 87 | address: address || ' ', 88 | gender, 89 | }; 90 | const token = jwt.sign(user, process.env.ACCESS_TOKEN_SECRET!); 91 | if (result) 92 | res.status(200).json(SuccessResponse({ token, user }, 'User Inserted !')); 93 | else throw new CustomError('User not inserted!', 500); 94 | }); 95 | //============================================== 96 | 97 | //login ======================================== 98 | export const loginUser = catchAsync(async (req: Request, res: Response) => { 99 | const db = getDb(); 100 | const { email, password } = req.body; 101 | if (!email || !password) 102 | throw new CustomError('Some Fields are missing !', 500); 103 | let findByEmail = 'select * from `User` where `email`=?'; 104 | let [rows, fields] = await db.execute(findByEmail, [email]); 105 | if (rows.length != 1) throw new CustomError('No User with this Email!', 500); 106 | const user = rows[0]; 107 | const isMatch = await bcrypt.compare(password, user.password); 108 | delete user.password; 109 | if (!isMatch) throw new CustomError("Password does'nt match !", 404); 110 | const token = jwt.sign(user, process.env.ACCESS_TOKEN_SECRET!); 111 | res.status(200).json(SuccessResponse({ token, user }, 'User Logged in!')); 112 | }); 113 | //============================================== 114 | 115 | //logout ======================================== 116 | export const logoutUser = catchAsync( 117 | async (req: CustomRequest, res: Response) => { 118 | res.status(200).send('Logout Successfull !'); 119 | } 120 | ); 121 | //================================================ 122 | 123 | //edit========================================== 124 | export const editUser = catchAsync( 125 | async (req: CustomRequest, res: Response) => { 126 | const db = getDb(); 127 | const userId = req.user?.id; 128 | const { name, bio, institution, phoneNumber, dob, address, image, gender } = 129 | req.body; 130 | if (!name || !institution || !phoneNumber || !dob || !gender) 131 | throw new CustomError('Some Fields are missing !', 500); 132 | let query = 133 | 'update `User` set `name`=?, bio=?, `institution`=?, `phoneNumber`=?, `dob`=?, `address`=?, `image`=?, `gender`=? where `id`=?'; 134 | const [rows] = await db.execute(query, [ 135 | name, 136 | bio, 137 | institution, 138 | phoneNumber, 139 | dob, 140 | address, 141 | image, 142 | gender, 143 | userId, 144 | ]); 145 | if (!rows.affectedRows) throw new CustomError('User Not updated', 500); 146 | return res.status(200).json(SuccessResponse(req.user, 'User Updated!')); 147 | } 148 | ); 149 | //============================================== 150 | 151 | export const getExamHosted = catchAsync( 152 | async (req: CustomRequest, res: Response) => { 153 | const db = getDb(); 154 | const userId = req.user?.id; 155 | let query = 156 | 'select id,name,description,image,userId,tags,startTime,duration,ongoing,finished,isPrivate from `Exam` where `userId`=?'; 157 | const [rows] = await db.execute(query, [userId]); 158 | res.status(200).json(SuccessResponse(rows, 'The Exam hosted are')); 159 | } 160 | ); 161 | 162 | export const getExamGiven = catchAsync( 163 | async (req: CustomRequest, res: Response) => { 164 | const db = getDb(); 165 | const userId = req.user?.id; 166 | console.log(req.user); 167 | const query = 168 | 'select e.* from `Exam` as e,`Exam-Participants` as ep where ep.`examId`=e.`id` and ep.`participantId`=?'; 169 | const [rows] = await db.execute(query, [userId]); 170 | res.status(200).json(SuccessResponse(rows, 'Exams given are : ')); 171 | } 172 | ); 173 | -------------------------------------------------------------------------------- /prisma/migrations/20220406082433_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE `User` ( 3 | `id` INTEGER NOT NULL AUTO_INCREMENT, 4 | `name` VARCHAR(191) NOT NULL, 5 | `email` VARCHAR(191) NOT NULL, 6 | `bio` VARCHAR(191) NULL, 7 | `dob` DATETIME(3) NOT NULL, 8 | `address` VARCHAR(255) NULL, 9 | `country` VARCHAR(191) NULL, 10 | `profile_image` VARCHAR(191) NULL, 11 | `password` VARCHAR(191) NOT NULL, 12 | `latest_degree` VARCHAR(191) NULL, 13 | `Institution` VARCHAR(191) NULL, 14 | `gender` VARCHAR(191) NULL, 15 | `country_code` INTEGER NOT NULL, 16 | `phone` VARCHAR(191) NOT NULL, 17 | `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 18 | `updated_at` DATETIME(3) NOT NULL, 19 | 20 | UNIQUE INDEX `User_email_key`(`email`), 21 | PRIMARY KEY (`id`) 22 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 23 | 24 | -- CreateTable 25 | CREATE TABLE `Exam` ( 26 | `id` INTEGER NOT NULL AUTO_INCREMENT, 27 | `creator_id` INTEGER NOT NULL, 28 | `name` VARCHAR(191) NOT NULL, 29 | `description` VARCHAR(191) NULL, 30 | `banner` VARCHAR(191) NULL, 31 | `thumbnail` VARCHAR(191) NULL, 32 | `start_time` DATETIME(3) NOT NULL, 33 | `duration` BIGINT NOT NULL, 34 | `ongoing` BOOLEAN NOT NULL DEFAULT false, 35 | `is_private` BOOLEAN NOT NULL DEFAULT false, 36 | `participant_count` INTEGER NOT NULL DEFAULT 0, 37 | `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 38 | `updated_at` DATETIME(3) NOT NULL, 39 | 40 | PRIMARY KEY (`id`) 41 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 42 | 43 | -- CreateTable 44 | CREATE TABLE `ExamParticipant` ( 45 | `exam_id` INTEGER NOT NULL, 46 | `participant_id` INTEGER NOT NULL, 47 | `score` INTEGER NULL, 48 | `finish_time` INTEGER NULL, 49 | `rank` INTEGER NULL, 50 | `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 51 | `updated_at` DATETIME(3) NOT NULL, 52 | 53 | UNIQUE INDEX `ExamParticipant_exam_id_participant_id_key`(`exam_id`, `participant_id`) 54 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 55 | 56 | -- CreateTable 57 | CREATE TABLE `PrivateExamEmail` ( 58 | `examId` INTEGER NOT NULL, 59 | `email` VARCHAR(191) NOT NULL, 60 | `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 61 | `updated_at` DATETIME(3) NOT NULL, 62 | 63 | UNIQUE INDEX `PrivateExamEmail_examId_email_key`(`examId`, `email`) 64 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 65 | 66 | -- CreateTable 67 | CREATE TABLE `Tag` ( 68 | `id` INTEGER NOT NULL AUTO_INCREMENT, 69 | `tag` VARCHAR(191) NOT NULL, 70 | `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 71 | `updated_at` DATETIME(3) NOT NULL, 72 | 73 | UNIQUE INDEX `Tag_tag_key`(`tag`), 74 | PRIMARY KEY (`id`) 75 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 76 | 77 | -- CreateTable 78 | CREATE TABLE `ExamTag` ( 79 | `exam_id` INTEGER NOT NULL, 80 | `tag_id` INTEGER NOT NULL, 81 | `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 82 | `updated_at` DATETIME(3) NOT NULL, 83 | 84 | UNIQUE INDEX `ExamTag_exam_id_tag_id_key`(`exam_id`, `tag_id`) 85 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 86 | 87 | -- CreateTable 88 | CREATE TABLE `Question` ( 89 | `id` INTEGER NOT NULL AUTO_INCREMENT, 90 | `question` VARCHAR(191) NOT NULL, 91 | `variant_id` INTEGER NOT NULL, 92 | `solution` VARCHAR(191) NOT NULL, 93 | 94 | PRIMARY KEY (`id`) 95 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 96 | 97 | -- CreateTable 98 | CREATE TABLE `ExamQuestion` ( 99 | `exam_id` INTEGER NOT NULL, 100 | `question_id` INTEGER NOT NULL, 101 | `mark` INTEGER NOT NULL, 102 | `negative_mark` INTEGER NOT NULL, 103 | 104 | UNIQUE INDEX `ExamQuestion_exam_id_question_id_key`(`exam_id`, `question_id`) 105 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 106 | 107 | -- CreateTable 108 | CREATE TABLE `QuestionVariant` ( 109 | `id` INTEGER NOT NULL AUTO_INCREMENT, 110 | `variant` VARCHAR(191) NOT NULL, 111 | 112 | UNIQUE INDEX `QuestionVariant_variant_key`(`variant`), 113 | PRIMARY KEY (`id`) 114 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 115 | 116 | -- CreateTable 117 | CREATE TABLE `QuestionUserAnswer` ( 118 | `id` INTEGER NOT NULL AUTO_INCREMENT, 119 | `exam_id` INTEGER NOT NULL, 120 | `question_id` INTEGER NOT NULL, 121 | `user_id` INTEGER NOT NULL, 122 | `given_answer` VARCHAR(191) NOT NULL, 123 | `score` INTEGER NOT NULL DEFAULT 0, 124 | 125 | PRIMARY KEY (`id`) 126 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 127 | 128 | -- AddForeignKey 129 | ALTER TABLE `Exam` ADD CONSTRAINT `Exam_creator_id_fkey` FOREIGN KEY (`creator_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; 130 | 131 | -- AddForeignKey 132 | ALTER TABLE `ExamParticipant` ADD CONSTRAINT `ExamParticipant_participant_id_fkey` FOREIGN KEY (`participant_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; 133 | 134 | -- AddForeignKey 135 | ALTER TABLE `ExamParticipant` ADD CONSTRAINT `ExamParticipant_exam_id_fkey` FOREIGN KEY (`exam_id`) REFERENCES `Exam`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; 136 | 137 | -- AddForeignKey 138 | ALTER TABLE `PrivateExamEmail` ADD CONSTRAINT `PrivateExamEmail_examId_fkey` FOREIGN KEY (`examId`) REFERENCES `Exam`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; 139 | 140 | -- AddForeignKey 141 | ALTER TABLE `ExamTag` ADD CONSTRAINT `ExamTag_exam_id_fkey` FOREIGN KEY (`exam_id`) REFERENCES `Exam`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; 142 | 143 | -- AddForeignKey 144 | ALTER TABLE `ExamTag` ADD CONSTRAINT `ExamTag_tag_id_fkey` FOREIGN KEY (`tag_id`) REFERENCES `Tag`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; 145 | 146 | -- AddForeignKey 147 | ALTER TABLE `Question` ADD CONSTRAINT `Question_variant_id_fkey` FOREIGN KEY (`variant_id`) REFERENCES `QuestionVariant`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; 148 | 149 | -- AddForeignKey 150 | ALTER TABLE `ExamQuestion` ADD CONSTRAINT `ExamQuestion_exam_id_fkey` FOREIGN KEY (`exam_id`) REFERENCES `Exam`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; 151 | 152 | -- AddForeignKey 153 | ALTER TABLE `ExamQuestion` ADD CONSTRAINT `ExamQuestion_question_id_fkey` FOREIGN KEY (`question_id`) REFERENCES `Question`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; 154 | 155 | -- AddForeignKey 156 | ALTER TABLE `QuestionUserAnswer` ADD CONSTRAINT `QuestionUserAnswer_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; 157 | 158 | -- AddForeignKey 159 | ALTER TABLE `QuestionUserAnswer` ADD CONSTRAINT `QuestionUserAnswer_exam_id_fkey` FOREIGN KEY (`exam_id`) REFERENCES `Exam`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; 160 | 161 | -- AddForeignKey 162 | ALTER TABLE `QuestionUserAnswer` ADD CONSTRAINT `QuestionUserAnswer_question_id_fkey` FOREIGN KEY (`question_id`) REFERENCES `Question`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; 163 | -------------------------------------------------------------------------------- /dist/controllers/userController.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __importDefault = (this && this.__importDefault) || function (mod) { 12 | return (mod && mod.__esModule) ? mod : { "default": mod }; 13 | }; 14 | Object.defineProperty(exports, "__esModule", { value: true }); 15 | exports.getExamGiven = exports.getExamHosted = exports.editUser = exports.logoutUser = exports.loginUser = exports.registerUser = exports.getuser = exports.getCurrentUser = void 0; 16 | const catchAsync_1 = __importDefault(require("../utils/catchAsync")); 17 | const response_handler_1 = require("../utils/response-handler"); 18 | const custom_error_1 = __importDefault(require("../errors/custom-error")); 19 | const dbConnection_1 = require("../database/dbConnection"); 20 | const uuid_1 = require("uuid"); 21 | const jsonwebtoken_1 = __importDefault(require("jsonwebtoken")); 22 | const bcrypt_1 = __importDefault(require("bcrypt")); 23 | const defaultDp = 'https://www.tenforums.com/geek/gars/images/2/types/thumb_15951118880user.png'; 24 | exports.getCurrentUser = (0, catchAsync_1.default)((req, res) => __awaiter(void 0, void 0, void 0, function* () { 25 | return res.status(200).json((0, response_handler_1.SuccessResponse)(req.user, 'User found')); 26 | })); 27 | exports.getuser = (0, catchAsync_1.default)((req, res) => __awaiter(void 0, void 0, void 0, function* () { 28 | const db = (0, dbConnection_1.getDb)(); 29 | const id = req.params.id || ''; 30 | let query = 'select * from `User` where `id` = ?'; 31 | const [rows, fields] = yield db.execute(query, [id]); 32 | if (rows && rows.length > 0) { 33 | res.status(200).json((0, response_handler_1.SuccessResponse)(rows[0], 'User Found !')); 34 | //console.log(rows); 35 | } 36 | else 37 | res.status(500).send('User Not Found !'); 38 | })); 39 | //Register ===================================== 40 | exports.registerUser = (0, catchAsync_1.default)((req, res) => __awaiter(void 0, void 0, void 0, function* () { 41 | const db = (0, dbConnection_1.getDb)(); 42 | const { name, email, bio, password, institution, phoneNumber, dob, address, gender, } = req.body; 43 | if (!name || 44 | !email || 45 | !password || 46 | !institution || 47 | !phoneNumber || 48 | !dob || 49 | !gender) 50 | throw new custom_error_1.default('Some Fields are missing !', 500); 51 | let findByEmail = 'select count(*) as userExists from `User` where `email`=? limit 1'; 52 | let [rows] = yield db.execute(findByEmail, [email]); 53 | if (rows[0].userExists) 54 | throw new custom_error_1.default('User with this email id already exists !', 500); 55 | let registerUser = 'insert into `User` (`id`,`name`,`email`,`bio`,`image`,`password`,`institution`,`phoneNumber`,`dob`,`address`,`gender`) values(?,?,?,?,?,?,?,?,?,?,?)'; 56 | const salt = yield bcrypt_1.default.genSalt(); 57 | const passwordHash = yield bcrypt_1.default.hash(password, salt); 58 | let id = (0, uuid_1.v4)(); 59 | const result = yield db.execute(registerUser, [ 60 | id, 61 | name, 62 | email, 63 | bio || ' ', 64 | defaultDp, 65 | passwordHash, 66 | institution, 67 | phoneNumber, 68 | dob, 69 | address || ' ', 70 | gender, 71 | ]); 72 | const user = { 73 | id, 74 | name, 75 | email, 76 | bio, 77 | image: defaultDp, 78 | institution, 79 | phoneNumber, 80 | dob, 81 | address: address || ' ', 82 | gender, 83 | }; 84 | const token = jsonwebtoken_1.default.sign(user, process.env.ACCESS_TOKEN_SECRET); 85 | if (result) 86 | res.status(200).json((0, response_handler_1.SuccessResponse)({ token, user }, 'User Inserted !')); 87 | else 88 | throw new custom_error_1.default('User not inserted!', 500); 89 | })); 90 | //============================================== 91 | //login ======================================== 92 | exports.loginUser = (0, catchAsync_1.default)((req, res) => __awaiter(void 0, void 0, void 0, function* () { 93 | const db = (0, dbConnection_1.getDb)(); 94 | const { email, password } = req.body; 95 | if (!email || !password) 96 | throw new custom_error_1.default('Some Fields are missing !', 500); 97 | let findByEmail = 'select * from `User` where `email`=?'; 98 | let [rows, fields] = yield db.execute(findByEmail, [email]); 99 | if (rows.length != 1) 100 | throw new custom_error_1.default('No User with this Email!', 500); 101 | const user = rows[0]; 102 | const isMatch = yield bcrypt_1.default.compare(password, user.password); 103 | delete user.password; 104 | if (!isMatch) 105 | throw new custom_error_1.default("Password does'nt match !", 404); 106 | const token = jsonwebtoken_1.default.sign(user, process.env.ACCESS_TOKEN_SECRET); 107 | res.status(200).json((0, response_handler_1.SuccessResponse)({ token, user }, 'User Logged in!')); 108 | })); 109 | //============================================== 110 | //logout ======================================== 111 | exports.logoutUser = (0, catchAsync_1.default)((req, res) => __awaiter(void 0, void 0, void 0, function* () { 112 | res.status(200).send('Logout Successfull !'); 113 | })); 114 | //================================================ 115 | //edit========================================== 116 | exports.editUser = (0, catchAsync_1.default)((req, res) => __awaiter(void 0, void 0, void 0, function* () { 117 | var _a; 118 | const db = (0, dbConnection_1.getDb)(); 119 | const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id; 120 | const { name, bio, institution, phoneNumber, dob, address, image, gender } = req.body; 121 | if (!name || !institution || !phoneNumber || !dob || !gender) 122 | throw new custom_error_1.default('Some Fields are missing !', 500); 123 | let query = 'update `User` set `name`=?, bio=?, `institution`=?, `phoneNumber`=?, `dob`=?, `address`=?, `image`=?, `gender`=? where `id`=?'; 124 | const [rows] = yield db.execute(query, [ 125 | name, 126 | bio, 127 | institution, 128 | phoneNumber, 129 | dob, 130 | address, 131 | image, 132 | gender, 133 | userId, 134 | ]); 135 | if (!rows.affectedRows) 136 | throw new custom_error_1.default('User Not updated', 500); 137 | return res.status(200).json((0, response_handler_1.SuccessResponse)(req.user, 'User Updated!')); 138 | })); 139 | //============================================== 140 | exports.getExamHosted = (0, catchAsync_1.default)((req, res) => __awaiter(void 0, void 0, void 0, function* () { 141 | var _b; 142 | const db = (0, dbConnection_1.getDb)(); 143 | const userId = (_b = req.user) === null || _b === void 0 ? void 0 : _b.id; 144 | let query = 'select id,name,description,image,userId,tags,startTime,duration,ongoing,finished,isPrivate from `Exam` where `userId`=?'; 145 | const [rows] = yield db.execute(query, [userId]); 146 | res.status(200).json((0, response_handler_1.SuccessResponse)(rows, 'The Exam hosted are')); 147 | })); 148 | exports.getExamGiven = (0, catchAsync_1.default)((req, res) => __awaiter(void 0, void 0, void 0, function* () { 149 | var _c; 150 | const db = (0, dbConnection_1.getDb)(); 151 | const userId = (_c = req.user) === null || _c === void 0 ? void 0 : _c.id; 152 | console.log(req.user); 153 | const query = 'select e.* from `Exam` as e,`Exam-Participants` as ep where ep.`examId`=e.`id` and ep.`participantId`=?'; 154 | const [rows] = yield db.execute(query, [userId]); 155 | res.status(200).json((0, response_handler_1.SuccessResponse)(rows, 'Exams given are : ')); 156 | })); 157 | -------------------------------------------------------------------------------- /src/utils/examFunctions.ts: -------------------------------------------------------------------------------- 1 | import scheduler from 'node-schedule'; 2 | import db, { getDb } from '../database/dbConnection'; 3 | import { 4 | participantDataInterface, 5 | answerInterface, 6 | answersObjInterface, 7 | participantRankingInterface, 8 | participantRankingData, 9 | } from './CustomInterfaces/ParticipantDataInterfaces'; 10 | import { 11 | examInterface, 12 | RootObject, 13 | RootQuestionsObject, 14 | } from './CustomInterfaces/ExamInterface'; 15 | import { evaluateQuestion } from './questionFunctions'; 16 | import CustomError from '../errors/custom-error'; 17 | import { 18 | fillInTheBlanksInterface, 19 | mcqInterface, 20 | multipleOptionsInterface, 21 | } from './CustomInterfaces/QuestionInterfaces'; 22 | 23 | export const parseExam = (examObject: examInterface): examInterface => { 24 | examObject.tags = JSON.parse(examObject.tags); 25 | if (examObject.questions) 26 | examObject.questions = JSON.parse(examObject.questions); 27 | return examObject; 28 | }; 29 | 30 | function shuffleArray(array: Array) { 31 | var currentIndex = array.length, 32 | temporaryValue, 33 | randomIndex; 34 | while (0 !== currentIndex) { 35 | randomIndex = Math.floor(Math.random() * currentIndex); 36 | currentIndex -= 1; 37 | temporaryValue = array[currentIndex]; 38 | array[currentIndex] = array[randomIndex]; 39 | array[randomIndex] = temporaryValue; 40 | if ( 41 | array[randomIndex].type == 'mcq' || 42 | array[randomIndex].type == 'multipleOptions' 43 | ) 44 | array[randomIndex].options = shuffleArray(array[randomIndex].options); 45 | if ( 46 | array[currentIndex].type == 'mcq' || 47 | array[currentIndex].type == 'multipleOptions' 48 | ) 49 | array[currentIndex].options = shuffleArray(array[currentIndex].options); 50 | } 51 | return array; 52 | } 53 | 54 | export const shuffleExam = (examObject: examInterface) => { 55 | examObject.questions = shuffleArray(examObject.questions); 56 | // console.log(examObject.questions); 57 | return examObject; 58 | }; 59 | 60 | const getDefaultAns = (type: string) => { 61 | if (type == 'mcq' || type == 'multipleOptions') return []; 62 | return ''; //this means fillInTheBlanks type 63 | }; 64 | 65 | export const getExamWithUserAns = ( 66 | examQuestions: RootQuestionsObject, 67 | answers: answersObjInterface 68 | ) => { 69 | let questions = examQuestions; 70 | let questionsArray; 71 | questionsArray = Object.keys(questions).map((questionId: number | string) => { 72 | let answer = answers[questionId]?.answer; 73 | let question: RootObject = questions[questionId]; 74 | if (!answer) answer = getDefaultAns(question.type); //this is triggered if the answer is not available then a default ans value is taken 75 | question.givenOption = answer; 76 | return question; 77 | }); 78 | return questionsArray; 79 | }; 80 | 81 | export const scheduleExam = (id: string, date: number, duration: number) => { 82 | //date = new Date(date); 83 | scheduler.scheduleJob(id + 'start', new Date(date), () => { 84 | startExam(id); 85 | destroyScheduler(id + 'start'); 86 | }); 87 | 88 | scheduler.scheduleJob(id + 'end', new Date(date + duration + 120000), () => { 89 | evaluateExam(id); 90 | console.log(id, ' Ended '); 91 | destroyScheduler(id + 'end'); 92 | }); 93 | }; 94 | 95 | export const startExam = async (id: String) => { 96 | const db = getDb(); 97 | let updateExamById = 'update `Exam` set `ongoing`=? where `id`=?'; 98 | let result = await db.execute(updateExamById, [true, id]); 99 | if (result) console.log(id, ' Started !'); 100 | else console.log(id, ' Error occured to start Exam'); 101 | }; 102 | 103 | export const scheduleOnServerRestart = async () => { 104 | const db = getDb(); 105 | let query = 106 | 'select `id`,`startTime`,`duration` from `Exam` where `startTime`+`duration`>=?'; 107 | let [rows] = await db.execute(query, [new Date().getTime()]); 108 | if (rows.length > 0) { 109 | console.log(rows); 110 | rows.map((exam: examInterface) => 111 | scheduleExam(exam.id, exam.startTime, exam.duration) 112 | ); 113 | } else console.log('No Exams to schedule!'); 114 | }; 115 | export const destroyScheduler = (id: any) => { 116 | let task = scheduler.scheduledJobs[id]; 117 | if (task) task.cancel(); 118 | }; 119 | 120 | export const removeCorrectOptions = (examObject: examInterface) => { 121 | examObject.questions = [...examObject.questions].map((question) => { 122 | delete question['correctOption']; 123 | return question; 124 | }); 125 | }; 126 | 127 | export const evaluateParticipantData = ( 128 | examData: examInterface, 129 | participantAnswers: answersObjInterface 130 | ): number => { 131 | // console.log(participantAnswers, ' ', examData.questions); 132 | let totalScore = 0; 133 | Object.keys(participantAnswers).map((key) => { 134 | let answer = participantAnswers[key]; 135 | let question = examData.questions[key]; 136 | if (!question || !answer || !(question.type === answer.type)) { 137 | console.log('error in question :', question, ' answer: ', answer); 138 | totalScore += 0; 139 | } else { 140 | totalScore += evaluateQuestion[question.type](question, answer); 141 | } 142 | }); 143 | return totalScore; 144 | }; 145 | 146 | const evaluateRanking = async ( 147 | examId: string | undefined, 148 | totalRankingData: participantRankingData[] 149 | ) => { 150 | const db = getDb(); 151 | totalRankingData.sort( 152 | (a: participantRankingInterface, b: participantRankingInterface) => { 153 | if (a.totalScore > b.totalScore) return -1; 154 | else if (b.totalScore > a.totalScore) return 1; 155 | else { 156 | let aFinishTime = a.finishTime || new Date().getTime() + 120000; 157 | let bFinishTime = b.finishTime || new Date().getTime() + 120000; 158 | return aFinishTime - bFinishTime; 159 | } 160 | } 161 | ); 162 | console.log('rank sorted data is:', totalRankingData); 163 | let updateQuery = 'update `Exam-Participants` set `rank`= (case '; 164 | totalRankingData.map( 165 | (rankingData: participantRankingInterface, index: number) => { 166 | let rank = index + 1; 167 | updateQuery += 168 | " when `participantId`='" + rankingData.id + "' then " + rank; 169 | } 170 | ); 171 | updateQuery += " end) where `examId`='" + examId + "';"; 172 | const [rows] = await db.execute(updateQuery); 173 | if (!rows.affectedRows) 174 | throw new CustomError('Error while rank update!', 500); 175 | }; 176 | 177 | // Note : Evaluate exam modifies question data from arrays to hashmaps for algorithim purposes! 178 | export const evaluateExam = async (id: string | undefined) => { 179 | const db = getDb(); 180 | let query; 181 | query = 'select * from `Exam` where id=? limit 1'; 182 | const [examRows] = await db.execute(query, [id]); 183 | if (examRows.length != 1) throw new CustomError('Exam not found!', 500); 184 | let examData: examInterface = parseExam(examRows[0]); 185 | let questionsObj: any = {}; 186 | examData.questions.map((question: any) => { 187 | questionsObj[question.id] = question; 188 | }); 189 | examData.questions = questionsObj; 190 | query = 'select * from `Exam-Participants` where `examId`=? and `virtual`=?'; 191 | const [participantRows] = await db.execute(query, [id, false]); 192 | if (participantRows.length > 0) { 193 | let totalRankingData: participantRankingInterface[] = []; 194 | let updateQuery = 'update `Exam-Participants` set `totalScore`= (case '; 195 | participantRows.map((data: participantDataInterface) => { 196 | let participantAnswers: answersObjInterface = JSON.parse( 197 | data.answers || '{}' 198 | ); 199 | // console.log('xxxxxx ', participantAnswers); 200 | let totalScore: number = evaluateParticipantData( 201 | examData, 202 | participantAnswers 203 | ); 204 | updateQuery += 205 | " when `participantId`='" + data.participantId + "' then " + totalScore; 206 | // console.log(data.participantId, ' got : ', totalScore); 207 | totalRankingData.push( 208 | new participantRankingData( 209 | data.participantId, 210 | totalScore, 211 | data.finishTime 212 | ) 213 | ); 214 | }); 215 | updateQuery += " end) where `examId`='" + id + "';"; 216 | let [rows] = await db.execute(updateQuery); 217 | if (!rows.affectedRows) throw new CustomError('Score update error!', 500); 218 | await evaluateRanking(id, totalRankingData); 219 | } 220 | query = 'update `Exam` set `ongoing`=?,`finished`=? where `id`=?'; 221 | await db.execute(query, [false, true, id]); 222 | }; 223 | -------------------------------------------------------------------------------- /dist/utils/examFunctions.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __importDefault = (this && this.__importDefault) || function (mod) { 12 | return (mod && mod.__esModule) ? mod : { "default": mod }; 13 | }; 14 | Object.defineProperty(exports, "__esModule", { value: true }); 15 | exports.evaluateExam = exports.evaluateParticipantData = exports.removeCorrectOptions = exports.destroyScheduler = exports.scheduleOnServerRestart = exports.startExam = exports.scheduleExam = exports.getExamWithUserAns = exports.shuffleExam = exports.parseExam = void 0; 16 | const node_schedule_1 = __importDefault(require("node-schedule")); 17 | const dbConnection_1 = require("../database/dbConnection"); 18 | const ParticipantDataInterfaces_1 = require("./CustomInterfaces/ParticipantDataInterfaces"); 19 | const questionFunctions_1 = require("./questionFunctions"); 20 | const custom_error_1 = __importDefault(require("../errors/custom-error")); 21 | const parseExam = (examObject) => { 22 | examObject.tags = JSON.parse(examObject.tags); 23 | if (examObject.questions) 24 | examObject.questions = JSON.parse(examObject.questions); 25 | return examObject; 26 | }; 27 | exports.parseExam = parseExam; 28 | function shuffleArray(array) { 29 | var currentIndex = array.length, temporaryValue, randomIndex; 30 | while (0 !== currentIndex) { 31 | randomIndex = Math.floor(Math.random() * currentIndex); 32 | currentIndex -= 1; 33 | temporaryValue = array[currentIndex]; 34 | array[currentIndex] = array[randomIndex]; 35 | array[randomIndex] = temporaryValue; 36 | if (array[randomIndex].type == 'mcq' || 37 | array[randomIndex].type == 'multipleOptions') 38 | array[randomIndex].options = shuffleArray(array[randomIndex].options); 39 | if (array[currentIndex].type == 'mcq' || 40 | array[currentIndex].type == 'multipleOptions') 41 | array[currentIndex].options = shuffleArray(array[currentIndex].options); 42 | } 43 | return array; 44 | } 45 | const shuffleExam = (examObject) => { 46 | examObject.questions = shuffleArray(examObject.questions); 47 | // console.log(examObject.questions); 48 | return examObject; 49 | }; 50 | exports.shuffleExam = shuffleExam; 51 | const getDefaultAns = (type) => { 52 | if (type == 'mcq' || type == 'multipleOptions') 53 | return []; 54 | return ''; //this means fillInTheBlanks type 55 | }; 56 | const getExamWithUserAns = (examQuestions, answers) => { 57 | let questions = examQuestions; 58 | let questionsArray; 59 | questionsArray = Object.keys(questions).map((questionId) => { 60 | var _a; 61 | let answer = (_a = answers[questionId]) === null || _a === void 0 ? void 0 : _a.answer; 62 | let question = questions[questionId]; 63 | if (!answer) 64 | answer = getDefaultAns(question.type); //this is triggered if the answer is not available then a default ans value is taken 65 | question.givenOption = answer; 66 | return question; 67 | }); 68 | return questionsArray; 69 | }; 70 | exports.getExamWithUserAns = getExamWithUserAns; 71 | const scheduleExam = (id, date, duration) => { 72 | //date = new Date(date); 73 | node_schedule_1.default.scheduleJob(id + 'start', new Date(date), () => { 74 | (0, exports.startExam)(id); 75 | (0, exports.destroyScheduler)(id + 'start'); 76 | }); 77 | node_schedule_1.default.scheduleJob(id + 'end', new Date(date + duration + 120000), () => { 78 | (0, exports.evaluateExam)(id); 79 | console.log(id, ' Ended '); 80 | (0, exports.destroyScheduler)(id + 'end'); 81 | }); 82 | }; 83 | exports.scheduleExam = scheduleExam; 84 | const startExam = (id) => __awaiter(void 0, void 0, void 0, function* () { 85 | const db = (0, dbConnection_1.getDb)(); 86 | let updateExamById = 'update `Exam` set `ongoing`=? where `id`=?'; 87 | let result = yield db.execute(updateExamById, [true, id]); 88 | if (result) 89 | console.log(id, ' Started !'); 90 | else 91 | console.log(id, ' Error occured to start Exam'); 92 | }); 93 | exports.startExam = startExam; 94 | const scheduleOnServerRestart = () => __awaiter(void 0, void 0, void 0, function* () { 95 | const db = (0, dbConnection_1.getDb)(); 96 | let query = 'select `id`,`startTime`,`duration` from `Exam` where `startTime`+`duration`>=?'; 97 | let [rows] = yield db.execute(query, [new Date().getTime()]); 98 | if (rows.length > 0) { 99 | console.log(rows); 100 | rows.map((exam) => (0, exports.scheduleExam)(exam.id, exam.startTime, exam.duration)); 101 | } 102 | else 103 | console.log('No Exams to schedule!'); 104 | }); 105 | exports.scheduleOnServerRestart = scheduleOnServerRestart; 106 | const destroyScheduler = (id) => { 107 | let task = node_schedule_1.default.scheduledJobs[id]; 108 | if (task) 109 | task.cancel(); 110 | }; 111 | exports.destroyScheduler = destroyScheduler; 112 | const removeCorrectOptions = (examObject) => { 113 | examObject.questions = [...examObject.questions].map((question) => { 114 | delete question['correctOption']; 115 | return question; 116 | }); 117 | }; 118 | exports.removeCorrectOptions = removeCorrectOptions; 119 | const evaluateParticipantData = (examData, participantAnswers) => { 120 | // console.log(participantAnswers, ' ', examData.questions); 121 | let totalScore = 0; 122 | Object.keys(participantAnswers).map((key) => { 123 | let answer = participantAnswers[key]; 124 | let question = examData.questions[key]; 125 | if (!question || !answer || !(question.type === answer.type)) { 126 | console.log('error in question :', question, ' answer: ', answer); 127 | totalScore += 0; 128 | } 129 | else { 130 | totalScore += questionFunctions_1.evaluateQuestion[question.type](question, answer); 131 | } 132 | }); 133 | return totalScore; 134 | }; 135 | exports.evaluateParticipantData = evaluateParticipantData; 136 | const evaluateRanking = (examId, totalRankingData) => __awaiter(void 0, void 0, void 0, function* () { 137 | const db = (0, dbConnection_1.getDb)(); 138 | totalRankingData.sort((a, b) => { 139 | if (a.totalScore > b.totalScore) 140 | return -1; 141 | else if (b.totalScore > a.totalScore) 142 | return 1; 143 | else { 144 | let aFinishTime = a.finishTime || new Date().getTime() + 120000; 145 | let bFinishTime = b.finishTime || new Date().getTime() + 120000; 146 | return aFinishTime - bFinishTime; 147 | } 148 | }); 149 | console.log('rank sorted data is:', totalRankingData); 150 | let updateQuery = 'update `Exam-Participants` set `rank`= (case '; 151 | totalRankingData.map((rankingData, index) => { 152 | let rank = index + 1; 153 | updateQuery += 154 | " when `participantId`='" + rankingData.id + "' then " + rank; 155 | }); 156 | updateQuery += " end) where `examId`='" + examId + "';"; 157 | const [rows] = yield db.execute(updateQuery); 158 | if (!rows.affectedRows) 159 | throw new custom_error_1.default('Error while rank update!', 500); 160 | }); 161 | // Note : Evaluate exam modifies question data from arrays to hashmaps for algorithim purposes! 162 | const evaluateExam = (id) => __awaiter(void 0, void 0, void 0, function* () { 163 | const db = (0, dbConnection_1.getDb)(); 164 | let query; 165 | query = 'select * from `Exam` where id=? limit 1'; 166 | const [examRows] = yield db.execute(query, [id]); 167 | if (examRows.length != 1) 168 | throw new custom_error_1.default('Exam not found!', 500); 169 | let examData = (0, exports.parseExam)(examRows[0]); 170 | let questionsObj = {}; 171 | examData.questions.map((question) => { 172 | questionsObj[question.id] = question; 173 | }); 174 | examData.questions = questionsObj; 175 | query = 'select * from `Exam-Participants` where `examId`=? and `virtual`=?'; 176 | const [participantRows] = yield db.execute(query, [id, false]); 177 | if (participantRows.length > 0) { 178 | let totalRankingData = []; 179 | let updateQuery = 'update `Exam-Participants` set `totalScore`= (case '; 180 | participantRows.map((data) => { 181 | let participantAnswers = JSON.parse(data.answers || '{}'); 182 | // console.log('xxxxxx ', participantAnswers); 183 | let totalScore = (0, exports.evaluateParticipantData)(examData, participantAnswers); 184 | updateQuery += 185 | " when `participantId`='" + data.participantId + "' then " + totalScore; 186 | // console.log(data.participantId, ' got : ', totalScore); 187 | totalRankingData.push(new ParticipantDataInterfaces_1.participantRankingData(data.participantId, totalScore, data.finishTime)); 188 | }); 189 | updateQuery += " end) where `examId`='" + id + "';"; 190 | let [rows] = yield db.execute(updateQuery); 191 | if (!rows.affectedRows) 192 | throw new custom_error_1.default('Score update error!', 500); 193 | yield evaluateRanking(id, totalRankingData); 194 | } 195 | query = 'update `Exam` set `ongoing`=?,`finished`=? where `id`=?'; 196 | yield db.execute(query, [false, true, id]); 197 | }); 198 | exports.evaluateExam = evaluateExam; 199 | -------------------------------------------------------------------------------- /prisma/migrations/20220406091007_init/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the `Exam` table. If the table is not empty, all the data it contains will be lost. 5 | - You are about to drop the `ExamParticipant` table. If the table is not empty, all the data it contains will be lost. 6 | - You are about to drop the `ExamQuestion` table. If the table is not empty, all the data it contains will be lost. 7 | - You are about to drop the `ExamTag` table. If the table is not empty, all the data it contains will be lost. 8 | - You are about to drop the `PrivateExamEmail` table. If the table is not empty, all the data it contains will be lost. 9 | - You are about to drop the `Question` table. If the table is not empty, all the data it contains will be lost. 10 | - You are about to drop the `QuestionUserAnswer` table. If the table is not empty, all the data it contains will be lost. 11 | - You are about to drop the `QuestionVariant` table. If the table is not empty, all the data it contains will be lost. 12 | - You are about to drop the `Tag` table. If the table is not empty, all the data it contains will be lost. 13 | - You are about to drop the `User` table. If the table is not empty, all the data it contains will be lost. 14 | 15 | */ 16 | -- DropForeignKey 17 | ALTER TABLE `Exam` DROP FOREIGN KEY `Exam_creator_id_fkey`; 18 | 19 | -- DropForeignKey 20 | ALTER TABLE `ExamParticipant` DROP FOREIGN KEY `ExamParticipant_exam_id_fkey`; 21 | 22 | -- DropForeignKey 23 | ALTER TABLE `ExamParticipant` DROP FOREIGN KEY `ExamParticipant_participant_id_fkey`; 24 | 25 | -- DropForeignKey 26 | ALTER TABLE `ExamQuestion` DROP FOREIGN KEY `ExamQuestion_exam_id_fkey`; 27 | 28 | -- DropForeignKey 29 | ALTER TABLE `ExamQuestion` DROP FOREIGN KEY `ExamQuestion_question_id_fkey`; 30 | 31 | -- DropForeignKey 32 | ALTER TABLE `ExamTag` DROP FOREIGN KEY `ExamTag_exam_id_fkey`; 33 | 34 | -- DropForeignKey 35 | ALTER TABLE `ExamTag` DROP FOREIGN KEY `ExamTag_tag_id_fkey`; 36 | 37 | -- DropForeignKey 38 | ALTER TABLE `PrivateExamEmail` DROP FOREIGN KEY `PrivateExamEmail_examId_fkey`; 39 | 40 | -- DropForeignKey 41 | ALTER TABLE `Question` DROP FOREIGN KEY `Question_variant_id_fkey`; 42 | 43 | -- DropForeignKey 44 | ALTER TABLE `QuestionUserAnswer` DROP FOREIGN KEY `QuestionUserAnswer_exam_id_fkey`; 45 | 46 | -- DropForeignKey 47 | ALTER TABLE `QuestionUserAnswer` DROP FOREIGN KEY `QuestionUserAnswer_question_id_fkey`; 48 | 49 | -- DropForeignKey 50 | ALTER TABLE `QuestionUserAnswer` DROP FOREIGN KEY `QuestionUserAnswer_user_id_fkey`; 51 | 52 | -- DropTable 53 | DROP TABLE `Exam`; 54 | 55 | -- DropTable 56 | DROP TABLE `ExamParticipant`; 57 | 58 | -- DropTable 59 | DROP TABLE `ExamQuestion`; 60 | 61 | -- DropTable 62 | DROP TABLE `ExamTag`; 63 | 64 | -- DropTable 65 | DROP TABLE `PrivateExamEmail`; 66 | 67 | -- DropTable 68 | DROP TABLE `Question`; 69 | 70 | -- DropTable 71 | DROP TABLE `QuestionUserAnswer`; 72 | 73 | -- DropTable 74 | DROP TABLE `QuestionVariant`; 75 | 76 | -- DropTable 77 | DROP TABLE `Tag`; 78 | 79 | -- DropTable 80 | DROP TABLE `User`; 81 | 82 | -- CreateTable 83 | CREATE TABLE `users` ( 84 | `id` INTEGER NOT NULL AUTO_INCREMENT, 85 | `name` VARCHAR(191) NOT NULL, 86 | `email` VARCHAR(191) NOT NULL, 87 | `bio` VARCHAR(191) NULL, 88 | `dob` DATETIME(3) NOT NULL, 89 | `address` VARCHAR(255) NULL, 90 | `country` VARCHAR(191) NULL, 91 | `profile_image` VARCHAR(191) NULL, 92 | `password` VARCHAR(191) NOT NULL, 93 | `latest_degree` VARCHAR(191) NULL, 94 | `Institution` VARCHAR(191) NULL, 95 | `gender` VARCHAR(191) NULL, 96 | `country_code` INTEGER NOT NULL, 97 | `phone` VARCHAR(191) NOT NULL, 98 | `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 99 | `updated_at` DATETIME(3) NOT NULL, 100 | 101 | UNIQUE INDEX `users_email_key`(`email`), 102 | PRIMARY KEY (`id`) 103 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 104 | 105 | -- CreateTable 106 | CREATE TABLE `exams` ( 107 | `id` INTEGER NOT NULL AUTO_INCREMENT, 108 | `creator_id` INTEGER NOT NULL, 109 | `name` VARCHAR(191) NOT NULL, 110 | `description` VARCHAR(191) NULL, 111 | `banner` VARCHAR(191) NULL, 112 | `thumbnail` VARCHAR(191) NULL, 113 | `start_time` DATETIME(3) NOT NULL, 114 | `duration` BIGINT NOT NULL, 115 | `ongoing` BOOLEAN NOT NULL DEFAULT false, 116 | `is_private` BOOLEAN NOT NULL DEFAULT false, 117 | `participant_count` INTEGER NOT NULL DEFAULT 0, 118 | `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 119 | `updated_at` DATETIME(3) NOT NULL, 120 | 121 | PRIMARY KEY (`id`) 122 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 123 | 124 | -- CreateTable 125 | CREATE TABLE `exam_participants` ( 126 | `exam_id` INTEGER NOT NULL, 127 | `participant_id` INTEGER NOT NULL, 128 | `score` INTEGER NULL, 129 | `finish_time` INTEGER NULL, 130 | `rank` INTEGER NULL, 131 | 132 | UNIQUE INDEX `exam_participants_exam_id_participant_id_key`(`exam_id`, `participant_id`) 133 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 134 | 135 | -- CreateTable 136 | CREATE TABLE `private_exam_emails` ( 137 | `examId` INTEGER NOT NULL, 138 | `email` VARCHAR(191) NOT NULL, 139 | 140 | UNIQUE INDEX `private_exam_emails_examId_email_key`(`examId`, `email`) 141 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 142 | 143 | -- CreateTable 144 | CREATE TABLE `tags` ( 145 | `id` INTEGER NOT NULL AUTO_INCREMENT, 146 | `tag` VARCHAR(191) NOT NULL, 147 | `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 148 | `updated_at` DATETIME(3) NOT NULL, 149 | 150 | UNIQUE INDEX `tags_tag_key`(`tag`), 151 | PRIMARY KEY (`id`) 152 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 153 | 154 | -- CreateTable 155 | CREATE TABLE `exam_tags` ( 156 | `exam_id` INTEGER NOT NULL, 157 | `tag_id` INTEGER NOT NULL, 158 | 159 | UNIQUE INDEX `exam_tags_exam_id_tag_id_key`(`exam_id`, `tag_id`) 160 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 161 | 162 | -- CreateTable 163 | CREATE TABLE `questions` ( 164 | `id` INTEGER NOT NULL AUTO_INCREMENT, 165 | `question` VARCHAR(191) NOT NULL, 166 | `variant_id` INTEGER NOT NULL, 167 | `solution` VARCHAR(191) NOT NULL, 168 | `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 169 | `updated_at` DATETIME(3) NOT NULL, 170 | 171 | PRIMARY KEY (`id`) 172 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 173 | 174 | -- CreateTable 175 | CREATE TABLE `exam_questions` ( 176 | `exam_id` INTEGER NOT NULL, 177 | `question_id` INTEGER NOT NULL, 178 | `mark` INTEGER NOT NULL, 179 | `negative_mark` INTEGER NOT NULL, 180 | 181 | UNIQUE INDEX `exam_questions_exam_id_question_id_key`(`exam_id`, `question_id`) 182 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 183 | 184 | -- CreateTable 185 | CREATE TABLE `question_variants` ( 186 | `id` INTEGER NOT NULL AUTO_INCREMENT, 187 | `variant` VARCHAR(191) NOT NULL, 188 | `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 189 | `updated_at` DATETIME(3) NOT NULL, 190 | 191 | UNIQUE INDEX `question_variants_variant_key`(`variant`), 192 | PRIMARY KEY (`id`) 193 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 194 | 195 | -- CreateTable 196 | CREATE TABLE `question_exam_submissions` ( 197 | `exam_id` INTEGER NOT NULL, 198 | `question_id` INTEGER NOT NULL, 199 | `user_id` INTEGER NOT NULL, 200 | `submission` VARCHAR(191) NOT NULL, 201 | `score` INTEGER NOT NULL DEFAULT 0, 202 | 203 | UNIQUE INDEX `question_exam_submissions_exam_id_question_id_user_id_key`(`exam_id`, `question_id`, `user_id`) 204 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 205 | 206 | -- AddForeignKey 207 | ALTER TABLE `exams` ADD CONSTRAINT `exams_creator_id_fkey` FOREIGN KEY (`creator_id`) REFERENCES `users`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; 208 | 209 | -- AddForeignKey 210 | ALTER TABLE `exam_participants` ADD CONSTRAINT `exam_participants_participant_id_fkey` FOREIGN KEY (`participant_id`) REFERENCES `users`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; 211 | 212 | -- AddForeignKey 213 | ALTER TABLE `exam_participants` ADD CONSTRAINT `exam_participants_exam_id_fkey` FOREIGN KEY (`exam_id`) REFERENCES `exams`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; 214 | 215 | -- AddForeignKey 216 | ALTER TABLE `private_exam_emails` ADD CONSTRAINT `private_exam_emails_examId_fkey` FOREIGN KEY (`examId`) REFERENCES `exams`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; 217 | 218 | -- AddForeignKey 219 | ALTER TABLE `exam_tags` ADD CONSTRAINT `exam_tags_exam_id_fkey` FOREIGN KEY (`exam_id`) REFERENCES `exams`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; 220 | 221 | -- AddForeignKey 222 | ALTER TABLE `exam_tags` ADD CONSTRAINT `exam_tags_tag_id_fkey` FOREIGN KEY (`tag_id`) REFERENCES `tags`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; 223 | 224 | -- AddForeignKey 225 | ALTER TABLE `questions` ADD CONSTRAINT `questions_variant_id_fkey` FOREIGN KEY (`variant_id`) REFERENCES `question_variants`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; 226 | 227 | -- AddForeignKey 228 | ALTER TABLE `exam_questions` ADD CONSTRAINT `exam_questions_exam_id_fkey` FOREIGN KEY (`exam_id`) REFERENCES `exams`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; 229 | 230 | -- AddForeignKey 231 | ALTER TABLE `exam_questions` ADD CONSTRAINT `exam_questions_question_id_fkey` FOREIGN KEY (`question_id`) REFERENCES `questions`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; 232 | 233 | -- AddForeignKey 234 | ALTER TABLE `question_exam_submissions` ADD CONSTRAINT `question_exam_submissions_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; 235 | 236 | -- AddForeignKey 237 | ALTER TABLE `question_exam_submissions` ADD CONSTRAINT `question_exam_submissions_exam_id_fkey` FOREIGN KEY (`exam_id`) REFERENCES `exams`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; 238 | 239 | -- AddForeignKey 240 | ALTER TABLE `question_exam_submissions` ADD CONSTRAINT `question_exam_submissions_question_id_fkey` FOREIGN KEY (`question_id`) REFERENCES `questions`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; 241 | -------------------------------------------------------------------------------- /src/controllers/examController.ts: -------------------------------------------------------------------------------- 1 | import catchAsync from '../utils/catchAsync'; 2 | import { SuccessResponse } from '../utils/response-handler'; 3 | import CustomError from '../errors/custom-error'; 4 | import { Response, Request, NextFunction } from 'express'; 5 | import db, { getDb } from '../database/dbConnection'; 6 | import { v4 as uuid } from 'uuid'; 7 | import { CustomRequest } from '../utils/CustomInterfaces/CustomRequest'; 8 | import { 9 | mcqInterface, 10 | fillInTheBlanksInterface, 11 | multipleOptionsInterface, 12 | } from '../utils/CustomInterfaces/QuestionInterfaces'; 13 | import { 14 | scheduleExam, 15 | destroyScheduler, 16 | parseExam, 17 | shuffleExam, 18 | removeCorrectOptions, 19 | evaluateExam, 20 | getExamWithUserAns, 21 | } from '../utils/examFunctions'; 22 | import { parse } from 'path'; 23 | 24 | const defaultBannerImage = 25 | 'https://image.freepik.com/free-vector/online-exam-isometric-web-banner_33099-2305.jpg'; 26 | 27 | export const testing = catchAsync(async (req: Request, res: Response) => 28 | res.status(200).send('Server is up and running!') 29 | ); 30 | 31 | export const createTables = catchAsync(async (req: Request, res: Response) => { 32 | const db = getDb(); 33 | let query, result; 34 | 35 | //User table=================== 36 | query = 37 | 'create table User ( id varchar(50) not null, name varchar(50) , email varchar(50) ,bio longtext, dob datetime , address longtext,image longtext, password longtext ,institution longtext ,gender varchar(50),phoneNumber varchar(50) ,constraint user_pk primary key(id) )'; 38 | result = await db.execute(query); 39 | if (!result) throw new CustomError('User Table Not created !', 500); 40 | //============================= 41 | 42 | //Exam table================== 43 | query = 44 | 'create table `Exam` ( id varchar(50) , name varchar(50) , description longtext , image longtext ,userId varchar(50),tags json, questions json , startTime bigint , duration integer ,ongoing boolean default False,finished boolean default False ,isPrivate boolean default False ,numberOfParticipants integer, constraint exam_pk primary key(id) ,constraint fk1 foreign key (userId) references User(id) )'; 45 | result = await db.execute(query); 46 | if (!result) throw new CustomError('Exam Table not created !', 500); 47 | //============================ 48 | 49 | //Exam-Participants table======== 50 | query = 51 | 'create table `Exam-Participants` (id integer auto_increment ,examId varchar(50) , participantId varchar(50) ,answers json ,totalScore integer , finishTime bigint,virtual boolean default False,rank boolean default 0,constraint pk primary key(id) ,constraint fep1 foreign key (examId) references Exam(id) , constraint fep2 foreign key (participantId) references User(id))'; 52 | result = await db.execute(query); 53 | if (!result) 54 | throw new CustomError('Exam-Participants table not created!', 500); 55 | //================================ 56 | 57 | //private exam emails table==== 58 | query = 59 | 'create table `Private-Exam-Emails` (id integer auto_increment,examId varchar(50) , email varchar(50) , constraint pk primary key(id) , constraint fkx foreign key (examId) references Exam(id))'; 60 | result = await db.execute(query); 61 | if (!result) 62 | throw new CustomError('Private-Exam Emails table not created !', 500); 63 | //============================= 64 | res.status(200).send('All tables created !'); 65 | }); 66 | 67 | export const getExams = catchAsync(async (req: Request, res: Response) => { 68 | const db = getDb(); 69 | let query = 70 | 'select id,name,description,image,tags,startTime,duration,ongoing,isPrivate,(SELECT COUNT(ep.id) FROM `Exam-Participants` AS ep WHERE ep.examId = Exam.id) AS numberOfParticipants from `Exam`'; 71 | let [rows] = await db.execute(query); 72 | rows.map((exam: any) => { 73 | exam = parseExam(exam); 74 | }); 75 | res 76 | .status(200) 77 | .json(SuccessResponse(rows, 'These are upcoming and ongoing Exams !')); 78 | }); 79 | 80 | export const getExamDetails = catchAsync( 81 | async (req: Request, res: Response) => { 82 | const db = getDb(); 83 | const examId = req.params.id; 84 | let query = 85 | "select id,name,description,questions,image,tags,startTime,duration,ongoing,isPrivate,(SELECT COUNT(ep.id) FROM `Exam-Participants` AS ep WHERE ep.examId = Exam.id) AS numberOfParticipants, (SELECT JSON_OBJECT('id',u.id,'name',u.name,'image', u.image) FROM User AS u WHERE u.id = Exam.userId) AS user from Exam where id=? limit 1"; 86 | let [rows] = await db.execute(query, [examId]); 87 | if (rows.length != 1) throw new CustomError('Exam not found', 500); 88 | parseExam(rows[0]); 89 | let totalMarks = 0; 90 | rows[0].questions?.map( 91 | ( 92 | question: 93 | | mcqInterface 94 | | multipleOptionsInterface 95 | | fillInTheBlanksInterface 96 | ) => { 97 | totalMarks += question.marks; 98 | } 99 | ); 100 | rows[0].user = JSON.parse(rows[0].user); 101 | rows[0].totalMarks = totalMarks; 102 | delete rows[0].questions; 103 | res.status(200).json(SuccessResponse(rows[0], 'Exam Found !')); 104 | } 105 | ); 106 | 107 | export const createExam = catchAsync( 108 | async (req: CustomRequest, res: Response) => { 109 | const db = getDb(); 110 | const userId = req.user?.id; 111 | let data = req.body || {}; 112 | let query = 113 | 'insert into `Exam` (`id`,`name`,`description`,`image`,`userId`,`tags`,`questions`,`startTime`,`duration`,`isPrivate`) values(?,?,?,?,?,?,?,?,?,?)'; 114 | data.id = uuid(); 115 | const result1 = await db.execute(query, [ 116 | data.id, 117 | data.name, 118 | data.description || ' ', 119 | data.image?.trim() || defaultBannerImage, 120 | userId, 121 | JSON.stringify(data.tags), 122 | JSON.stringify(data.questions), 123 | data.startTime, 124 | data.duration, 125 | data.isPrivate, 126 | ]); 127 | if (!result1) throw new CustomError('Exam not created !', 500); 128 | if (data.isPrivate && data.allowedUsers.length > 0) { 129 | query = 'insert into `Private-Exam-Emails` (`examId`,`email`) values '; 130 | let queryArray: any = []; 131 | data.allowedUsers.map((allowedUserEmail: String) => { 132 | queryArray.push(`('` + data.id + `','` + allowedUserEmail + `')`); 133 | }); 134 | queryArray = queryArray.join(','); 135 | let result2 = await db.execute(query + queryArray); 136 | if (!result2) 137 | throw new CustomError( 138 | 'Failed to set Private User in Private-Exam-Email table', 139 | 500 140 | ); 141 | } 142 | scheduleExam(data.id, data.startTime, data.duration); 143 | return res.status(200).json(SuccessResponse({}, 'Exam Created!')); 144 | } 145 | ); 146 | 147 | export const editExam = catchAsync( 148 | async (req: CustomRequest, res: Response) => { 149 | const db = getDb(); 150 | // if(!req.user) throw new CustomError("User Error",404); 151 | const examId = req.params.id; 152 | const userId = req.user?.id; 153 | const { 154 | name, 155 | description, 156 | image, 157 | tags, 158 | questions, 159 | startTime, 160 | duration, 161 | isPrivate, 162 | allowedUsers, 163 | } = req.body; 164 | let getExamByUserId = 165 | 'select count(*) as examExists from `Exam` where `userId`=? and `id`=? limit 1'; 166 | let [rows] = await db.execute(getExamByUserId, [userId, examId]); //authorization to be added ! to e compared with req.user.id and req.body.userId 167 | console.log(rows); 168 | if (!rows[0].examExists) throw new CustomError('Exam not found !', 500); 169 | //console.log(rows[0]); 170 | let date = new Date(); 171 | date.setSeconds(date.getSeconds() + 60); 172 | let updateExam = 173 | 'update `Exam` set `name`=? ,`description`=?,`image`=?,`tags` = ?, `questions`=? , `startTime`=? , `duration`=? , `isPrivate`=? where `id`=?'; 174 | let result1 = await db.execute(updateExam, [ 175 | name, 176 | description || '', 177 | image.trim() || defaultBannerImage, 178 | JSON.stringify(tags), 179 | JSON.stringify(questions), 180 | date, 181 | duration, 182 | isPrivate, 183 | examId, 184 | ]); 185 | if (!result1) throw new CustomError('Exam not updated !', 500); 186 | 187 | let deleteByEmail = 'delete from `Private-Exam-Emails` where `examId`=?'; 188 | const result2 = await db.execute(deleteByEmail, [examId]); 189 | if (!result2) 190 | throw new CustomError('Delete from Private-Exam-Emails failed !', 500); 191 | 192 | if (isPrivate && allowedUsers.length > 0) { 193 | let query = 194 | 'insert into `Private-Exam-Emails` (`examId`,`email`) values '; 195 | let queryArray: any = []; 196 | allowedUsers.map((allowedUserEmail: String) => { 197 | queryArray.push(`('` + examId + `','` + allowedUserEmail + `')`); 198 | }); 199 | queryArray = queryArray.join(','); 200 | let result3 = await db.execute(query + queryArray); 201 | if (!result3) 202 | throw new CustomError( 203 | 'Failed to update Private User in Private-Exam-Email table', 204 | 500 205 | ); 206 | } 207 | // destroyScheduler(examId + 'start'); 208 | // destroyScheduler(examId + 'end'); 209 | // scheduleExam(examId, date, duration); 210 | return res.status(200).json(SuccessResponse({}, 'Exam Updated!')); 211 | } 212 | ); 213 | 214 | export const registerInExam = catchAsync( 215 | async (req: CustomRequest, res: Response) => { 216 | const db = getDb(); 217 | let query; 218 | const { examId } = req.body; 219 | const userId = req.user?.id; 220 | const email = req.user?.email; 221 | //check if the ids are valid then insert in exam-participants table 222 | query = 223 | 'select isPrivate,userId as creatorId,count(*) as examExists,finished,startTime,duration from `Exam` where `id`=? limit 1'; 224 | let [rows] = await db.execute(query, [examId]); 225 | // console.log(rows); 226 | let { isPrivate, creatorId, examExists, finished, startTime, duration } = 227 | rows[0]; 228 | if (!examExists) throw new CustomError('Exam not Found', 500); 229 | if (creatorId === userId) 230 | throw new CustomError('Creator cannot give Exam !', 500); 231 | const currTime = new Date().getTime(); 232 | if (currTime >= startTime && currTime <= startTime + duration + 120000) 233 | throw new CustomError( 234 | 'Not a valid time to register! Let the evaluation finish!', 235 | 500 236 | ); 237 | if (isPrivate) { 238 | query = 239 | 'select count(*) as userAllowed from `Private-Exam-Emails` where `email`=? limit 1'; 240 | let [rows] = await db.execute(query, [email]); 241 | //console.log(rows); 242 | if (!rows[0].userAllowed) 243 | throw new CustomError('Not allowed to enter !', 404); 244 | } 245 | 246 | query = 247 | 'select count(*) as alreadyRegistered from `Exam-Participants` where `participantId`=? and `examId`=? limit 1'; 248 | [rows] = await db.execute(query, [userId, examId]); 249 | if (rows[0].alreadyRegistered) 250 | throw new CustomError('User already Registered !', 500); 251 | 252 | query = 253 | 'insert into `Exam-Participants` (`examId`,`participantId`,`virtual`) values(?,?,?)'; 254 | let result = await db.execute(query, [examId, userId, finished]); 255 | if (!result) throw new CustomError('Registering failed !', 500); 256 | res.status(200).send('Successfully Registered !'); 257 | } 258 | ); 259 | 260 | export const startExam = catchAsync( 261 | async (req: CustomRequest, res: Response) => { 262 | const db = getDb(); 263 | const examId = req.params.id; 264 | const userId = req.user?.id; 265 | let query = 'select * from `Exam` where `id`=? limit 1'; 266 | let [examRows] = await db.execute(query, [examId]); 267 | if (examRows.length != 1) throw new CustomError('Exam not found !', 500); 268 | examRows[0] = parseExam(examRows[0]); 269 | shuffleExam(examRows[0]); 270 | removeCorrectOptions(examRows[0]); 271 | query = 272 | 'select count(*) as userRegistered,virtual,answers from `Exam-Participants` where `participantId`=? and `examId`=? limit 1'; 273 | let [rows] = await db.execute(query, [userId, examId]); 274 | console.log(rows); 275 | if (!rows[0].userRegistered) 276 | throw new CustomError('User not yet Registered !', 500); 277 | if (rows[0].answers) 278 | throw new CustomError('Answers already Submitted!', 500); 279 | if (rows[0].virtual) 280 | return res 281 | .status(200) 282 | .json(SuccessResponse(examRows[0], 'Virtual Exam Started !')); 283 | if (!examRows[0].ongoing) 284 | throw new CustomError('Exam has not started yet !', 500); 285 | 286 | //console.log(shuffleExam(examRows[0])); 287 | res.status(200).json(SuccessResponse(examRows[0], 'Exam Started !')); 288 | } 289 | ); 290 | 291 | export const submitExam = catchAsync( 292 | async (req: CustomRequest, res: Response) => { 293 | const db = getDb(); 294 | let query; 295 | const { answers, finishTime, examId } = req.body; 296 | const participantId = req.user?.id; 297 | if (!answers || !finishTime || !participantId || !examId) 298 | throw new CustomError('Fields are missing !', 500); 299 | query = 300 | 'select `startTime`+`duration` as `endTime` from `Exam` where `id`=? limit 1'; 301 | let [rows] = await db.execute(query, [examId]); 302 | if (rows.length != 1) throw new CustomError('Exam not found!', 500); 303 | if (finishTime > rows[0].endTime + 15000) 304 | //extra 15secs 305 | throw new CustomError('Not a valid time to submit! Exam Finished!', 500); 306 | 307 | query = 308 | 'update `Exam-Participants` set `answers`=? , `finishTime`=? where `participantId`=? and `examId`=?'; 309 | [rows] = await db.execute(query, [ 310 | JSON.stringify(answers), 311 | finishTime, 312 | participantId, 313 | examId, 314 | ]); 315 | if (!rows.affectedRows) 316 | throw new CustomError('Error occured while submitting! :(', 500); 317 | res 318 | .status(200) 319 | .json(SuccessResponse(rows[0], 'Exam Submitted Successfully!')); 320 | } 321 | ); 322 | 323 | export const forceEvaluateExam = catchAsync( 324 | async (req: Request, res: Response) => { 325 | const id: any = req.query.id; 326 | await evaluateExam(id); 327 | res.status(200).send('Evaluation Completed!'); 328 | } 329 | ); 330 | 331 | export const getTags = catchAsync(async (req: Request, res: Response) => { 332 | const tags: String[] = [ 333 | 'Maths', 334 | 'Physics', 335 | 'Chemistry', 336 | 'Exam', 337 | 'Contest', 338 | 'Computer Science', 339 | 'Competitive Programming', 340 | 'Dynamic Programming', 341 | 'Graph Theory', 342 | 'Literature', 343 | ]; 344 | res.status(200).json(SuccessResponse(tags, 'The tags are :')); 345 | }); 346 | 347 | export const getQuestionTypes = catchAsync( 348 | async (req: Request, res: Response) => { 349 | const questionTypes = [ 350 | { label: 'MCQ Single Option', value: 'mcq' }, 351 | { label: 'Fill in the blanks', value: 'fillInTheBlanks' }, 352 | { label: 'MCQ Multiple Options', value: 'multipleOptions' }, 353 | ]; 354 | res 355 | .status(200) 356 | .json(SuccessResponse(questionTypes, 'The question types are :')); 357 | } 358 | ); 359 | 360 | export const examRegisterStatus = catchAsync( 361 | async (req: CustomRequest, res: Response) => { 362 | const db = getDb(); 363 | const userId = req.user?.id; 364 | const examId = req.query.examId; 365 | let query = 366 | 'select count(*) registered from `Exam-Participants` where `examId`=? and `participantId`=?'; 367 | const [rows] = await db.execute(query, [examId, userId]); 368 | res 369 | .status(200) 370 | .json( 371 | SuccessResponse( 372 | { registered: rows[0].registered }, 373 | 'Register status of the user is:' 374 | ) 375 | ); 376 | } 377 | ); 378 | 379 | //this route uses customized question object for algorithm purposes! 380 | export const getExamSolution = catchAsync( 381 | async (req: CustomRequest, res: Response) => { 382 | const db = getDb(); 383 | const examId = req.query.examId; 384 | const userId = req.user?.id; 385 | let examQuestions; 386 | let query = 'select questions,finished from `Exam` where `id`=? limit 1'; 387 | let [rows] = await db.execute(query, [examId]); 388 | if (rows.length != 1) throw new CustomError('Exam not found!', 500); 389 | if (!rows[0].finished) 390 | res.status(200).json(SuccessResponse(null, 'Exam not yet finished!')); 391 | examQuestions = JSON.parse(rows[0].questions); 392 | let questionsObj: any = {}; 393 | examQuestions.map((question: any) => { 394 | questionsObj[question.id] = question; 395 | }); 396 | examQuestions = questionsObj; 397 | query = 398 | 'select answers from `Exam-Participants` where `participantId`=? and `examId`=? limit 1'; 399 | [rows] = await db.execute(query, [userId, examId]); 400 | if (rows.length != 1) 401 | res 402 | .status(200) 403 | .json( 404 | SuccessResponse( 405 | null, 406 | 'Error while finding answers!, maybe user not registered!' 407 | ) 408 | ); 409 | if (!rows[0].answers) 410 | res.status(200).json(SuccessResponse(null, 'Answers null!')); 411 | let answers = JSON.parse(rows[0].answers); 412 | let examWithUserAns = getExamWithUserAns(examQuestions, answers); 413 | res 414 | .status(200) 415 | .json(SuccessResponse(examWithUserAns, 'Your solution is :')); 416 | } 417 | ); 418 | 419 | export const getExamScores = catchAsync( 420 | async (req: CustomRequest, res: Response) => { 421 | const db = getDb(); 422 | let query, rows; 423 | const userId = req.user?.id; 424 | const examId = req.query.examId; 425 | query = 'select `finished` from `Exam` where `id`=? limit 1'; 426 | [rows] = await db.execute(query, [examId]); 427 | if (rows.length != 1) throw new CustomError('Exam not found!', 500); 428 | if (!rows[0].finished) throw new CustomError('Exam not finished !', 500); 429 | query = 430 | "select `participantId`, `rank`, `finishTime`, `totalScore`, (SELECT JSON_ARRAYAGG(JSON_OBJECT('participantId', ep.participantId, 'totalScore', ep.totalScore, 'rank', ep.rank, 'finishTime', ep.finishTime, 'name', u.name)) FROM `Exam-Participants` AS ep, `User` AS u WHERE ep.participantId = u.id AND ep.examId = e.examId ORDER BY ep.rank LIMIT 5) AS `topPerformers` from `Exam-Participants` AS e where `participantId`=? and `examId`=? LIMIT 1"; 431 | [rows] = await db.execute(query, [userId, examId]); 432 | if (rows.length != 1) throw new CustomError('Data not found!', 500); 433 | rows[0].topPerformers = JSON.parse(rows[0].topPerformers); 434 | res.status(200).json(SuccessResponse(rows[0], 'The data is : ')); 435 | } 436 | ); 437 | -------------------------------------------------------------------------------- /dist/controllers/examController.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __importDefault = (this && this.__importDefault) || function (mod) { 12 | return (mod && mod.__esModule) ? mod : { "default": mod }; 13 | }; 14 | Object.defineProperty(exports, "__esModule", { value: true }); 15 | exports.getExamScores = exports.getExamSolution = exports.examRegisterStatus = exports.getQuestionTypes = exports.getTags = exports.forceEvaluateExam = exports.submitExam = exports.startExam = exports.registerInExam = exports.editExam = exports.createExam = exports.getExamDetails = exports.getExams = exports.createTables = exports.testing = void 0; 16 | const catchAsync_1 = __importDefault(require("../utils/catchAsync")); 17 | const response_handler_1 = require("../utils/response-handler"); 18 | const custom_error_1 = __importDefault(require("../errors/custom-error")); 19 | const dbConnection_1 = require("../database/dbConnection"); 20 | const uuid_1 = require("uuid"); 21 | const examFunctions_1 = require("../utils/examFunctions"); 22 | const defaultBannerImage = 'https://image.freepik.com/free-vector/online-exam-isometric-web-banner_33099-2305.jpg'; 23 | exports.testing = (0, catchAsync_1.default)((req, res) => __awaiter(void 0, void 0, void 0, function* () { return res.status(200).send('Server is up and running!'); })); 24 | exports.createTables = (0, catchAsync_1.default)((req, res) => __awaiter(void 0, void 0, void 0, function* () { 25 | const db = (0, dbConnection_1.getDb)(); 26 | let query, result; 27 | //User table=================== 28 | query = 29 | 'create table User ( id varchar(50) not null, name varchar(50) , email varchar(50) ,bio longtext, dob datetime , address longtext,image longtext, password longtext ,institution longtext ,gender varchar(50),phoneNumber varchar(50) ,constraint user_pk primary key(id) )'; 30 | result = yield db.execute(query); 31 | if (!result) 32 | throw new custom_error_1.default('User Table Not created !', 500); 33 | //============================= 34 | //Exam table================== 35 | query = 36 | 'create table `Exam` ( id varchar(50) , name varchar(50) , description longtext , image longtext ,userId varchar(50),tags json, questions json , startTime bigint , duration integer ,ongoing boolean default False,finished boolean default False ,isPrivate boolean default False ,numberOfParticipants integer, constraint exam_pk primary key(id) ,constraint fk1 foreign key (userId) references User(id) )'; 37 | result = yield db.execute(query); 38 | if (!result) 39 | throw new custom_error_1.default('Exam Table not created !', 500); 40 | //============================ 41 | //Exam-Participants table======== 42 | query = 43 | 'create table `Exam-Participants` (id integer auto_increment ,examId varchar(50) , participantId varchar(50) ,answers json ,totalScore integer , finishTime bigint,virtual boolean default False,rank boolean default 0,constraint pk primary key(id) ,constraint fep1 foreign key (examId) references Exam(id) , constraint fep2 foreign key (participantId) references User(id))'; 44 | result = yield db.execute(query); 45 | if (!result) 46 | throw new custom_error_1.default('Exam-Participants table not created!', 500); 47 | //================================ 48 | //private exam emails table==== 49 | query = 50 | 'create table `Private-Exam-Emails` (id integer auto_increment,examId varchar(50) , email varchar(50) , constraint pk primary key(id) , constraint fkx foreign key (examId) references Exam(id))'; 51 | result = yield db.execute(query); 52 | if (!result) 53 | throw new custom_error_1.default('Private-Exam Emails table not created !', 500); 54 | //============================= 55 | res.status(200).send('All tables created !'); 56 | })); 57 | exports.getExams = (0, catchAsync_1.default)((req, res) => __awaiter(void 0, void 0, void 0, function* () { 58 | const db = (0, dbConnection_1.getDb)(); 59 | let query = 'select id,name,description,image,tags,startTime,duration,ongoing,isPrivate,(SELECT COUNT(ep.id) FROM `Exam-Participants` AS ep WHERE ep.examId = Exam.id) AS numberOfParticipants from `Exam`'; 60 | let [rows] = yield db.execute(query); 61 | rows.map((exam) => { 62 | exam = (0, examFunctions_1.parseExam)(exam); 63 | }); 64 | res 65 | .status(200) 66 | .json((0, response_handler_1.SuccessResponse)(rows, 'These are upcoming and ongoing Exams !')); 67 | })); 68 | exports.getExamDetails = (0, catchAsync_1.default)((req, res) => __awaiter(void 0, void 0, void 0, function* () { 69 | var _a; 70 | const db = (0, dbConnection_1.getDb)(); 71 | const examId = req.params.id; 72 | let query = "select id,name,description,questions,image,tags,startTime,duration,ongoing,isPrivate,(SELECT COUNT(ep.id) FROM `Exam-Participants` AS ep WHERE ep.examId = Exam.id) AS numberOfParticipants, (SELECT JSON_OBJECT('id',u.id,'name',u.name,'image', u.image) FROM User AS u WHERE u.id = Exam.userId) AS user from Exam where id=? limit 1"; 73 | let [rows] = yield db.execute(query, [examId]); 74 | if (rows.length != 1) 75 | throw new custom_error_1.default('Exam not found', 500); 76 | (0, examFunctions_1.parseExam)(rows[0]); 77 | let totalMarks = 0; 78 | (_a = rows[0].questions) === null || _a === void 0 ? void 0 : _a.map((question) => { 79 | totalMarks += question.marks; 80 | }); 81 | rows[0].user = JSON.parse(rows[0].user); 82 | rows[0].totalMarks = totalMarks; 83 | delete rows[0].questions; 84 | res.status(200).json((0, response_handler_1.SuccessResponse)(rows[0], 'Exam Found !')); 85 | })); 86 | exports.createExam = (0, catchAsync_1.default)((req, res) => __awaiter(void 0, void 0, void 0, function* () { 87 | var _b, _c; 88 | const db = (0, dbConnection_1.getDb)(); 89 | const userId = (_b = req.user) === null || _b === void 0 ? void 0 : _b.id; 90 | let data = req.body || {}; 91 | let query = 'insert into `Exam` (`id`,`name`,`description`,`image`,`userId`,`tags`,`questions`,`startTime`,`duration`,`isPrivate`) values(?,?,?,?,?,?,?,?,?,?)'; 92 | data.id = (0, uuid_1.v4)(); 93 | const result1 = yield db.execute(query, [ 94 | data.id, 95 | data.name, 96 | data.description || ' ', 97 | ((_c = data.image) === null || _c === void 0 ? void 0 : _c.trim()) || defaultBannerImage, 98 | userId, 99 | JSON.stringify(data.tags), 100 | JSON.stringify(data.questions), 101 | data.startTime, 102 | data.duration, 103 | data.isPrivate, 104 | ]); 105 | if (!result1) 106 | throw new custom_error_1.default('Exam not created !', 500); 107 | if (data.isPrivate && data.allowedUsers.length > 0) { 108 | query = 'insert into `Private-Exam-Emails` (`examId`,`email`) values '; 109 | let queryArray = []; 110 | data.allowedUsers.map((allowedUserEmail) => { 111 | queryArray.push(`('` + data.id + `','` + allowedUserEmail + `')`); 112 | }); 113 | queryArray = queryArray.join(','); 114 | let result2 = yield db.execute(query + queryArray); 115 | if (!result2) 116 | throw new custom_error_1.default('Failed to set Private User in Private-Exam-Email table', 500); 117 | } 118 | (0, examFunctions_1.scheduleExam)(data.id, data.startTime, data.duration); 119 | return res.status(200).json((0, response_handler_1.SuccessResponse)({}, 'Exam Created!')); 120 | })); 121 | exports.editExam = (0, catchAsync_1.default)((req, res) => __awaiter(void 0, void 0, void 0, function* () { 122 | var _d; 123 | const db = (0, dbConnection_1.getDb)(); 124 | // if(!req.user) throw new CustomError("User Error",404); 125 | const examId = req.params.id; 126 | const userId = (_d = req.user) === null || _d === void 0 ? void 0 : _d.id; 127 | const { name, description, image, tags, questions, startTime, duration, isPrivate, allowedUsers, } = req.body; 128 | let getExamByUserId = 'select count(*) as examExists from `Exam` where `userId`=? and `id`=? limit 1'; 129 | let [rows] = yield db.execute(getExamByUserId, [userId, examId]); //authorization to be added ! to e compared with req.user.id and req.body.userId 130 | console.log(rows); 131 | if (!rows[0].examExists) 132 | throw new custom_error_1.default('Exam not found !', 500); 133 | //console.log(rows[0]); 134 | let date = new Date(); 135 | date.setSeconds(date.getSeconds() + 60); 136 | let updateExam = 'update `Exam` set `name`=? ,`description`=?,`image`=?,`tags` = ?, `questions`=? , `startTime`=? , `duration`=? , `isPrivate`=? where `id`=?'; 137 | let result1 = yield db.execute(updateExam, [ 138 | name, 139 | description || '', 140 | image.trim() || defaultBannerImage, 141 | JSON.stringify(tags), 142 | JSON.stringify(questions), 143 | date, 144 | duration, 145 | isPrivate, 146 | examId, 147 | ]); 148 | if (!result1) 149 | throw new custom_error_1.default('Exam not updated !', 500); 150 | let deleteByEmail = 'delete from `Private-Exam-Emails` where `examId`=?'; 151 | const result2 = yield db.execute(deleteByEmail, [examId]); 152 | if (!result2) 153 | throw new custom_error_1.default('Delete from Private-Exam-Emails failed !', 500); 154 | if (isPrivate && allowedUsers.length > 0) { 155 | let query = 'insert into `Private-Exam-Emails` (`examId`,`email`) values '; 156 | let queryArray = []; 157 | allowedUsers.map((allowedUserEmail) => { 158 | queryArray.push(`('` + examId + `','` + allowedUserEmail + `')`); 159 | }); 160 | queryArray = queryArray.join(','); 161 | let result3 = yield db.execute(query + queryArray); 162 | if (!result3) 163 | throw new custom_error_1.default('Failed to update Private User in Private-Exam-Email table', 500); 164 | } 165 | // destroyScheduler(examId + 'start'); 166 | // destroyScheduler(examId + 'end'); 167 | // scheduleExam(examId, date, duration); 168 | return res.status(200).json((0, response_handler_1.SuccessResponse)({}, 'Exam Updated!')); 169 | })); 170 | exports.registerInExam = (0, catchAsync_1.default)((req, res) => __awaiter(void 0, void 0, void 0, function* () { 171 | var _e, _f; 172 | const db = (0, dbConnection_1.getDb)(); 173 | let query; 174 | const { examId } = req.body; 175 | const userId = (_e = req.user) === null || _e === void 0 ? void 0 : _e.id; 176 | const email = (_f = req.user) === null || _f === void 0 ? void 0 : _f.email; 177 | //check if the ids are valid then insert in exam-participants table 178 | query = 179 | 'select isPrivate,userId as creatorId,count(*) as examExists,finished,startTime,duration from `Exam` where `id`=? limit 1'; 180 | let [rows] = yield db.execute(query, [examId]); 181 | // console.log(rows); 182 | let { isPrivate, creatorId, examExists, finished, startTime, duration } = rows[0]; 183 | if (!examExists) 184 | throw new custom_error_1.default('Exam not Found', 500); 185 | if (creatorId === userId) 186 | throw new custom_error_1.default('Creator cannot give Exam !', 500); 187 | const currTime = new Date().getTime(); 188 | if (currTime >= startTime && currTime <= startTime + duration + 120000) 189 | throw new custom_error_1.default('Not a valid time to register! Let the evaluation finish!', 500); 190 | if (isPrivate) { 191 | query = 192 | 'select count(*) as userAllowed from `Private-Exam-Emails` where `email`=? limit 1'; 193 | let [rows] = yield db.execute(query, [email]); 194 | //console.log(rows); 195 | if (!rows[0].userAllowed) 196 | throw new custom_error_1.default('Not allowed to enter !', 404); 197 | } 198 | query = 199 | 'select count(*) as alreadyRegistered from `Exam-Participants` where `participantId`=? and `examId`=? limit 1'; 200 | [rows] = yield db.execute(query, [userId, examId]); 201 | if (rows[0].alreadyRegistered) 202 | throw new custom_error_1.default('User already Registered !', 500); 203 | query = 204 | 'insert into `Exam-Participants` (`examId`,`participantId`,`virtual`) values(?,?,?)'; 205 | let result = yield db.execute(query, [examId, userId, finished]); 206 | if (!result) 207 | throw new custom_error_1.default('Registering failed !', 500); 208 | res.status(200).send('Successfully Registered !'); 209 | })); 210 | exports.startExam = (0, catchAsync_1.default)((req, res) => __awaiter(void 0, void 0, void 0, function* () { 211 | var _g; 212 | const db = (0, dbConnection_1.getDb)(); 213 | const examId = req.params.id; 214 | const userId = (_g = req.user) === null || _g === void 0 ? void 0 : _g.id; 215 | let query = 'select * from `Exam` where `id`=? limit 1'; 216 | let [examRows] = yield db.execute(query, [examId]); 217 | if (examRows.length != 1) 218 | throw new custom_error_1.default('Exam not found !', 500); 219 | examRows[0] = (0, examFunctions_1.parseExam)(examRows[0]); 220 | (0, examFunctions_1.shuffleExam)(examRows[0]); 221 | (0, examFunctions_1.removeCorrectOptions)(examRows[0]); 222 | query = 223 | 'select count(*) as userRegistered,virtual,answers from `Exam-Participants` where `participantId`=? and `examId`=? limit 1'; 224 | let [rows] = yield db.execute(query, [userId, examId]); 225 | console.log(rows); 226 | if (!rows[0].userRegistered) 227 | throw new custom_error_1.default('User not yet Registered !', 500); 228 | if (rows[0].answers) 229 | throw new custom_error_1.default('Answers already Submitted!', 500); 230 | if (rows[0].virtual) 231 | return res 232 | .status(200) 233 | .json((0, response_handler_1.SuccessResponse)(examRows[0], 'Virtual Exam Started !')); 234 | if (!examRows[0].ongoing) 235 | throw new custom_error_1.default('Exam has not started yet !', 500); 236 | //console.log(shuffleExam(examRows[0])); 237 | res.status(200).json((0, response_handler_1.SuccessResponse)(examRows[0], 'Exam Started !')); 238 | })); 239 | exports.submitExam = (0, catchAsync_1.default)((req, res) => __awaiter(void 0, void 0, void 0, function* () { 240 | var _h; 241 | const db = (0, dbConnection_1.getDb)(); 242 | let query; 243 | const { answers, finishTime, examId } = req.body; 244 | const participantId = (_h = req.user) === null || _h === void 0 ? void 0 : _h.id; 245 | if (!answers || !finishTime || !participantId || !examId) 246 | throw new custom_error_1.default('Fields are missing !', 500); 247 | query = 248 | 'select `startTime`+`duration` as `endTime` from `Exam` where `id`=? limit 1'; 249 | let [rows] = yield db.execute(query, [examId]); 250 | if (rows.length != 1) 251 | throw new custom_error_1.default('Exam not found!', 500); 252 | if (finishTime > rows[0].endTime + 15000) 253 | //extra 15secs 254 | throw new custom_error_1.default('Not a valid time to submit! Exam Finished!', 500); 255 | query = 256 | 'update `Exam-Participants` set `answers`=? , `finishTime`=? where `participantId`=? and `examId`=?'; 257 | [rows] = yield db.execute(query, [ 258 | JSON.stringify(answers), 259 | finishTime, 260 | participantId, 261 | examId, 262 | ]); 263 | if (!rows.affectedRows) 264 | throw new custom_error_1.default('Error occured while submitting! :(', 500); 265 | res 266 | .status(200) 267 | .json((0, response_handler_1.SuccessResponse)(rows[0], 'Exam Submitted Successfully!')); 268 | })); 269 | exports.forceEvaluateExam = (0, catchAsync_1.default)((req, res) => __awaiter(void 0, void 0, void 0, function* () { 270 | const id = req.query.id; 271 | yield (0, examFunctions_1.evaluateExam)(id); 272 | res.status(200).send('Evaluation Completed!'); 273 | })); 274 | exports.getTags = (0, catchAsync_1.default)((req, res) => __awaiter(void 0, void 0, void 0, function* () { 275 | const tags = [ 276 | 'Maths', 277 | 'Physics', 278 | 'Chemistry', 279 | 'Exam', 280 | 'Contest', 281 | 'Computer Science', 282 | 'Competitive Programming', 283 | 'Dynamic Programming', 284 | 'Graph Theory', 285 | 'Literature', 286 | ]; 287 | res.status(200).json((0, response_handler_1.SuccessResponse)(tags, 'The tags are :')); 288 | })); 289 | exports.getQuestionTypes = (0, catchAsync_1.default)((req, res) => __awaiter(void 0, void 0, void 0, function* () { 290 | const questionTypes = [ 291 | { label: 'MCQ Single Option', value: 'mcq' }, 292 | { label: 'Fill in the blanks', value: 'fillInTheBlanks' }, 293 | { label: 'MCQ Multiple Options', value: 'multipleOptions' }, 294 | ]; 295 | res 296 | .status(200) 297 | .json((0, response_handler_1.SuccessResponse)(questionTypes, 'The question types are :')); 298 | })); 299 | exports.examRegisterStatus = (0, catchAsync_1.default)((req, res) => __awaiter(void 0, void 0, void 0, function* () { 300 | var _j; 301 | const db = (0, dbConnection_1.getDb)(); 302 | const userId = (_j = req.user) === null || _j === void 0 ? void 0 : _j.id; 303 | const examId = req.query.examId; 304 | let query = 'select count(*) registered from `Exam-Participants` where `examId`=? and `participantId`=?'; 305 | const [rows] = yield db.execute(query, [examId, userId]); 306 | res 307 | .status(200) 308 | .json((0, response_handler_1.SuccessResponse)({ registered: rows[0].registered }, 'Register status of the user is:')); 309 | })); 310 | //this route uses customized question object for algorithm purposes! 311 | exports.getExamSolution = (0, catchAsync_1.default)((req, res) => __awaiter(void 0, void 0, void 0, function* () { 312 | var _k; 313 | const db = (0, dbConnection_1.getDb)(); 314 | const examId = req.query.examId; 315 | const userId = (_k = req.user) === null || _k === void 0 ? void 0 : _k.id; 316 | let examQuestions; 317 | let query = 'select questions,finished from `Exam` where `id`=? limit 1'; 318 | let [rows] = yield db.execute(query, [examId]); 319 | if (rows.length != 1) 320 | throw new custom_error_1.default('Exam not found!', 500); 321 | if (!rows[0].finished) 322 | res.status(200).json((0, response_handler_1.SuccessResponse)(null, 'Exam not yet finished!')); 323 | examQuestions = JSON.parse(rows[0].questions); 324 | let questionsObj = {}; 325 | examQuestions.map((question) => { 326 | questionsObj[question.id] = question; 327 | }); 328 | examQuestions = questionsObj; 329 | query = 330 | 'select answers from `Exam-Participants` where `participantId`=? and `examId`=? limit 1'; 331 | [rows] = yield db.execute(query, [userId, examId]); 332 | if (rows.length != 1) 333 | res 334 | .status(200) 335 | .json((0, response_handler_1.SuccessResponse)(null, 'Error while finding answers!, maybe user not registered!')); 336 | if (!rows[0].answers) 337 | res.status(200).json((0, response_handler_1.SuccessResponse)(null, 'Answers null!')); 338 | let answers = JSON.parse(rows[0].answers); 339 | let examWithUserAns = (0, examFunctions_1.getExamWithUserAns)(examQuestions, answers); 340 | res 341 | .status(200) 342 | .json((0, response_handler_1.SuccessResponse)(examWithUserAns, 'Your solution is :')); 343 | })); 344 | exports.getExamScores = (0, catchAsync_1.default)((req, res) => __awaiter(void 0, void 0, void 0, function* () { 345 | var _l; 346 | const db = (0, dbConnection_1.getDb)(); 347 | let query, rows; 348 | const userId = (_l = req.user) === null || _l === void 0 ? void 0 : _l.id; 349 | const examId = req.query.examId; 350 | query = 'select `finished` from `Exam` where `id`=? limit 1'; 351 | [rows] = yield db.execute(query, [examId]); 352 | if (rows.length != 1) 353 | throw new custom_error_1.default('Exam not found!', 500); 354 | if (!rows[0].finished) 355 | throw new custom_error_1.default('Exam not finished !', 500); 356 | query = 357 | "select `participantId`, `rank`, `finishTime`, `totalScore`, (SELECT JSON_ARRAYAGG(JSON_OBJECT('participantId', ep.participantId, 'totalScore', ep.totalScore, 'rank', ep.rank, 'finishTime', ep.finishTime, 'name', u.name)) FROM `Exam-Participants` AS ep, `User` AS u WHERE ep.participantId = u.id AND ep.examId = e.examId ORDER BY ep.rank LIMIT 5) AS `topPerformers` from `Exam-Participants` AS e where `participantId`=? and `examId`=? LIMIT 1"; 358 | [rows] = yield db.execute(query, [userId, examId]); 359 | if (rows.length != 1) 360 | throw new custom_error_1.default('Data not found!', 500); 361 | rows[0].topPerformers = JSON.parse(rows[0].topPerformers); 362 | res.status(200).json((0, response_handler_1.SuccessResponse)(rows[0], 'The data is : ')); 363 | })); 364 | --------------------------------------------------------------------------------