├── .gitignore ├── src ├── .vscode │ └── settings.json ├── config │ ├── jwt.config.js │ ├── constants.config.js │ └── db.config.js ├── routes │ ├── authRouter.js │ ├── index.js │ ├── activityRouter.js │ ├── userRouter.js │ └── courseRouter.js ├── auth │ └── auth.js ├── models │ ├── userCoursesModel.js │ ├── answerModel.js │ ├── questionModel.js │ ├── courseModel.js │ ├── activityModel.js │ ├── index.js │ └── userModel.js ├── package.json ├── index.js └── controllers │ ├── activityController.js │ ├── userController.js │ └── courseController.js ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | **/package-lock.json 3 | .vscode/settings.json 4 | src/config/db.config.js 5 | -------------------------------------------------------------------------------- /src/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "conventionalCommits.scopes": [ 3 | "validate try catch for all database qureies" 4 | ] 5 | } -------------------------------------------------------------------------------- /src/config/jwt.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | SECRET_KEY: 'Mahboub & Adel', 3 | SECRET_KEY_RESET_PASSWORD: "enta falaaa7", 4 | JWT_EXPIRES_IN: '30d' 5 | } -------------------------------------------------------------------------------- /src/config/constants.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | userType: { 3 | LEARNER: "learner", 4 | INSTRUCTOR: "instructor", 5 | ADMIN: "admin" 6 | }, 7 | env: { 8 | CLIENT_URL: "http://localhost:8080" 9 | } 10 | } -------------------------------------------------------------------------------- /src/routes/authRouter.js: -------------------------------------------------------------------------------- 1 | const userController = require("../controllers/userController.js"); 2 | 3 | const router = require("express").Router(); 4 | 5 | router.post("/login", userController.login); 6 | router.post("/signup", userController.signup); 7 | router.post("/forgot", userController.forgotPassword); 8 | router.post("/reset", userController.resetPassword); 9 | 10 | module.exports = router; 11 | 12 | 13 | /** 14 | * localhost:4000/api/signup 15 | */ -------------------------------------------------------------------------------- /src/routes/index.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | 3 | const authRouter = require("./authRouter"); 4 | const userRouter = require("./userRouter"); 5 | const courseRouter = require("./courseRouter"); 6 | const activityRouter = require("./activityRouter"); 7 | 8 | const router = express.Router(); 9 | 10 | router.use("/", authRouter); 11 | router.use("/users", userRouter); 12 | router.use("/courses", courseRouter); 13 | router.use("/activities", activityRouter); 14 | 15 | module.exports = router; 16 | -------------------------------------------------------------------------------- /src/routes/activityRouter.js: -------------------------------------------------------------------------------- 1 | const auth = require("../auth/auth"); 2 | const router = require("express").Router(); 3 | 4 | const activityController = require("../controllers/activityController.js"); 5 | 6 | router 7 | .route("/:courseId") 8 | .get(auth.verifyToken, activityController.getActivities) 9 | .post(auth.verifyToken, activityController.addActivity); 10 | 11 | router 12 | .route("/:id") 13 | .delete(auth.verifyToken, activityController.deleteActivity); 14 | 15 | module.exports = router; 16 | -------------------------------------------------------------------------------- /src/auth/auth.js: -------------------------------------------------------------------------------- 1 | const httpStatus = require("http-status"); 2 | 3 | function verifyToken(req, res, next) { 4 | const bearerHeader = req.headers["authorization"]; 5 | 6 | if (typeof bearerHeader !== "undefined") { 7 | const bearer = bearerHeader.split(" "); 8 | const bearerToken = bearer[1]; 9 | 10 | req.token = bearerToken; 11 | next(); 12 | } else { 13 | return res.status(httpStatus.FORBIDDEN).send({ 14 | message: "This operation is forbibden for you hehe hahhaha !", 15 | }); 16 | } 17 | } 18 | 19 | module.exports = { 20 | verifyToken, 21 | }; 22 | -------------------------------------------------------------------------------- /src/models/userCoursesModel.js: -------------------------------------------------------------------------------- 1 | const user = require("./userModel"); 2 | const course = require("./courseModel"); 3 | 4 | module.exports = (sequelize, DataTypes) => { 5 | return sequelize.define("user_courses", { 6 | userId: { 7 | type: DataTypes.INTEGER, 8 | references: { 9 | model: user, 10 | key: "_id", 11 | }, 12 | }, 13 | courseId: { 14 | type: DataTypes.INTEGER, 15 | references: { 16 | model: course, 17 | key: "_id", 18 | }, 19 | }, 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /src/config/db.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | HOST: "localhost", 3 | USER: "root", 4 | PASSWORD: "root", 5 | DB: "lms", 6 | dialect: "mysql", 7 | 8 | pool: { 9 | max: 5, 10 | min: 0, 11 | acquire: 30000, 12 | idle: 10000, 13 | }, 14 | }; 15 | 16 | // module.exports = { 17 | // HOST: "sql11.freemysqlhosting.net", 18 | // USER: "sql11461123", 19 | // PASSWORD: "HuuCN9953R", 20 | // DB: "sql11461123", 21 | // dialect: "mysql", 22 | 23 | // pool: { 24 | // max: 5, 25 | // min: 0, 26 | // acquire: 30000, 27 | // idle: 10000, 28 | // }, 29 | // }; 30 | -------------------------------------------------------------------------------- /src/routes/userRouter.js: -------------------------------------------------------------------------------- 1 | const userController = require("../controllers/userController.js"); 2 | const auth = require("../auth/auth"); 3 | 4 | const router = require("express").Router(); 5 | 6 | router.route("/").get(auth.verifyToken, userController.getUsers); 7 | 8 | router.route("/enrollMe").post(auth.verifyToken, userController.enrollMe); 9 | 10 | router 11 | .route("/:id") 12 | .get(auth.verifyToken, userController.getUser) 13 | .put(auth.verifyToken, userController.updateUser) 14 | .delete(auth.verifyToken, userController.deleteUser); 15 | 16 | router 17 | .route("/upgrade/:username") 18 | .put(auth.verifyToken, userController.upgradeLearner); 19 | 20 | 21 | module.exports = router; 22 | -------------------------------------------------------------------------------- /src/models/answerModel.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | return sequelize.define("answer", { 3 | _id: { 4 | type: DataTypes.INTEGER, 5 | primaryKey: true, 6 | allowNull: false, 7 | autoIncrement: true, 8 | }, 9 | username: { 10 | type: DataTypes.STRING, 11 | allowNull: false, 12 | }, 13 | questionId: { 14 | type: DataTypes.INTEGER, 15 | allowNull: false, 16 | }, 17 | body: { 18 | type: DataTypes.STRING, 19 | allowNull: false, 20 | }, 21 | date: { 22 | type: DataTypes.DATEONLY, 23 | allowNull: false, 24 | }, 25 | }); 26 | }; -------------------------------------------------------------------------------- /src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lms", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "npx nodemon index", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "ahmed mahboub", 11 | "license": "ISC", 12 | "dependencies": { 13 | "bcrypt": "^5.0.1", 14 | "body-parser": "^1.19.1", 15 | "cors": "^2.8.5", 16 | "express": "^4.17.2", 17 | "http-status": "^1.5.0", 18 | "jsonwebtoken": "^8.5.1", 19 | "mysql2": "^2.3.3", 20 | "nodemailer": "^6.7.2", 21 | "sequelize": "^6.12.0" 22 | }, 23 | "devDependencies": { 24 | "nodemon": "^2.0.15" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/models/questionModel.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | return sequelize.define("question", { 3 | _id: { 4 | type: DataTypes.INTEGER, 5 | primaryKey: true, 6 | allowNull: false, 7 | autoIncrement: true, 8 | }, 9 | username: { 10 | type: DataTypes.STRING, 11 | allowNull: false, 12 | }, 13 | courseId: { 14 | type: DataTypes.INTEGER, 15 | allowNull: false, 16 | }, 17 | body: { 18 | type: DataTypes.STRING, 19 | allowNull: false, 20 | }, 21 | date: { 22 | type: DataTypes.DATEONLY, 23 | allowNull: false, 24 | }, 25 | }); 26 | }; -------------------------------------------------------------------------------- /src/models/courseModel.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | return sequelize.define("course", { 3 | _id: { 4 | type: DataTypes.INTEGER, 5 | primaryKey: true, 6 | allowNull: false, 7 | autoIncrement: true, 8 | }, 9 | name: { 10 | type: DataTypes.STRING, 11 | unique: true, 12 | allowNull: false, 13 | }, 14 | syllabus: { 15 | type: DataTypes.STRING, 16 | allowNull: false, 17 | get() { 18 | const syllabus = this.getDataValue('syllabus'); 19 | return syllabus.split("$%&"); 20 | }, 21 | set(syllabus) { 22 | this.setDataValue('syllabus', syllabus.join("$%&")); 23 | } 24 | }, 25 | }); 26 | }; -------------------------------------------------------------------------------- /src/models/activityModel.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | return sequelize.define("activity", { 3 | _id: { 4 | type: DataTypes.INTEGER, 5 | primaryKey: true, 6 | allowNull: false, 7 | autoIncrement: true, 8 | }, 9 | name: { 10 | type: DataTypes.STRING, 11 | allowNull: false, 12 | }, 13 | type: { 14 | type: DataTypes.STRING, // {"youtube", "pdf"} 15 | allowNull: false, 16 | }, 17 | link: { 18 | type: DataTypes.STRING, 19 | allowNull: false, 20 | // allowNull: true // uncomment me in case of adding quizzes 👌 21 | }, 22 | courseId: { 23 | type: DataTypes.INTEGER, 24 | allowNull: false, 25 | }, 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const cors = require("cors"); 2 | const express = require("express"); 3 | const bodyParser = require("body-parser"); 4 | 5 | const app = express(); 6 | const PORT = process.env.port || 4000; 7 | 8 | let corsOptions = { 9 | origin: "*", 10 | }; 11 | 12 | // ^ Middleware 13 | app.use(cors(corsOptions)); 14 | app.use(bodyParser.json()); 15 | app.use( 16 | express.urlencoded({ 17 | extended: true, 18 | }) 19 | ); 20 | 21 | // ^ Routers 22 | const router = require("./routes"); 23 | app.use("/api", router); 24 | 25 | // ^ Routes 26 | app.get("/", (req, res) => { 27 | res.json({ 28 | works: "fine", 29 | }); 30 | }); 31 | 32 | // ^ Errors 33 | app.use(function (err, req, res, next) { 34 | return res.status(422).send({ 35 | error: err.message, 36 | }); 37 | }); 38 | 39 | // ^ Listen to requests 40 | app.listen(PORT, function () { 41 | console.log(`Listening on port: ${PORT}`); 42 | }); 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Adel Rizq 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/routes/courseRouter.js: -------------------------------------------------------------------------------- 1 | const courseController = require("../controllers/courseController.js"); 2 | const auth = require("../auth/auth"); 3 | 4 | const router = require("express").Router(); 5 | 6 | router 7 | .route("/") 8 | .get(auth.verifyToken, courseController.getCourses) 9 | .post(auth.verifyToken, courseController.addCourse); 10 | 11 | router 12 | .route("/my-courses") 13 | .get(auth.verifyToken, courseController.getMyCourses); 14 | 15 | router 16 | .route("/top-courses") 17 | .get(auth.verifyToken, courseController.getTopCourses); 18 | 19 | router 20 | .route("/:id") 21 | .get(auth.verifyToken, courseController.getCourse) 22 | .post(auth.verifyToken, courseController.enrollLearner) 23 | .delete(auth.verifyToken, courseController.deleteCourse); 24 | 25 | router 26 | .route("/:id/questions") 27 | .get(auth.verifyToken, courseController.getQuestions) 28 | .post(auth.verifyToken, courseController.addQuestion); 29 | 30 | router 31 | .route("/:id/answers") 32 | .get(auth.verifyToken, courseController.getAnswers) 33 | .post(auth.verifyToken, courseController.addAnswer); 34 | 35 | module.exports = router; 36 | -------------------------------------------------------------------------------- /src/models/index.js: -------------------------------------------------------------------------------- 1 | const dbConfig = require("../config/db.config"); 2 | const { Sequelize, DataTypes } = require("sequelize"); 3 | 4 | const sequelize = new Sequelize(dbConfig.DB, dbConfig.USER, dbConfig.PASSWORD, { 5 | host: dbConfig.HOST, 6 | dialect: dbConfig.dialect, 7 | logging: false, 8 | pool: { 9 | max: dbConfig.pool.max, 10 | min: dbConfig.pool.min, 11 | acquire: dbConfig.pool.acquire, 12 | idle: dbConfig.pool.idle, 13 | }, 14 | }); 15 | 16 | sequelize 17 | .authenticate() 18 | .then(() => { 19 | console.log("connected..."); 20 | }) 21 | .catch((err) => { 22 | console.log("Error" + err); 23 | }); 24 | 25 | const db = {}; 26 | db.Sequelize = Sequelize; 27 | db.sequelize = sequelize; 28 | 29 | db.users = require("./userModel")(sequelize, DataTypes); 30 | db.courses = require("./courseModel")(sequelize, DataTypes); 31 | db.activities = require("./activityModel")(sequelize, DataTypes); 32 | 33 | db.answers = require("./answerModel")(sequelize, DataTypes); 34 | db.questions = require("./questionModel")(sequelize, DataTypes); 35 | db.userCourses = require("./userCoursesModel")(sequelize, DataTypes); 36 | 37 | db.users.belongsToMany(db.courses, { 38 | through: db.userCourses, 39 | onDelete: "RESTRICT", 40 | onUpdate: "RESTRICT", 41 | }); 42 | 43 | db.courses.belongsToMany(db.users, { 44 | through: db.userCourses, 45 | onDelete: "RESTRICT", 46 | onUpdate: "RESTRICT", 47 | }); 48 | 49 | db.courses.belongsTo(db.users, { 50 | as: "instructor", 51 | }); 52 | 53 | db.courses.hasMany(db.activities, { 54 | foreignKey: "courseId", 55 | onDelete: "CASCADE", 56 | onUpdate: "CASCADE", 57 | }); 58 | db.activities.belongsTo(db.courses, { 59 | foreignKey: "courseId", 60 | }); 61 | 62 | db.courses.hasMany(db.questions, { 63 | foreignKey: "courseId", 64 | onDelete: "CASCADE", 65 | onUpdate: "CASCADE", 66 | }); 67 | 68 | db.questions.hasMany(db.answers, { 69 | foreignKey: "questionId", 70 | onDelete: "CASCADE", 71 | onUpdate: "CASCADE", 72 | }); 73 | 74 | db.sequelize 75 | .sync({ 76 | force: false, 77 | }) 78 | .then(() => { 79 | console.log("re-sync done"); 80 | }); 81 | 82 | module.exports = db; 83 | -------------------------------------------------------------------------------- /src/models/userModel.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require("bcrypt"); 2 | 3 | module.exports = (sequelize, DataTypes) => { 4 | let userSchema = sequelize.define( 5 | "user", 6 | { 7 | _id: { 8 | type: DataTypes.INTEGER, 9 | primaryKey: true, 10 | allowNull: false, 11 | autoIncrement: true, 12 | // defaultValue: DataTypes.UUIDV4, 13 | }, 14 | username: { 15 | type: DataTypes.STRING(45), 16 | allowNull: false, 17 | unique: true, 18 | }, 19 | email: { 20 | type: DataTypes.STRING(45), 21 | allowNull: false, 22 | unique: true, 23 | }, 24 | password: { 25 | type: DataTypes.STRING, 26 | allowNull: false, 27 | }, 28 | firstName: { 29 | type: DataTypes.STRING, 30 | allowNull: false, 31 | }, 32 | lastName: { 33 | type: DataTypes.STRING, 34 | allowNull: false, 35 | }, 36 | birthDate: { 37 | type: DataTypes.DATEONLY, 38 | allowNull: false, 39 | }, 40 | type: { 41 | type: DataTypes.STRING, 42 | allowNull: false, 43 | }, 44 | }, 45 | { 46 | hooks: { 47 | beforeCreate: async (user) => { 48 | if (user.password) { 49 | const salt = await bcrypt.genSaltSync(10, "a"); 50 | user.password = bcrypt.hashSync(user.password, salt); 51 | } 52 | }, 53 | beforeUpdate: async (user) => { 54 | if (user.password) { 55 | const salt = await bcrypt.genSaltSync(10, "a"); 56 | user.password = bcrypt.hashSync(user.password, salt); 57 | } 58 | }, 59 | }, 60 | instanceMethods: { 61 | validPassword: (password) => { 62 | return bcrypt.compareSync(password, this.password); 63 | }, 64 | }, 65 | } 66 | ); 67 | 68 | userSchema.prototype.validPassword = async (password, hash) => { 69 | return bcrypt.compareSync(password, hash); 70 | }; 71 | 72 | return userSchema; 73 | }; 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
7 | 8 |Adel Rizq |
54 |
55 | Ahmed Mahboub |
56 |
57 | Abdallah Hemdan |
58 |
59 | Eman Othman |
60 |
61 |
${constants.env.CLIENT_URL}/reset/${token}
`, 129 | }; 130 | 131 | transporter.sendMail(mailOptions, function(error, info) { 132 | if (error) { 133 | return res.status(httpStatus.OK).send({ 134 | message: "invalid email sent", 135 | }); 136 | } else { 137 | return res.status(httpStatus.OK).send({ 138 | message: "email sent", 139 | }); 140 | } 141 | }); 142 | }; 143 | 144 | const resetPassword = async(req, res) => { 145 | try { 146 | const email = jwt.verify( 147 | req.body.token, 148 | config.SECRET_KEY_RESET_PASSWORD 149 | ).email; 150 | 151 | const salt = await bcrypt.genSaltSync(10, "a"); 152 | const updatedPassword = bcrypt.hashSync(req.body.password, salt); 153 | 154 | const user = await User.update({ 155 | password: updatedPassword, 156 | }, { 157 | where: { 158 | email: email, 159 | }, 160 | }); 161 | 162 | if (!user) { 163 | return res.status(httpStatus.FORBIDDEN).send({ 164 | message: "invalid email", 165 | }); 166 | } 167 | 168 | return res.status(httpStatus.OK).send({ 169 | message: "password updated successfully", 170 | }); 171 | } catch (error) { 172 | return res.status(httpStatus.FORBIDDEN).send({ 173 | message: "invalid token sent", 174 | }); 175 | } 176 | }; 177 | 178 | // 2. Get current user 179 | const getUser = async(req, res) => { 180 | jwt.verify(req.token, config.SECRET_KEY, async(err, authData) => { 181 | if (err) { 182 | return res.status(httpStatus.UNAUTHORIZED).send({ 183 | message: err.message, 184 | }); 185 | } 186 | 187 | const user = await User.findOne({ 188 | where: { 189 | _id: authData._id, 190 | }, 191 | attributes: [ 192 | "username", 193 | "email", 194 | "firstName", 195 | "lastName", 196 | "type", 197 | "birthDate", 198 | ], 199 | }); 200 | 201 | if (!user) { 202 | return res.status(httpStatus.NOT_FOUND).send({ 203 | message: "Not Found", 204 | }); 205 | } 206 | 207 | return res.status(httpStatus.OK).send(user); 208 | }); 209 | }; 210 | 211 | // 3. Get Users for manage users page 212 | const getUsers = async(req, res) => { 213 | jwt.verify(req.token, config.SECRET_KEY, async(err, authData) => { 214 | if (err) { 215 | return res.status(httpStatus.UNAUTHORIZED).send({ 216 | message: err.message, 217 | }); 218 | } 219 | 220 | const user = await User.findOne({ 221 | where: { 222 | username: authData.username, 223 | }, 224 | }); 225 | 226 | if (!user) { 227 | return res.status(httpStatus.NOT_FOUND).send({ 228 | message: "user Not Found", 229 | }); 230 | } 231 | 232 | if (user.type != constants.userType.ADMIN) { 233 | return res.status(httpStatus.UNAUTHORIZED).send({ 234 | message: "you must be an admin to make this operation ", 235 | }); 236 | } 237 | try { 238 | const users = await User.findAll({ 239 | where: { type: ["learner", "instructor"] }, 240 | attributes: ["username", "type"], 241 | }); 242 | 243 | return res.status(httpStatus.OK).send(users); 244 | } catch (error) { 245 | return res.status(httpStatus.NOT_FOUND).send({ 246 | message: "error fetching users", 247 | }); 248 | } 249 | }); 250 | }; 251 | 252 | const upgradeLearner = async(req, res) => { 253 | jwt.verify(req.token, config.SECRET_KEY, async(err, authData) => { 254 | if (err) { 255 | return res.status(httpStatus.UNAUTHORIZED).send({ 256 | message: err.message, 257 | }); 258 | } 259 | 260 | const admin = await User.findOne({ 261 | where: { 262 | username: authData.username, 263 | }, 264 | }); 265 | 266 | if (!admin) { 267 | return res.status(httpStatus.NOT_FOUND).send({ 268 | message: "user Not Found", 269 | }); 270 | } 271 | 272 | if (admin.type != constants.userType.ADMIN) { 273 | return res.status(httpStatus.UNAUTHORIZED).send({ 274 | message: "you must be an admin to make this operation ", 275 | }); 276 | } 277 | 278 | const user = await User.findOne({ 279 | where: { 280 | username: req.params.username, 281 | }, 282 | }); 283 | 284 | if (!user) { 285 | return res.status(httpStatus.NOT_FOUND).send({ 286 | message: "user not found", 287 | }); 288 | } 289 | 290 | if (user.type != constants.userType.LEARNER) { 291 | return res.status(httpStatus.FORBIDDEN).send({ 292 | message: "user is already instructor", 293 | }); 294 | } 295 | try { 296 | await User.update({ 297 | type: constants.userType.INSTRUCTOR, 298 | }, { 299 | where: { 300 | username: req.params.username, 301 | }, 302 | attributes: [ 303 | "username", 304 | "email", 305 | "firstName", 306 | "lastName", 307 | "type", 308 | "birthDate", 309 | ], 310 | }); 311 | await UserCourse.destroy({ 312 | where: { 313 | userId: user._id 314 | } 315 | }); 316 | return res.status(httpStatus.OK).send({ 317 | message: "updated successfully", 318 | }); 319 | } catch (error) { 320 | return res.status(httpStatus.INTERNAL_SERVER_ERROR).send({ 321 | message: "error during updating database", 322 | }); 323 | } 324 | }); 325 | }; 326 | 327 | const updateUser = async(req, res) => { 328 | jwt.verify(req.token, config.SECRET_KEY, async(err, authData) => { 329 | if (err) { 330 | return res.status(httpStatus.UNAUTHORIZED).send({ 331 | message: err.message, 332 | }); 333 | } 334 | const user = await User.findOne({ 335 | where: { 336 | _id: authData._id, 337 | }, 338 | }); 339 | 340 | if (!user) { 341 | return res.status(httpStatus.NOT_FOUND).send({ 342 | message: "user Not Found", 343 | }); 344 | } 345 | 346 | const updatedData = { 347 | username: req.body.username || user.username, 348 | email: req.body.email || user.email, 349 | firstName: req.body.firstName || user.firstName, 350 | lastName: req.body.lastName || user.lastName, 351 | birthDate: req.body.birthDate || user.birthDate, 352 | }; 353 | 354 | try { 355 | await User.update(updatedData, { 356 | where: { 357 | _id: user._id, 358 | }, 359 | }); 360 | const updatedUser = await User.findOne({ 361 | where: { 362 | _id: user._id, 363 | }, 364 | }); 365 | 366 | const userData = { 367 | username: updatedUser.username, 368 | email: updatedUser.email, 369 | firstName: updatedUser.firstName, 370 | lastName: updatedUser.lastName, 371 | birthDate: updatedUser.birthDate, 372 | type: updatedUser.type, 373 | }; 374 | 375 | return res.status(httpStatus.OK).send({ 376 | message: "updated successfully", 377 | userData: userData, 378 | token: jwt.sign(userData, config.SECRET_KEY, { 379 | expiresIn: config.JWT_EXPIRES_IN, 380 | }), 381 | }); 382 | } catch (error) { 383 | return res.status(httpStatus.BAD_REQUEST).send({ 384 | message: "invalid info provided", 385 | }); 386 | } 387 | }); 388 | }; 389 | 390 | const enrollMe = async(req, res) => { 391 | jwt.verify(req.token, config.SECRET_KEY, async(err, authData) => { 392 | if (err) { 393 | return res.status(httpStatus.UNAUTHORIZED).send({ 394 | message: err.message, 395 | }); 396 | } 397 | 398 | const user = await User.findOne({ 399 | where: { 400 | _id: authData._id, 401 | }, 402 | }); 403 | 404 | if (!user) { 405 | return res.status(httpStatus.NOT_FOUND).send({ 406 | message: "user Not Found", 407 | }); 408 | } 409 | if ( 410 | user.type != constants.userType.LEARNER && 411 | user.type != constants.userType.ADMIN 412 | ) { 413 | return res.status(httpStatus.UNAUTHORIZED).send({ 414 | message: "you must be an admin or learner to make this operation", 415 | }); 416 | } 417 | try { 418 | const userCourse = await UserCourse.create({ 419 | userId: authData._id, 420 | courseId: req.body.courseId, 421 | }); 422 | 423 | if (!userCourse) 424 | return res.status(httpStatus.INTERNAL_SERVER_ERROR).send({ 425 | message: "failed to enroll, please try again", 426 | }); 427 | 428 | return res.status(httpStatus.OK).send(userCourse); 429 | } catch (error) { 430 | return res.status(httpStatus.ALREADY_REPORTED).send({ 431 | message: "Already enrolled", 432 | }); 433 | } 434 | }); 435 | }; 436 | 437 | // 4. Delete User 438 | const deleteUser = async(req, res) => { 439 | jwt.verify(req.token, config.SECRET_KEY, async(err, authData) => { 440 | if (err) { 441 | return res.status(httpStatus.UNAUTHORIZED).send({ 442 | message: err.message, 443 | }); 444 | } 445 | const user = await User.findOne({ 446 | where: { 447 | username: authData.username, 448 | }, 449 | }); 450 | 451 | if (!user) { 452 | return res.status(httpStatus.NOT_FOUND).send({ 453 | message: "user Not Found", 454 | }); 455 | } 456 | 457 | if (user.type != constants.userType.ADMIN) { 458 | return res.status(httpStatus.UNAUTHORIZED).send({ 459 | message: "you must be an admin to make this operation ", 460 | }); 461 | } 462 | 463 | const userToBeDeleted = await User.findOne({ 464 | where: { 465 | username: req.params.username, 466 | }, 467 | }); 468 | 469 | if (!userToBeDeleted) { 470 | return res.status(httpStatus.NOT_FOUND).send({ 471 | message: "User Not Found", 472 | }); 473 | } 474 | try { 475 | await User.destroy({ 476 | where: { 477 | username: req.params.username, 478 | }, 479 | }); 480 | return res.status(httpStatus.OK).send({ 481 | message: "User Deleted Successfully", 482 | }); 483 | } catch (error) { 484 | return res.status(httpStatus.INTERNAL_SERVER_ERROR).send({ 485 | message: "failed to delete user", 486 | }); 487 | } 488 | }); 489 | }; 490 | 491 | module.exports = { 492 | signup, 493 | login, 494 | updateUser, 495 | forgotPassword, 496 | resetPassword, 497 | getUser, 498 | getUsers, 499 | upgradeLearner, 500 | enrollMe, 501 | deleteUser, 502 | }; -------------------------------------------------------------------------------- /src/controllers/courseController.js: -------------------------------------------------------------------------------- 1 | const httpStatus = require("http-status"); 2 | const jwt = require("jsonwebtoken"); 3 | const config = require("./../config/jwt.config"); 4 | 5 | const db = require("../models"); 6 | const constants = require("../config/constants.config"); 7 | 8 | const Course = db.courses; 9 | const User = db.users; 10 | const Question = db.questions; 11 | const Answer = db.answers; 12 | const UserCourse = db.userCourses; 13 | 14 | // 1. Add Course 15 | // instructor or admin accounts 16 | const addCourse = async (req, res) => { 17 | jwt.verify(req.token, config.SECRET_KEY, async (err, authData) => { 18 | if (err) { 19 | return res.status(httpStatus.UNAUTHORIZED).send({ 20 | message: err.message, 21 | }); 22 | } 23 | const user = await User.findOne({ 24 | where: { 25 | username: authData.username, 26 | }, 27 | }); 28 | 29 | if (!user) { 30 | return res.status(httpStatus.NOT_FOUND).send({ 31 | message: "user Not Found", 32 | }); 33 | } 34 | 35 | if ( 36 | user.type != constants.userType.ADMIN && 37 | user.type != constants.userType.INSTRUCTOR 38 | ) { 39 | return res.status(httpStatus.UNAUTHORIZED).send({ 40 | message: 41 | "you must be an admin or instructor to do this operation ", 42 | }); 43 | } 44 | 45 | const info = { 46 | name: req.body.name, 47 | syllabus: req.body.syllabus, 48 | instructorId: authData._id, 49 | }; 50 | 51 | try { 52 | const course = await Course.create(info); 53 | return res.status(httpStatus.OK).send(course); 54 | } catch (error) { 55 | return res.status(httpStatus.FORBIDDEN).send({ 56 | message: "Duplicate course name", 57 | }); 58 | } 59 | }); 60 | }; 61 | 62 | const addQuestion = async (req, res) => { 63 | jwt.verify(req.token, config.SECRET_KEY, async (err, authData) => { 64 | if (err) { 65 | return res.status(httpStatus.UNAUTHORIZED).send({ 66 | message: err.message, 67 | }); 68 | } 69 | 70 | console.log(req); 71 | const user = await User.findOne({ 72 | where: { 73 | username: authData.username, 74 | }, 75 | }); 76 | 77 | if (user == null) { 78 | return res.status(httpStatus.NOT_FOUND).send({ 79 | message: "user Not Found", 80 | }); 81 | } 82 | 83 | const course = await Course.findOne({ 84 | where: { 85 | _id: req.params.id, 86 | }, 87 | }); 88 | 89 | if (course == null) { 90 | return res.status(httpStatus.NOT_FOUND).send({ 91 | message: "course Not Found", 92 | }); 93 | } 94 | 95 | const info = { 96 | username: authData.username, 97 | courseId: req.params.id, 98 | body: req.body.question, 99 | date: req.body.date, 100 | }; 101 | 102 | console.log(info); 103 | try { 104 | const question = await Question.create(info); 105 | return res.status(httpStatus.OK).send(question); 106 | } catch (error) { 107 | return res.status(httpStatus.FORBIDDEN).send({ 108 | message: "invalid question, please try again", 109 | }); 110 | } 111 | }); 112 | }; 113 | 114 | const addAnswer = async (req, res) => { 115 | jwt.verify(req.token, config.SECRET_KEY, async (err, authData) => { 116 | if (err) { 117 | return res.status(httpStatus.UNAUTHORIZED).send({ 118 | message: err.message, 119 | }); 120 | } 121 | 122 | const user = await User.findOne({ 123 | where: { 124 | username: authData.username, 125 | }, 126 | }); 127 | 128 | if (user == null) { 129 | return res.status(httpStatus.NOT_FOUND).send({ 130 | message: "user Not Found", 131 | }); 132 | } 133 | 134 | const question = await Question.findOne({ 135 | where: { 136 | _id: req.params.id, 137 | }, 138 | }); 139 | 140 | if (question == null) { 141 | return res.status(httpStatus.NOT_FOUND).send({ 142 | message: "question Not Found", 143 | }); 144 | } 145 | 146 | const info = { 147 | username: authData.username, 148 | questionId: req.params.id, 149 | body: req.body.answer, 150 | date: req.body.date, 151 | }; 152 | 153 | try { 154 | const answer = await Answer.create(info); 155 | return res.status(httpStatus.OK).send(answer); 156 | } catch (error) { 157 | return res.status(httpStatus.FORBIDDEN).send({ 158 | message: "invalid answer, please try again", 159 | }); 160 | } 161 | }); 162 | }; 163 | 164 | // 2. Get Courses 165 | // any authorized user 166 | const getCourse = async (req, res) => { 167 | jwt.verify(req.token, config.SECRET_KEY, async (err, authData) => { 168 | if (err) { 169 | return res.status(httpStatus.UNAUTHORIZED).send({ 170 | message: err.message, 171 | }); 172 | } 173 | const course = await Course.findOne({ 174 | where: { 175 | _id: req.params.id, 176 | }, 177 | attributes: ["name", "syllabus", "instructorId", "createdAt"], 178 | }); 179 | 180 | // course.dataValues.createdAt.substr(0, 10) 181 | 182 | if (!course) { 183 | return res.status(httpStatus.NOT_FOUND).send({ 184 | message: "Course Not Found", 185 | }); 186 | } 187 | 188 | course.dataValues.date = new Date(course.dataValues.createdAt) 189 | .toISOString() 190 | .split("T")[0]; 191 | 192 | course.dataValues.instructorName = ( 193 | await User.findOne({ 194 | where: { 195 | _id: course.dataValues.instructorId, 196 | }, 197 | attributes: ["username"], 198 | }) 199 | ).dataValues.username; 200 | 201 | delete course.dataValues.createdAt; 202 | delete course.dataValues.instructorId; 203 | 204 | return res.status(httpStatus.OK).send(course); 205 | }); 206 | }; 207 | 208 | // any authorized user 209 | const getCourses = async (req, res) => { 210 | jwt.verify(req.token, config.SECRET_KEY, async (err, authData) => { 211 | if (err) { 212 | return res.status(httpStatus.UNAUTHORIZED).send({ 213 | message: err.message, 214 | }); 215 | } 216 | 217 | let myCoursesIds = await UserCourse.findAll({ 218 | where: { 219 | userId: authData._id, 220 | }, 221 | attributes: ["courseId"], 222 | }); 223 | myCoursesIds = myCoursesIds.map( 224 | (userCourse) => userCourse.dataValues.courseId 225 | ); 226 | 227 | let courses = await Course.findAll({ 228 | attributes: [ 229 | "_id", 230 | "name", 231 | "syllabus", 232 | "instructorId", 233 | "createdAt", 234 | ], 235 | }); 236 | 237 | if (!courses) { 238 | return res.status(httpStatus.NO_CONTENT).send({ 239 | message: "No Courses Found", 240 | }); 241 | } 242 | 243 | for (let course of courses) { 244 | course.dataValues.date = new Date(course.dataValues.createdAt) 245 | .toISOString() 246 | .split("T")[0]; 247 | 248 | course.dataValues.isEnrolled = myCoursesIds.includes( 249 | course.dataValues._id 250 | ); 251 | 252 | course.dataValues.instructorName = ( 253 | await User.findOne({ 254 | where: { 255 | _id: course.dataValues.instructorId, 256 | }, 257 | attributes: ["username"], 258 | }) 259 | ).dataValues.username; 260 | 261 | delete course.dataValues.createdAt; 262 | delete course.dataValues.instructorId; 263 | } 264 | 265 | return res.status(httpStatus.OK).send(courses); 266 | }); 267 | }; 268 | 269 | const getMyCourses = async (req, res) => { 270 | jwt.verify(req.token, config.SECRET_KEY, async (err, authData) => { 271 | if (err) { 272 | return res.status(httpStatus.UNAUTHORIZED).send({ 273 | message: err.message, 274 | }); 275 | } 276 | 277 | let myCoursesIds = await UserCourse.findAll({ 278 | where: { 279 | userId: authData._id, 280 | }, 281 | attributes: ["courseId"], 282 | }); 283 | 284 | myCoursesIds = myCoursesIds.map( 285 | (userCourse) => userCourse.dataValues.courseId 286 | ); 287 | 288 | let courses = await Course.findAll({ 289 | where: { 290 | [Op.or]: [ 291 | { _id: myCoursesIds }, 292 | { instructorId: authData._id }, 293 | ], 294 | }, 295 | attributes: [ 296 | "_id", 297 | "name", 298 | "syllabus", 299 | "instructorId", 300 | "createdAt", 301 | ], 302 | }); 303 | 304 | if (!courses) { 305 | return res.status(httpStatus.NO_CONTENT).send({ 306 | message: "No Courses Found For You", 307 | }); 308 | } 309 | 310 | for (let course of courses) { 311 | course.dataValues.date = new Date(course.dataValues.createdAt) 312 | .toISOString() 313 | .split("T")[0]; 314 | 315 | course.dataValues.isEnrolled = myCoursesIds.includes( 316 | course.dataValues._id 317 | ); 318 | 319 | course.dataValues.instructorName = ( 320 | await User.findOne({ 321 | where: { 322 | _id: course.dataValues.instructorId, 323 | }, 324 | attributes: ["username"], 325 | }) 326 | ).dataValues.username; 327 | 328 | delete course.dataValues.createdAt; 329 | delete course.dataValues.instructorId; 330 | } 331 | 332 | return res.status(httpStatus.OK).send(courses); 333 | }); 334 | }; 335 | 336 | // any authorized user 337 | const getTopCourses = async (req, res) => { 338 | jwt.verify(req.token, config.SECRET_KEY, async (err, authData) => { 339 | if (err) { 340 | return res.status(httpStatus.UNAUTHORIZED).send({ 341 | message: err.message, 342 | }); 343 | } 344 | 345 | let courses = await Course.findAll({ 346 | attributes: [ 347 | "_id", 348 | "name", 349 | "syllabus", 350 | "instructorId", 351 | "createdAt", 352 | ], 353 | limit: 3, 354 | }); 355 | 356 | if (!courses) { 357 | return res.status(httpStatus.NO_CONTENT).send({ 358 | message: "No Courses Found", 359 | }); 360 | } 361 | 362 | for (let course of courses) { 363 | course.dataValues.date = new Date(course.dataValues.createdAt) 364 | .toISOString() 365 | .split("T")[0]; 366 | 367 | course.dataValues.instructorName = ( 368 | await User.findOne({ 369 | where: { 370 | _id: course.dataValues.instructorId, 371 | }, 372 | attributes: ["username"], 373 | }) 374 | ).dataValues.username; 375 | 376 | delete course.dataValues.createdAt; 377 | delete course.dataValues.instructorId; 378 | } 379 | 380 | return res.status(httpStatus.OK).send(courses); 381 | }); 382 | }; 383 | 384 | // any authorized user 385 | const getQuestions = async (req, res) => { 386 | jwt.verify(req.token, config.SECRET_KEY, async (err, authData) => { 387 | if (err) { 388 | return res.status(httpStatus.UNAUTHORIZED).send({ 389 | message: err.message, 390 | }); 391 | } 392 | let questions = await Question.findAll({ 393 | where: { 394 | courseId: req.params.id, 395 | }, 396 | attributes: ["_id", "username", "body", "createdAt"], 397 | }); 398 | 399 | if (!questions) { 400 | return res.status(httpStatus.NO_CONTENT).send({ 401 | message: "No Questions Found", 402 | }); 403 | } 404 | 405 | for (let question of questions) { 406 | const answers = await Answer.findAll({ 407 | where: { 408 | questionId: question.dataValues._id, 409 | }, 410 | attributes: ["username", "body", "createdAt"], 411 | }); 412 | 413 | question.dataValues.date = new Date(question.dataValues.createdAt) 414 | .toISOString() 415 | .split("T")[0]; 416 | 417 | for (let answer of answers) { 418 | answer.date = new Date(answer.createdAt) 419 | .toISOString() 420 | .split("T")[0]; 421 | 422 | delete answer.createdAt; 423 | } 424 | 425 | question.dataValues.answers = answers; 426 | 427 | delete question.dataValues.createdAt; 428 | } 429 | 430 | return res.status(httpStatus.OK).send(questions); 431 | }); 432 | }; 433 | 434 | // any authorized user 435 | const getAnswers = async (req, res) => { 436 | jwt.verify(req.token, config.SECRET_KEY, async (err, authData) => { 437 | if (err) { 438 | return res.status(httpStatus.UNAUTHORIZED).send({ 439 | message: err.message, 440 | }); 441 | } 442 | try { 443 | const answers = await Answer.findAll({ 444 | where: { 445 | questionId: req.params.id, 446 | }, 447 | attributes: ["username", "body", "createdAt"], 448 | }); 449 | 450 | if (!answers) { 451 | return res.status(httpStatus.NO_CONTENT).send({ 452 | message: "No Answers Found", 453 | }); 454 | } 455 | return res.status(httpStatus.OK).send(answers); 456 | } catch (error) { 457 | return res.status(httpStatus.INTERNAL_SERVER_ERROR).send({ 458 | message: "error getting answers", 459 | }); 460 | } 461 | }); 462 | }; 463 | 464 | const enrollLearner = async (req, res) => { 465 | jwt.verify(req.token, config.SECRET_KEY, async (err, authData) => { 466 | if (err) { 467 | return res.status(httpStatus.UNAUTHORIZED).send({ 468 | message: err.message, 469 | }); 470 | } 471 | 472 | const user = await User.findOne({ 473 | where: { 474 | _id: authData._id, 475 | }, 476 | }); 477 | 478 | if (user == null) { 479 | return res.status(httpStatus.NOT_FOUND).send({ 480 | message: "user Not Found", 481 | }); 482 | } 483 | 484 | if ( 485 | user.type != constants.userType.INSTRUCTOR && 486 | user.type != constants.userType.ADMIN 487 | ) { 488 | return res.status(httpStatus.UNAUTHORIZED).send({ 489 | message: 490 | "you must be an admin or learner to make this operation", 491 | }); 492 | } 493 | 494 | try { 495 | if (!req.body.email) { 496 | return res.status(httpStatus.NOT_FOUND).send({ 497 | message: "Email should be sent at the request body", 498 | }); 499 | } 500 | 501 | const learner = await User.findOne({ 502 | where: { 503 | email: req.body.email, 504 | }, 505 | attributes: ["_id"], 506 | }); 507 | 508 | if (!learner) { 509 | return res.status(httpStatus.NOT_FOUND).send({ 510 | message: "User Not Found", 511 | }); 512 | } 513 | 514 | const course = await Course.findOne({ 515 | where: { 516 | _id: req.params.id, 517 | }, 518 | attributes: ["_id"], 519 | }); 520 | 521 | if (!course) { 522 | return res.status(httpStatus.NOT_FOUND).send({ 523 | message: "Course Not Found", 524 | }); 525 | } 526 | 527 | const userCourse = await UserCourse.create({ 528 | userId: learner._id, 529 | courseId: req.params.id, 530 | }); 531 | 532 | if (!userCourse) 533 | return res.status(httpStatus.INTERNAL_SERVER_ERROR).send({ 534 | message: "failed to enroll, please try again", 535 | }); 536 | 537 | return res.status(httpStatus.OK).send(userCourse); 538 | } catch (error) { 539 | return res.status(httpStatus.ALREADY_REPORTED).send({ 540 | message: "Already enrolled", 541 | }); 542 | } 543 | }); 544 | }; 545 | 546 | // 3. Delete Course 547 | const deleteCourse = async (req, res) => { 548 | jwt.verify(req.token, config.SECRET_KEY, async (err, authData) => { 549 | if (err) { 550 | return res.status(httpStatus.UNAUTHORIZED).send({ 551 | message: err.message, 552 | }); 553 | } 554 | const user = await User.findOne({ 555 | where: { 556 | username: authData.username, 557 | }, 558 | }); 559 | 560 | const course = await Course.findOne({ 561 | where: { 562 | _id: req.params.id, 563 | }, 564 | }); 565 | 566 | if (!user || !course) { 567 | return res.status(httpStatus.UNAUTHORIZED).send({ 568 | message: 569 | "you must be an admin or instructor this course to do this operation ", 570 | }); 571 | } 572 | 573 | if ( 574 | user.type != constants.userType.ADMIN && 575 | user._id != course.instructorId 576 | ) { 577 | return res.status(httpStatus.NOT_FOUND).send({ 578 | message: "course Not Found", 579 | }); 580 | } 581 | 582 | try { 583 | await Course.destroy({ 584 | where: { 585 | _id: req.params.id, 586 | }, 587 | }); 588 | 589 | return res.status(httpStatus.OK).send({ 590 | message: "Course Deleted Successfully", 591 | }); 592 | } catch (error) { 593 | return res.status(httpStatus.FORBIDDEN).send({ 594 | message: "error during deletion", 595 | }); 596 | } 597 | }); 598 | }; 599 | 600 | module.exports = { 601 | addCourse, 602 | getCourse, 603 | getCourses, 604 | getMyCourses, 605 | getTopCourses, 606 | addQuestion, 607 | getQuestions, 608 | addAnswer, 609 | getAnswers, 610 | enrollLearner, 611 | deleteCourse, 612 | }; 613 | --------------------------------------------------------------------------------