diff --git a/controllers/auth.js b/controllers/auth.js deleted file mode 100644 index c5ebfd8..0000000 --- a/controllers/auth.js +++ /dev/null @@ -1,202 +0,0 @@ -import response from "../response.js"; -import bcrypt from "bcrypt"; -import jwt from "jsonwebtoken"; -import nodemailer from 'nodemailer'; -import models from "../models/index.js"; - -const transporter = nodemailer.createTransport({ - service: 'gmail', // Anda bisa menggunakan layanan email lainnya - auth: { - user: process.env.EMAIL_USER, - pass: process.env.EMAIL_PASS, - }, -}); - -export const registerUser = async (req, res) => { - const { name, email, password, confirmPassword } = req.body; - let roles = "student"; - - if (!name) { - return res.status(400).json({ message: "Name is required!" }); - } - - if (!email) { - return res.status(400).json({ message: "Email is required!" }); - } - - if (!password) { - return res.status(400).json({ message: "Password is required!" }); - } - - if (!confirmPassword) { - return res.status(400).json({ message: "Confirm Password is required!" }); - } - - if (password !== confirmPassword) { - return res.status(400).json({ message: "Passwords do not match!" }); - } - - try { - const salt = await bcrypt.genSalt(10); - const hashedPassword = await bcrypt.hash(password, salt); - - const newUser = await models.User.create({ - name, - email, - password: hashedPassword, - roles, - }); - - res.status(200).json({ message: "Registration success", result: newUser }); - } catch (error) { - console.log(error); - - // Check for unique constraint error on email - if (error.name === "SequelizeUniqueConstraintError") { - return res.status(400).json({ message: "Email already registered!" }); - } - - res.status(500).json({ message: "Internal Server Error" }); - } -}; - -export const loginUser = async (req, res) => { - const { email, password } = req.body; - - if (!email) { - return response(400, null, "Email is required!", res); - } - - if (!password) { - return response(400, null, "Password is required!", res); - } - - try { - const user = await models.User.findOne({ where: { email } }); - - if (!user) { - return response(404, null, "User data not found!", res); - } - - const validPassword = await bcrypt.compare(password, user.password); - if (!validPassword) { - return response(401, null, "The password you entered is incorrect!", res); - } - - const accessToken = jwt.sign( - { id: user.id }, - process.env.ACCESS_TOKEN_SECRET - ); - - // Set tokens as HTTP-only cookies - res.cookie("accessToken", accessToken, { - httpOnly: true, - secure: process.env.NODE_ENV === "production", // Use secure cookies in production - }); - - // Selectively pick fields to send in the response - const userResponse = { - id: user.id, - name: user.name, - email: user.email, - roles: user.roles, - }; - - response(200, userResponse, "Success", res); - } catch (error) { - console.log(error); - res.status(500).json({ message: "Internal Server Error" }); - } -}; - -export const logoutUser = (req, res) => { - res.clearCookie("accessToken", { - httpOnly: true, - secure: process.env.NODE_ENV === "production", - }); - - res.status(200).json({ message: "You have successfully logged out." }); -}; - -export const forgotPassword = async (req, res) => { - const { email } = req.body; - - if (!email) { - return response(400, null, "Email is required!", res); - } - - try { - const user = await models.User.findOne({ where: { email } }); - - if (!user) { - return response(404, null, "Email is not registered!", res); - } - - const resetToken = jwt.sign({ id: user.id }, process.env.RESET_PASSWORD_SECRET, { - expiresIn: '1h', // Token valid for 1 hour - }); - - const resetLink = `http://localhost:${process.env.APP_PORT}/resetPassword/${resetToken}`; - - const mailOptions = { - from: process.env.EMAIL_USER, - to: user.email, - subject: 'Password Reset', - text: `You are receiving this because you (or someone else) have requested the reset of the password for your account. - Please click on the following link, or paste this into your browser to complete the process: - ${resetLink} - If you did not request this, please ignore this email and your password will remain unchanged.`, - }; - - await transporter.sendMail(mailOptions); - - response(200, null, "Password reset email sent successfully!", res); - } catch (error) { - console.log(error); - res.status(500).json({ message: "Internal Server Error" }); - } -}; - -export const resetPassword = async (req, res) => { - const { token, newPassword, confirmNewPassword } = req.body; - - if (!token) { - return response(400, null, "Token is required!", res); - } - - if (!newPassword) { - return response(400, null, "New password is required!", res); - } - - if (!confirmNewPassword) { - return response(400, null, "Confirm new password is required!", res); - } - - if (newPassword !== confirmNewPassword) { - return response(400, null, "Passwords do not match!", res); - } - - try { - const decoded = jwt.verify(token, process.env.RESET_PASSWORD_SECRET); - const user = await models.User.findOne({ where: { id: decoded.id } }); - - if (!user) { - return response(404, null, "User data not found!", res); - } - - const salt = await bcrypt.genSalt(10); - const hashedPassword = await bcrypt.hash(newPassword, salt); - - user.password = hashedPassword; - await user.save(); - - response(200, null, "Password has been reset successfully!", res); - } catch (error) { - console.log(error); - if (error.name === "TokenExpiredError") { - return response(400, null, "Reset token has expired!", res); - } else { - return res.status(500).json({ message: "Internal Server Error" }); - } - } -}; \ No newline at end of file diff --git a/controllers/auth/auth.js b/controllers/auth/auth.js new file mode 100644 index 0000000..6330fd2 --- /dev/null +++ b/controllers/auth/auth.js @@ -0,0 +1,371 @@ +import response from "../../response.js"; +import bcrypt from "bcrypt"; +import jwt from "jsonwebtoken"; +import nodemailer from "nodemailer"; +import models from "../../models/index.js"; + +const transporter = nodemailer.createTransport({ + service: "gmail", + auth: { + user: process.env.EMAIL_USER, + pass: process.env.EMAIL_PASS, + }, +}); + +export const registerAdmin = async (req, res) => { + const { name, email, password, confirmPassword } = req.body; + + if (!name) { + return response(400, null, "Name is required!", res); + } + + if (!email) { + return response(400, null, "Email is required!", res); + } + + if (!password) { + return response(400, null, "Password is required!", res); + } + + if (!confirmPassword) { + return response(400, null, "Confirm Password is required!", res); + } + + if (password !== confirmPassword) { + return response(400, null, "Passwords do not match!", res); + } + + try { + const salt = await bcrypt.genSalt(10); + const hashedPassword = await bcrypt.hash(password, salt); + + const newUser = await models.User.create({ + NAME_USERS: name, + EMAIL: email, + PASSWORD: hashedPassword, + ROLE: "admin", + PICTURE: "default-avatar.jpeg", + }); + + const adminResponse = { + id: newUser.ID, + name: newUser.NAME_USERS, + email: newUser.EMAIL, + role: newUser.ROLE, + picture: newUser.PICTURE, + }; + + response(200, adminResponse, "Admin registration successful", res); + } catch (error) { + console.log(error); + + if (error.name === "SequelizeUniqueConstraintError") { + return response(400, null, "Email already registered!", res); + } + + response(500, null, "Internal Server Error", res); + } +}; + +export const registerTeacher = async (req, res) => { + const { name, email, nip, password, confirmPassword } = req.body; + + if (!name) { + return response(400, null, "Name is required!", res); + } + + if (!email) { + return response(400, null, "Email is required!", res); + } + + if (!nip) { + return response(400, null, "NIP is required for teachers!", res); + } + + if (!password) { + return response(400, null, "Password is required!", res); + } + + if (!confirmPassword) { + return response(400, null, "Confirm Password is required!", res); + } + + if (password !== confirmPassword) { + return response(400, null, "Passwords do not match!", res); + } + + const transaction = await models.db.transaction(); + + try { + const salt = await bcrypt.genSalt(10); + const hashedPassword = await bcrypt.hash(password, salt); + + const newUser = await models.User.create( + { + NAME_USERS: name, + EMAIL: email, + PASSWORD: hashedPassword, + ROLE: "teacher", + PICTURE: "default-avatar.jpeg", + }, + { transaction } + ); + + await models.Teacher.create( + { + ID: newUser.ID, + NIP: nip, + }, + { transaction } + ); + + await transaction.commit(); + + const teacherResponse = { + id: newUser.ID, + name: newUser.NAME_USERS, + email: newUser.EMAIL, + nip: nip, + role: newUser.ROLE, + picture: newUser.PICTURE, + }; + + response(200, teacherResponse, "Teacher registration successful", res); + } catch (error) { + console.log(error); + await transaction.rollback(); + + if (error.name === "SequelizeUniqueConstraintError") { + const field = error.original.sqlMessage.match(/for key '(.+)'/)[1]; + + if (field === "teacher_unique_nip") { + return response(400, null, "NIP already registered!", res); + } + + if (field === "user_unique_email") { + return response(400, null, "Email already registered!", res); + } + } + + response(500, null, "Internal Server Error", res); + } +}; + +export const registerStudent = async (req, res) => { + const { name, email, nisn, password, confirmPassword } = req.body; + + if (!name) { + return response(400, null, "Name is required!", res); + } + + if (!email) { + return response(400, null, "Email is required!", res); + } + + if (!nisn) { + return response(400, null, "NISN is required for students!", res); + } + + if (!password) { + return response(400, null, "Password is required!", res); + } + + if (!confirmPassword) { + return response(400, null, "Confirm Password is required!", res); + } + + if (password !== confirmPassword) { + return response(400, null, "Passwords do not match!", res); + } + + const transaction = await models.db.transaction(); + + try { + const salt = await bcrypt.genSalt(10); + const hashedPassword = await bcrypt.hash(password, salt); + + const newUser = await models.User.create( + { + NAME_USERS: name, + EMAIL: email, + PASSWORD: hashedPassword, + ROLE: "student", + PICTURE: "default-avatar.jpeg", + }, + { transaction } + ); + + await models.Student.create( + { + ID: newUser.ID, + NISN: nisn, + }, + { transaction } + ); + + await transaction.commit(); + + const studentResponse = { + id: newUser.ID, + name: newUser.NAME_USERS, + email: newUser.EMAIL, + nisn: nisn, + role: newUser.ROLE, + picture: newUser.PICTURE, + }; + + response(200, studentResponse, "Student registration successful", res); + } catch (error) { + console.log(error); + await transaction.rollback(); + + if (error.name === "SequelizeUniqueConstraintError") { + const field = error.original.sqlMessage.match(/for key '(.+)'/)[1]; + + if (field === "student_unique_nisn") { + return response(400, null, "NISN already registered!", res); + } + + if (field === "user_unique_email") { + return response(400, null, "Email already registered!", res); + } + } + + response(500, null, "Internal Server Error", res); + } +}; + +export const loginUser = async (req, res) => { + const { email, password } = req.body; + + if (!email) { + return response(400, null, "Email is required!", res); + } + + if (!password) { + return response(400, null, "Password is required!", res); + } + + try { + const user = await models.User.findOne({ where: { email } }); + + if (!user) { + return response(404, null, "User data not found!", res); + } + + const validPassword = await bcrypt.compare(password, user.PASSWORD); + if (!validPassword) { + return response(401, null, "The password you entered is incorrect!", res); + } + + const accessToken = jwt.sign( + { id: user.ID }, + process.env.ACCESS_TOKEN_SECRET, + { expiresIn: "6h" } + ); + + const userResponse = { + id: user.ID, + name: user.NAME_USERS, + email: user.EMAIL, + roles: user.ROLE, + token: `Bearer ${accessToken}`, + }; + + response(200, userResponse, "Login successful", res); + } catch (error) { + console.log(error); + response(500, null, "Internal Server Error", res); + } +}; + +export const logoutUser = (req, res) => { + response(200, null, "You have successfully logged out.", res); +}; + +export const forgotPassword = async (req, res) => { + const { email } = req.body; + + if (!email) { + return response(400, null, "Email is required!", res); + } + + try { + const user = await models.User.findOne({ where: { EMAIL: email } }); + + if (!user) { + return response(404, null, "Email is not registered!", res); + } + + const resetToken = jwt.sign( + { id: user.ID }, + process.env.RESET_PASSWORD_SECRET, + { + expiresIn: "1h", + } + ); + + const resetLink = `http://localhost:${process.env.APP_PORT}/resetPassword/${resetToken}`; + + const mailOptions = { + from: process.env.EMAIL_USER, + to: user.EMAIL, + subject: "Password Reset", + text: `You are receiving this because you (or someone else) have requested the reset of the password for your account. + Please click on the following link, or paste this into your browser to complete the process: + ${resetLink} + If you did not request this, please ignore this email and your password will remain unchanged.`, + }; + + await transporter.sendMail(mailOptions); + + response(200, null, "Password reset email sent successfully!", res); + } catch (error) { + console.log(error); + response(500, null, "Internal Server Error", res); + } +}; + +export const resetPassword = async (req, res) => { + const { token, newPassword, confirmNewPassword } = req.body; + + if (!token) { + return response(400, null, "Token is required!", res); + } + + if (!newPassword) { + return response(400, null, "New password is required!", res); + } + + if (!confirmNewPassword) { + return response(400, null, "Confirm new password is required!", res); + } + + if (newPassword !== confirmNewPassword) { + return response(400, null, "Passwords do not match!", res); + } + + try { + const decoded = jwt.verify(token, process.env.RESET_PASSWORD_SECRET); + const user = await models.User.findOne({ where: { ID: decoded.id } }); + + if (!user) { + return response(404, null, "User data not found!", res); + } + + const salt = await bcrypt.genSalt(10); + const hashedPassword = await bcrypt.hash(newPassword, salt); + + user.PASSWORD = hashedPassword; + await user.save(); + + response(200, null, "Password has been reset successfully!", res); + } catch (error) { + console.log(error); + if (error.name === "TokenExpiredError") { + return response(400, null, "Reset token has expired!", res); + } else { + return response(500, null, "Internal Server Error", res); + } + } +}; diff --git a/controllers/contentControllers/exercise.js b/controllers/contentControllers/exercise.js new file mode 100644 index 0000000..af52997 --- /dev/null +++ b/controllers/contentControllers/exercise.js @@ -0,0 +1,312 @@ +import response from "../../response.js"; +import models from "../../models/index.js"; +import fs from "fs"; +import path from "path"; + +export const getExercises = async (req, res) => { + try { + const exercises = await models.Exercise.findAll({ + include: [ + { + model: models.MultipleChoices, + as: "multipleChoices", + }, + { + model: models.MatchingPairs, + as: "matchingPairs", + }, + { + model: models.TrueFalse, + as: "trueFalse", + }, + ], + }); + + if (exercises.length === 0) { + return response(404, null, "No exercises found", res); + } + + const result = exercises.map((exercise) => { + const exerciseData = { ...exercise.dataValues }; + const questionType = exercise.QUESTION_TYPE; + + if (questionType === "MCQ") { + delete exerciseData.matchingPairs; + delete exerciseData.trueFalse; + } else if (questionType === "MPQ") { + delete exerciseData.multipleChoices; + delete exerciseData.trueFalse; + } else if (questionType === "TFQ") { + delete exerciseData.multipleChoices; + delete exerciseData.matchingPairs; + } else { + delete exerciseData.multipleChoices; + delete exerciseData.matchingPairs; + delete exerciseData.trueFalse; + } + + return exerciseData; + }); + + response(200, result, "Success", res); + } catch (error) { + console.log(error); + res.status(500).json({ message: "Internal Server Error" }); + } +}; + +export const getExercisesForAdmin = async (req, res) => { + try { + const exercises = await models.Exercise.findAll({ + include: [ + { + model: models.MultipleChoices, + as: "multipleChoices", + attributes: ["ANSWER_KEY"], + }, + { + model: models.MatchingPairs, + as: "matchingPairs", + attributes: ["LEFT_PAIR", "RIGHT_PAIR"], + }, + { + model: models.TrueFalse, + as: "trueFalse", + attributes: ["IS_TRUE"], + }, + ], + }); + + const formattedExercises = exercises.map((exercise) => { + const questionType = exercise.QUESTION_TYPE; + let answerKey = null; + + if (questionType === "MCQ" && exercise.multipleChoices.length > 0) { + answerKey = exercise.multipleChoices[0].ANSWER_KEY; + } else if ( + questionType === "MPQ" && + exercise.matchingPairs.length > 0 + ) { + answerKey = exercise.matchingPairs + .map((pair) => `${pair.LEFT_PAIR}-${pair.RIGHT_PAIR}`) + .join(", "); + } else if ( + questionType === "TFQ" && + exercise.trueFalse.length > 0 + ) { + answerKey = exercise.trueFalse[0].IS_TRUE === 1 ? "true" : "false"; + } + + return { + ID_ADMIN_EXERCISE: exercise.ID_ADMIN_EXERCISE, + ID_LEVEL: exercise.ID_LEVEL, + TITLE: exercise.TITLE, + QUESTION: exercise.QUESTION, + SCORE_WEIGHT: exercise.SCORE_WEIGHT, + QUESTION_TYPE: questionType, + AUDIO: exercise.AUDIO, + VIDEO: exercise.VIDEO, + IMAGE: exercise.IMAGE, + ANSWER_KEY: answerKey, + }; + }); + + response(200, formattedExercises, "Success", res); + } catch (error) { + console.log(error); + response(500, null, "Error retrieving exercises data!", res); + } +}; + +export const getExerciseById = async (req, res) => { + try { + const { id } = req.params; + + const exercise = await models.Exercise.findByPk(id, { + include: [ + { + model: models.MultipleChoices, + as: "multipleChoices", + }, + { + model: models.MatchingPairs, + as: "matchingPairs", + }, + { + model: models.TrueFalse, + as: "trueFalse", + }, + ], + }); + + if (!exercise) { + return response(404, null, "Exercise not found", res); + } + + const exerciseData = { ...exercise.dataValues }; + const questionType = exercise.QUESTION_TYPE; + + if (questionType === "MCQ") { + delete exerciseData.matchingPairs; + delete exerciseData.trueFalse; + } else if (questionType === "MPQ") { + delete exerciseData.multipleChoices; + delete exerciseData.trueFalse; + } else if (questionType === "TFQ") { + delete exerciseData.multipleChoices; + delete exerciseData.matchingPairs; + } else { + delete exerciseData.multipleChoices; + delete exerciseData.matchingPairs; + delete exerciseData.trueFalse; + } + + response(200, exerciseData, "Success", res); + } catch (error) { + console.log(error); + res.status(500).json({ message: "Internal Server Error" }); + } +}; + +export const deleteExerciseById = async (req, res) => { + const { id } = req.params; + const transaction = await models.db.transaction(); + + try { + const exercise = await models.Exercise.findByPk(id, { + include: [ + { + model: models.MultipleChoices, + as: "multipleChoices", + }, + { + model: models.MatchingPairs, + as: "matchingPairs", + }, + { + model: models.TrueFalse, + as: "trueFalse", + }, + ], + }); + + if (!exercise) { + await transaction.rollback(); + return response(404, null, "Exercise not found", res); + } + + if (exercise.VIDEO) { + const videoPath = path.join( + "public/uploads/exercise/video", + exercise.VIDEO + ); + if (fs.existsSync(videoPath)) fs.unlinkSync(videoPath); + } + + if (exercise.AUDIO) { + const audioPath = path.join( + "public/uploads/exercise/audio", + exercise.AUDIO + ); + if (fs.existsSync(audioPath)) fs.unlinkSync(audioPath); + } + + if (exercise.IMAGE) { + const imagePath = path.join( + "public/uploads/exercise/image", + exercise.IMAGE + ); + if (fs.existsSync(imagePath)) fs.unlinkSync(imagePath); + } + + const questionType = exercise.QUESTION_TYPE; + + if (questionType === "MCQ") { + await models.MultipleChoices.destroy({ + where: { ID_ADMIN_EXERCISE: id }, + transaction, + }); + } else if (questionType === "MPQ") { + await models.MatchingPairs.destroy({ + where: { ID_ADMIN_EXERCISE: id }, + transaction, + }); + } else if (questionType === "TFQ") { + await models.TrueFalse.destroy({ + where: { ID_ADMIN_EXERCISE: id }, + transaction, + }); + } + + await exercise.destroy({ transaction }); + + await transaction.commit(); + + response(200, null, "Exercise and related data deleted successfully", res); + } catch (error) { + console.log(error); + await transaction.rollback(); + response(500, null, "Internal Server Error", res); + } +}; + +export const deleteExerciseFileById = async (req, res) => { + const { id } = req.params; + const { fileType } = req.body; + + if (!["audio", "video", "image"].includes(fileType)) { + return response(400, null, "Invalid file type specified", res); + } + + try { + const exercise = await models.Exercise.findByPk(id); + + if (!exercise) { + return response(404, null, "Exercise not found", res); + } + + let filePath; + let fileName; + + if (fileType === "video" && exercise.VIDEO) { + fileName = exercise.VIDEO; + filePath = path.join("public/uploads/exercise/video", fileName); + exercise.VIDEO = null; + } else if (fileType === "audio" && exercise.AUDIO) { + fileName = exercise.AUDIO; + filePath = path.join("public/uploads/exercise/audio", fileName); + exercise.AUDIO = null; + } else if (fileType === "image" && exercise.IMAGE) { + fileName = exercise.IMAGE; + filePath = path.join("public/uploads/exercise/image", fileName); + exercise.IMAGE = null; + } else { + return response( + 404, + null, + `${ + fileType.charAt(0).toUpperCase() + fileType.slice(1) + } file not found`, + res + ); + } + + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + } + + await exercise.save(); + + response( + 200, + exercise, + `${ + fileType.charAt(0).toUpperCase() + fileType.slice(1) + } file deleted successfully`, + res + ); + } catch (error) { + console.log(error); + response(500, null, "Internal Server Error", res); + } +}; diff --git a/controllers/contentControllers/level.js b/controllers/contentControllers/level.js new file mode 100644 index 0000000..23148f6 --- /dev/null +++ b/controllers/contentControllers/level.js @@ -0,0 +1,332 @@ +import response from "../../response.js"; +import models from "../../models/index.js"; +import { + clearFileBuffers, + saveFileToDisk, +} from "../../middlewares/Level/uploadLevel.js"; +import fs from "fs"; +import path from "path"; +import { + updateOtherLevelsRoutes, + updateOtherLevelsRoutesOnDelete, +} from "../../middlewares/Level/checkLevel.js"; + +export const getLevels = async (req, res) => { + try { + const levels = await models.Level.findAll({ + attributes: { + exclude: [ + "ROUTE_1", + "ROUTE_2", + "ROUTE_3", + "ROUTE_4", + "ROUTE_5", + "ROUTE_6", + ], + }, + }); + + response(200, levels, "Success", res); + } catch (error) { + console.log(error); + res.status(500).json({ message: "Internal Server Error" }); + } +}; + +export const getLevelById = async (req, res) => { + try { + const { id } = req.params; + const level = await models.Level.findByPk(id, { + attributes: { + exclude: [ + "ROUTE_1", + "ROUTE_2", + "ROUTE_3", + "ROUTE_4", + "ROUTE_5", + "ROUTE_6", + ], + }, + }); + + if (!level) { + return response(404, null, "Level not found", res); + } + + response(200, level, "Success", res); + } catch (error) { + console.log(error); + res.status(500).json({ message: "Internal Server Error" }); + } +}; + +export const createLevel = async (req, res, next) => { + const { NAME_LEVEL, ID_SECTION, ID_TOPIC, CONTENT } = req.body; + const { video, image, audio } = req.filesToSave || {}; + + if (!NAME_LEVEL) { + clearFileBuffers({ video, image, audio }); + return response(400, null, "Level name is required", res); + } + + if (!ID_SECTION) { + clearFileBuffers({ video, image, audio }); + return response(400, null, "Section is required", res); + } + + if (!ID_TOPIC) { + clearFileBuffers({ video, image, audio }); + return response(400, null, "Topic is required", res); + } + + try { + const sectionWithTopic = await models.Topic.findOne({ + where: { ID_SECTION, ID_TOPIC }, + }); + + if (!sectionWithTopic) { + clearFileBuffers({ video, image, audio }); + return response( + 400, + null, + "Topic does not relate to the provided Section!", + res + ); + } + + const existingLevel = await models.Level.findOne({ + where: { NAME_LEVEL, ID_TOPIC }, + }); + + if (existingLevel) { + clearFileBuffers({ video, image, audio }); + return response( + 409, + null, + "A level with this name already exists under this topic", + res + ); + } + + const newLevel = await models.Level.create({ + NAME_LEVEL, + ID_SECTION, + ID_TOPIC, + IS_PRETEST: req.body.IS_PRETEST || 0, + CONTENT, + VIDEO: null, + AUDIO: null, + IMAGE: null, + ROUTE_1: req.body.ROUTE_1, + ROUTE_2: req.body.ROUTE_2, + ROUTE_3: req.body.ROUTE_3, + ROUTE_4: req.body.ROUTE_4, + ROUTE_5: req.body.ROUTE_5, + ROUTE_6: req.body.ROUTE_6, + }); + + req.body.newLevelId = newLevel.ID_LEVEL; + + const videoFilename = video + ? saveFileToDisk(video, "video", ID_TOPIC, ID_SECTION, newLevel.ID_LEVEL) + : null; + const audioFilename = audio + ? saveFileToDisk(audio, "audio", ID_TOPIC, ID_SECTION, newLevel.ID_LEVEL) + : null; + const imageFilename = image + ? saveFileToDisk(image, "image", ID_TOPIC, ID_SECTION, newLevel.ID_LEVEL) + : null; + + newLevel.VIDEO = videoFilename; + newLevel.AUDIO = audioFilename; + newLevel.IMAGE = imageFilename; + await newLevel.save(); + + await updateOtherLevelsRoutes(req, res, next); + + response(201, newLevel, "Level created successfully", res); + } catch (error) { + console.log(error); + clearFileBuffers({ video, image, audio }); + return response(500, null, "Internal Server Error", res); + } +}; + +export const updateLevelById = async (req, res, next) => { + const { id } = req.params; + const { NAME_LEVEL, ID_SECTION, ID_TOPIC, CONTENT } = req.body; + + const { video, image, audio } = req.filesToSave || {}; + + try { + const level = await models.Level.findByPk(id); + + if (!level) { + clearFileBuffers({ video, image, audio }); + return response(404, null, "Level not found", res); + } + + const sectionWithTopic = await models.Topic.findOne({ + where: { ID_SECTION, ID_TOPIC }, + }); + + if (!sectionWithTopic) { + clearFileBuffers({ video, image, audio }); + return response( + 400, + null, + "Topic does not relate to the provided Section", + res + ); + } + + if (NAME_LEVEL && ID_TOPIC) { + const existingLevel = await models.Level.findOne({ + where: { + NAME_LEVEL, + ID_TOPIC, + ID_LEVEL: { [models.Sequelize.Op.ne]: id }, + }, + }); + + if (existingLevel) { + clearFileBuffers({ video, image, audio }); + return response( + 409, + null, + "A level with this name already exists under this topic", + res + ); + } + } + + if (NAME_LEVEL) { + level.NAME_LEVEL = NAME_LEVEL; + if (NAME_LEVEL === "Level 1") { + level.IS_PRETEST = 1; + } else { + level.IS_PRETEST = 0; + } + } + if (ID_SECTION) level.ID_SECTION = ID_SECTION; + if (ID_TOPIC) level.ID_TOPIC = ID_TOPIC; + if (CONTENT) level.CONTENT = CONTENT; + + if (video) { + if (level.VIDEO) { + const oldVideoPath = path.join( + "public/uploads/level/video", + level.VIDEO + ); + if (fs.existsSync(oldVideoPath)) { + fs.unlinkSync(oldVideoPath); + } + } + level.VIDEO = saveFileToDisk( + video, + "video", + ID_TOPIC || level.ID_TOPIC, + ID_SECTION || level.ID_SECTION, + level.ID_LEVEL + ); + } + + if (audio) { + if (level.AUDIO) { + const oldAudioPath = path.join( + "public/uploads/level/audio", + level.AUDIO + ); + if (fs.existsSync(oldAudioPath)) { + fs.unlinkSync(oldAudioPath); + } + } + level.AUDIO = saveFileToDisk( + audio, + "audio", + ID_TOPIC || level.ID_TOPIC, + ID_SECTION || level.ID_SECTION, + level.ID_LEVEL + ); + } + + if (image) { + if (level.IMAGE) { + const oldImagePath = path.join( + "public/uploads/level/image", + level.IMAGE + ); + if (fs.existsSync(oldImagePath)) { + fs.unlinkSync(oldImagePath); + } + } + level.IMAGE = saveFileToDisk( + image, + "image", + ID_TOPIC || level.ID_TOPIC, + ID_SECTION || level.ID_SECTION, + level.ID_LEVEL + ); + } + + await level.save(); + + req.body.newLevelId = level.ID_LEVEL; + + await updateOtherLevelsRoutes(req, res, next); + + response(200, level, "Level updated successfully", res); + } catch (error) { + console.log(error); + clearFileBuffers({ video, image, audio }); + return response(500, null, "Internal Server Error", res); + } +}; + +export const deleteLevelById = async (req, res, next) => { + const { id } = req.params; + + try { + const level = await models.Level.findByPk(id); + + if (!level) { + return response(404, null, "Level not found", res); + } + + const deleteFile = (filePath) => { + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + } + }; + + if (level.VIDEO) { + const videoPath = path.join("public/uploads/level/video", level.VIDEO); + deleteFile(videoPath); + } + + if (level.AUDIO) { + const audioPath = path.join("public/uploads/level/audio", level.AUDIO); + deleteFile(audioPath); + } + + if (level.IMAGE) { + const imagePath = path.join("public/uploads/level/image", level.IMAGE); + deleteFile(imagePath); + } + + req.body.newLevelId = level.ID_LEVEL; + + await level.destroy(); + + await updateOtherLevelsRoutesOnDelete(req, res, next); + + response(200, null, "Level deleted successfully", res); + } catch (error) { + console.log(error); + return response(500, null, "Internal Server Error", res); + } +}; + +export const unlockPreviousRoutes = async (req, res) => { + const { NEXT_LEARNING } = req.params; +}; diff --git a/controllers/contentControllers/section.js b/controllers/contentControllers/section.js new file mode 100644 index 0000000..5f2938e --- /dev/null +++ b/controllers/contentControllers/section.js @@ -0,0 +1,144 @@ +import response from "../../response.js"; +import models from "../../models/index.js"; +import fs from "fs"; +import path from "path"; +import { + clearFileBuffers, + saveFileToDisk, +} from "../../middlewares/uploadSection.js"; + +export const getSections = async (req, res) => { + try { + const sections = await models.Section.findAll(); + response(200, sections, "Success", res); + } catch (error) { + console.log(error); + response(500, null, "Error retrieving sections data!", res); + } +}; + +export const getSectionById = async (req, res) => { + try { + const { id } = req.params; + const section = await models.Section.findByPk(id); + + if (!section) { + return response(404, null, "Section not found", res); + } + + response(200, section, "Success", res); + } catch (error) { + console.log(error); + response(500, null, "Internal Server Error", res); + } +}; + +export const createSection = async (req, res) => { + const { NAME_SECTION, DESCRIPTION_SECTION } = req.body; + + const { thumbnail } = req.filesToSave || {}; + + if (!NAME_SECTION) { + clearFileBuffers({ thumbnail }); + return response(400, null, "Section name is required", res); + } + + if (!DESCRIPTION_SECTION) { + clearFileBuffers({ thumbnail }); + return response(400, null, "Description is required", res); + } + + try { + const newSection = await models.Section.create({ + NAME_SECTION, + DESCRIPTION_SECTION, + THUMBNAIL: null, + }); + + const thumbnailFilename = thumbnail + ? saveFileToDisk(thumbnail, "thumbnail", newSection.ID_SECTION) + : null; + + newSection.THUMBNAIL = thumbnailFilename; + await newSection.save(); + + response(201, newSection, "Section created successfully", res); + } catch (error) { + console.log(error); + clearFileBuffers({ thumbnail }); + response(500, null, "Internal Server Error", res); + } +}; + +export const updateSectionById = async (req, res) => { + const { id } = req.params; + const { NAME_SECTION, DESCRIPTION_SECTION } = req.body; + + const { thumbnail } = req.filesToSave || {}; + + try { + const section = await models.Section.findByPk(id); + + if (!section) { + clearFileBuffers({ thumbnail }); + return response(404, null, "Section not found", res); + } + + if (NAME_SECTION) section.NAME_SECTION = NAME_SECTION; + if (DESCRIPTION_SECTION) section.DESCRIPTION_SECTION = DESCRIPTION_SECTION; + + if (thumbnail) { + if (section.THUMBNAIL) { + const oldThumbnailPath = path.join( + "public/uploads/section", + section.THUMBNAIL + ); + if (fs.existsSync(oldThumbnailPath)) { + fs.unlinkSync(oldThumbnailPath); + } + } + section.THUMBNAIL = saveFileToDisk( + thumbnail, + "thumbnail", + section.ID_SECTION + ); + } + + await section.save(); + + response(200, section, "Section updated successfully", res); + } catch (error) { + console.log(error); + clearFileBuffers({ thumbnail }); + response(500, null, "Internal Server Error", res); + } +}; + +export const deleteSectionById = async (req, res) => { + const { id } = req.params; + + try { + const section = await models.Section.findByPk(id); + + if (!section) { + return response(404, null, "Section not found", res); + } + + if (section.THUMBNAIL) { + const thumbnailPath = path.join( + "public/uploads/section", + section.THUMBNAIL + ); + if (fs.existsSync(thumbnailPath)) { + fs.unlinkSync(thumbnailPath); + } + } + + await section.destroy(); + + response(200, null, "Section deleted successfully", res); + } catch (error) { + console.log(error); + response(500, null, "Internal Server Error", res); + } +}; diff --git a/controllers/topic.js b/controllers/contentControllers/topic.js similarity index 60% rename from controllers/topic.js rename to controllers/contentControllers/topic.js index 1d0aa73..9e032e8 100644 --- a/controllers/topic.js +++ b/controllers/contentControllers/topic.js @@ -1,5 +1,5 @@ -import response from "../response.js"; -import models from "../models/index.js"; +import response from "../../response.js"; +import models from "../../models/index.js"; export const getTopics = async (req, res) => { try { @@ -23,33 +23,35 @@ export const getTopicById = async (req, res) => { response(200, topic, "Success", res); } catch (error) { console.log(error); - res.status(500).json({ message: "Internal Server Error" }); + response(500, null, "Internal Server Error", res); } }; export const createTopic = async (req, res) => { - const { subject_id, title } = req.body; + const { ID_SECTION, NAME_TOPIC, DESCRIPTION_TOPIC } = req.body; - // Validate subject_id - if (!subject_id) { - return response(400, null, "Subject ID is required", res); + if (!ID_SECTION) { + return response(400, null, "Section ID is required", res); } - // Validate title - if (!title) { - return response(400, null, "Title is required", res); + if (!NAME_TOPIC) { + return response(400, null, "Topic name is required", res); + } + + if (!DESCRIPTION_TOPIC) { + return response(400, null, "Topic Description is required", res); } try { - // Verify that the subject_id exists in the m_subjects table - const subject = await models.Subject.findByPk(subject_id); - if (!subject) { - return response(404, null, "Subject not found", res); + const section = await models.Section.findByPk(ID_SECTION); + if (!section) { + return response(404, null, "Section not found", res); } const newTopic = await models.Topic.create({ - subject_id, - title, + ID_SECTION, + NAME_TOPIC, + DESCRIPTION_TOPIC, }); response(201, newTopic, "Topic created successfully", res); @@ -61,31 +63,31 @@ export const createTopic = async (req, res) => { export const updateTopicById = async (req, res) => { const { id } = req.params; - const { subject_id, title } = req.body; + const { ID_SECTION, NAME_TOPIC, DESCRIPTION_TOPIC } = req.body; try { - // Find the topic by its ID const topic = await models.Topic.findByPk(id); if (!topic) { return response(404, null, "Topic not found", res); } - // Validate and update subject_id if provided - if (subject_id) { - const subject = await models.Subject.findByPk(subject_id); - if (!subject) { - return response(404, null, "Subject not found", res); + if (ID_SECTION) { + const section = await models.Section.findByPk(ID_SECTION); + if (!section) { + return response(404, null, "Section not found", res); } - topic.subject_id = subject_id; + topic.ID_SECTION = ID_SECTION; } - // Validate and update title if provided - if (title) { - topic.title = title; + if (NAME_TOPIC) { + topic.NAME_TOPIC = NAME_TOPIC; + } + + if (DESCRIPTION_TOPIC) { + topic.DESCRIPTION_TOPIC = DESCRIPTION_TOPIC; } - // Save the updated topic await topic.save(); response(200, topic, "Topic updated successfully", res); @@ -99,14 +101,12 @@ export const deleteTopicById = async (req, res) => { const { id } = req.params; try { - // Find the topic by its ID const topic = await models.Topic.findByPk(id); if (!topic) { return response(404, null, "Topic not found", res); } - // Delete the topic await topic.destroy(); response(200, null, "Topic deleted successfully", res); diff --git a/controllers/exerciseTypesControllers/matchingPairs.js b/controllers/exerciseTypesControllers/matchingPairs.js new file mode 100644 index 0000000..59c4ed6 --- /dev/null +++ b/controllers/exerciseTypesControllers/matchingPairs.js @@ -0,0 +1,273 @@ +import response from "../../response.js"; +import models from "../../models/index.js"; +import { + clearFileBuffers, + saveFileToDisk, +} from "../../middlewares/uploadExercise.js"; +import fs from "fs"; +import path from "path"; + +export const createMatchingPairsExercise = async (req, res) => { + const { ID_LEVEL, TITLE, QUESTION, SCORE_WEIGHT } = req.body; + let PAIRS = req.body.PAIRS; + + try { + if (typeof PAIRS === "string") { + PAIRS = JSON.parse(PAIRS); + } + } catch (error) { + return response(400, null, "Invalid PAIRS format", res); + } + + const { video, image, audio } = req.filesToSave || {}; + + if (!ID_LEVEL) return response(400, null, "Level ID is required", res); + if (!QUESTION) return response(400, null, "Question is required", res); + if (!PAIRS || !Array.isArray(PAIRS) || PAIRS.length === 0) + return response(400, null, "At least one pair is required", res); + if (!SCORE_WEIGHT) + return response(400, null, "Score weight is required", res); + + const transaction = await models.db.transaction(); + + try { + const level = await models.Level.findByPk(ID_LEVEL); + if (!level) { + clearFileBuffers({ video, image, audio }); + return response(404, null, "Level not found", res); + } + + let generatedTitle = TITLE; + if (!TITLE) { + const exerciseCount = await models.Exercise.count({ + where: { ID_LEVEL }, + }); + generatedTitle = `Soal ${exerciseCount + 1}`; + } + + const existingExercise = await models.Exercise.findOne({ + where: { ID_LEVEL, TITLE: generatedTitle }, + }); + if (existingExercise) { + clearFileBuffers({ video, image, audio }); + return response( + 400, + null, + "An exercise with the same title already exists in this level", + res + ); + } + + const newExercise = await models.Exercise.create( + { + ID_LEVEL, + TITLE: generatedTitle, + QUESTION, + SCORE_WEIGHT, + QUESTION_TYPE: "MPQ", + AUDIO: null, + VIDEO: null, + IMAGE: null, + }, + { transaction } + ); + + const videoFilename = video + ? saveFileToDisk(video, "video", ID_LEVEL, newExercise.ID_ADMIN_EXERCISE) + : null; + const audioFilename = audio + ? saveFileToDisk(audio, "audio", ID_LEVEL, newExercise.ID_ADMIN_EXERCISE) + : null; + const imageFilename = image + ? saveFileToDisk(image, "image", ID_LEVEL, newExercise.ID_ADMIN_EXERCISE) + : null; + + newExercise.VIDEO = videoFilename; + newExercise.AUDIO = audioFilename; + newExercise.IMAGE = imageFilename; + await newExercise.save({ transaction }); + + const matchingPairsPromises = PAIRS.map((pair) => + models.MatchingPairs.create( + { + ID_ADMIN_EXERCISE: newExercise.ID_ADMIN_EXERCISE, + LEFT_PAIR: pair.LEFT_PAIR, + RIGHT_PAIR: pair.RIGHT_PAIR, + }, + { transaction } + ) + ); + + const newMatchingPairs = await Promise.all(matchingPairsPromises); + + await transaction.commit(); + + const payload = { + exercise: newExercise, + matchingPairs: newMatchingPairs, + }; + + response(201, payload, "Matching Pairs Exercise created successfully", res); + } catch (error) { + console.error(error); + await transaction.rollback(); + clearFileBuffers({ video, image, audio }); + response(500, null, "Internal Server Error", res); + } +}; + +export const updateMatchingPairsExerciseById = async (req, res) => { + const { id } = req.params; + const { ID_LEVEL, QUESTION, SCORE_WEIGHT } = req.body; + let PAIRS = req.body.PAIRS; + + try { + if (typeof PAIRS === "string") { + PAIRS = JSON.parse(PAIRS); + } + } catch (error) { + return response(400, null, "Invalid PAIRS format", res); + } + + const { video, image, audio } = req.filesToSave || {}; + + const transaction = await models.db.transaction(); + + try { + const exercise = await models.Exercise.findByPk(id, { transaction }); + if (!exercise) { + clearFileBuffers({ video, image, audio }); + await transaction.rollback(); + return response(404, null, "Exercise not found", res); + } + + if (ID_LEVEL) { + const level = await models.Level.findByPk(ID_LEVEL, { transaction }); + if (!level) { + clearFileBuffers({ video, image, audio }); + await transaction.rollback(); + return response(404, null, "Level not found", res); + } + exercise.ID_LEVEL = ID_LEVEL; + } + + if (QUESTION) exercise.QUESTION = QUESTION; + if (SCORE_WEIGHT) exercise.SCORE_WEIGHT = SCORE_WEIGHT; + + if (video) { + if (exercise.VIDEO) { + const oldVideoPath = path.join( + "public/uploads/exercise/video", + exercise.VIDEO + ); + if (fs.existsSync(oldVideoPath)) { + fs.unlinkSync(oldVideoPath); + } + } + exercise.VIDEO = saveFileToDisk( + video, + "video", + ID_LEVEL || exercise.ID_LEVEL, + exercise.ID_ADMIN_EXERCISE + ); + } + + if (audio) { + if (exercise.AUDIO) { + const oldAudioPath = path.join( + "public/uploads/exercise/audio", + exercise.AUDIO + ); + if (fs.existsSync(oldAudioPath)) { + fs.unlinkSync(oldAudioPath); + } + } + exercise.AUDIO = saveFileToDisk( + audio, + "audio", + ID_LEVEL || exercise.ID_LEVEL, + exercise.ID_ADMIN_EXERCISE + ); + } + + if (image) { + if (exercise.IMAGE) { + const oldImagePath = path.join( + "public/uploads/exercise/image", + exercise.IMAGE + ); + if (fs.existsSync(oldImagePath)) { + fs.unlinkSync(oldImagePath); + } + } + exercise.IMAGE = saveFileToDisk( + image, + "image", + ID_LEVEL || exercise.ID_LEVEL, + exercise.ID_ADMIN_EXERCISE + ); + } + + await exercise.save({ transaction }); + + if (PAIRS && Array.isArray(PAIRS)) { + const existingPairs = await models.MatchingPairs.findAll({ + where: { ID_ADMIN_EXERCISE: exercise.ID_ADMIN_EXERCISE }, + transaction, + }); + + const pairsToDelete = new Set( + existingPairs.map((pair) => pair.ID_MATCHING_PAIRS) + ); + + for (const pair of PAIRS) { + if (pair.ID_MATCHING_PAIRS) { + const existingPair = existingPairs.find( + (p) => p.ID_MATCHING_PAIRS === pair.ID_MATCHING_PAIRS + ); + if (existingPair) { + existingPair.LEFT_PAIR = pair.LEFT_PAIR; + existingPair.RIGHT_PAIR = pair.RIGHT_PAIR; + await existingPair.save({ transaction }); + + pairsToDelete.delete(existingPair.ID_MATCHING_PAIRS); + } + } else { + await models.MatchingPairs.create( + { + ID_ADMIN_EXERCISE: exercise.ID_ADMIN_EXERCISE, + LEFT_PAIR: pair.LEFT_PAIR, + RIGHT_PAIR: pair.RIGHT_PAIR, + }, + { transaction } + ); + } + } + + for (const pairId of pairsToDelete) { + await models.MatchingPairs.destroy({ + where: { ID_MATCHING_PAIRS: pairId }, + transaction, + }); + } + } + + await transaction.commit(); + + const updatedPairs = await models.MatchingPairs.findAll({ + where: { ID_ADMIN_EXERCISE: exercise.ID_ADMIN_EXERCISE }, + }); + + const payload = { + exercise, + matchingPairs: updatedPairs, + }; + + response(200, payload, "Matching Pairs Exercise updated successfully", res); + } catch (error) { + console.error(error); + await transaction.rollback(); + clearFileBuffers({ video, image, audio }); + response(500, null, "Internal Server Error", res); + } +}; diff --git a/controllers/exerciseTypesControllers/multipleChoices.js b/controllers/exerciseTypesControllers/multipleChoices.js new file mode 100644 index 0000000..2ef2729 --- /dev/null +++ b/controllers/exerciseTypesControllers/multipleChoices.js @@ -0,0 +1,255 @@ +import response from "../../response.js"; +import models from "../../models/index.js"; +import { + clearFileBuffers, + saveFileToDisk, +} from "../../middlewares/uploadExercise.js"; +import fs from "fs"; +import path from "path"; + +export const createMultipleChoicesExercise = async (req, res) => { + const { + ID_LEVEL, + TITLE, + QUESTION, + OPTION_A, + OPTION_B, + OPTION_C, + OPTION_D, + OPTION_E, + ANSWER_KEY, + SCORE_WEIGHT, + } = req.body; + + const { video, image, audio } = req.filesToSave || {}; + + if (!ID_LEVEL) return response(400, null, "Level ID is required", res); + if (!QUESTION) return response(400, null, "Question is required", res); + if (!OPTION_A) return response(400, null, "Option A is required", res); + if (!OPTION_B) return response(400, null, "Option B is required", res); + if (!OPTION_C) return response(400, null, "Option C is required", res); + if (!OPTION_D) return response(400, null, "Option D is required", res); + if (!OPTION_E) return response(400, null, "Option E is required", res); + if (!ANSWER_KEY) return response(400, null, "Answer key is required", res); + if (!SCORE_WEIGHT) + return response(400, null, "Score weight is required", res); + + const transaction = await models.db.transaction(); + + try { + const level = await models.Level.findByPk(ID_LEVEL); + if (!level) { + clearFileBuffers({ video, image, audio }); + return response(404, null, "Level not found", res); + } + + let generatedTitle = TITLE; + if (!TITLE) { + const exerciseCount = await models.Exercise.count({ + where: { ID_LEVEL }, + }); + generatedTitle = `Soal ${exerciseCount + 1}`; + } + + const existingExercise = await models.Exercise.findOne({ + where: { ID_LEVEL, TITLE: generatedTitle }, + }); + if (existingExercise) { + clearFileBuffers({ video, image, audio }); + return response( + 400, + null, + "An exercise with the same title already exists in this level", + res + ); + } + + const newExercise = await models.Exercise.create( + { + ID_LEVEL, + TITLE: generatedTitle, + QUESTION, + SCORE_WEIGHT, + QUESTION_TYPE: "MCQ", + AUDIO: null, + VIDEO: null, + IMAGE: null, + }, + { transaction } + ); + + const videoFilename = video + ? saveFileToDisk(video, "video", ID_LEVEL, newExercise.ID_ADMIN_EXERCISE) + : null; + const audioFilename = audio + ? saveFileToDisk(audio, "audio", ID_LEVEL, newExercise.ID_ADMIN_EXERCISE) + : null; + const imageFilename = image + ? saveFileToDisk(image, "image", ID_LEVEL, newExercise.ID_ADMIN_EXERCISE) + : null; + + newExercise.VIDEO = videoFilename; + newExercise.AUDIO = audioFilename; + newExercise.IMAGE = imageFilename; + await newExercise.save({ transaction }); + + const newMultipleChoices = await models.MultipleChoices.create( + { + ID_ADMIN_EXERCISE: newExercise.ID_ADMIN_EXERCISE, + OPTION_A, + OPTION_B, + OPTION_C, + OPTION_D, + OPTION_E, + ANSWER_KEY, + }, + { transaction } + ); + + await transaction.commit(); + + const payload = { + exercise: newExercise, + multipleChoices: newMultipleChoices, + }; + + response(201, payload, "Exercise created successfully", res); + } catch (error) { + console.error(error); + await transaction.rollback(); + clearFileBuffers({ video, image, audio }); + response(500, null, "Internal Server Error", res); + } +}; + +export const updateMultipleChoicesExerciseById = async (req, res) => { + const { id } = req.params; + const { + ID_LEVEL, + QUESTION, + OPTION_A, + OPTION_B, + OPTION_C, + OPTION_D, + OPTION_E, + ANSWER_KEY, + SCORE_WEIGHT, + } = req.body; + + const { video, image, audio } = req.filesToSave || {}; + + const transaction = await models.db.transaction(); + + try { + const exercise = await models.Exercise.findByPk(id, { transaction }); + + if (!exercise) { + clearFileBuffers({ video, image, audio }); + await transaction.rollback(); + return response(404, null, "Exercise not found", res); + } + + if (ID_LEVEL) { + const level = await models.Level.findByPk(ID_LEVEL, { transaction }); + if (!level) { + clearFileBuffers({ video, image, audio }); + await transaction.rollback(); + return response(404, null, "Level not found", res); + } + exercise.ID_LEVEL = ID_LEVEL; + } + + if (QUESTION) exercise.QUESTION = QUESTION; + if (SCORE_WEIGHT) exercise.SCORE_WEIGHT = SCORE_WEIGHT; + + if (video) { + if (exercise.VIDEO) { + const oldVideoPath = path.join( + "public/uploads/exercise/video", + exercise.VIDEO + ); + if (fs.existsSync(oldVideoPath)) { + fs.unlinkSync(oldVideoPath); + } + } + exercise.VIDEO = saveFileToDisk( + video, + "video", + ID_LEVEL || exercise.ID_LEVEL, + exercise.ID_ADMIN_EXERCISE + ); + } + + if (audio) { + if (exercise.AUDIO) { + const oldAudioPath = path.join( + "public/uploads/exercise/audio", + exercise.AUDIO + ); + if (fs.existsSync(oldAudioPath)) { + fs.unlinkSync(oldAudioPath); + } + } + exercise.AUDIO = saveFileToDisk( + audio, + "audio", + ID_LEVEL || exercise.ID_LEVEL, + exercise.ID_ADMIN_EXERCISE + ); + } + + if (image) { + if (exercise.IMAGE) { + const oldImagePath = path.join( + "public/uploads/exercise/image", + exercise.IMAGE + ); + if (fs.existsSync(oldImagePath)) { + fs.unlinkSync(oldImagePath); + } + } + exercise.IMAGE = saveFileToDisk( + image, + "image", + ID_LEVEL || exercise.ID_LEVEL, + exercise.ID_ADMIN_EXERCISE + ); + } + + await exercise.save({ transaction }); + + const multipleChoices = await models.MultipleChoices.findOne({ + where: { ID_ADMIN_EXERCISE: exercise.ID_ADMIN_EXERCISE }, + transaction, + }); + + if (!multipleChoices) { + clearFileBuffers({ video, image, audio }); + await transaction.rollback(); + return response(404, null, "Multiple Choices not found", res); + } + + if (OPTION_A) multipleChoices.OPTION_A = OPTION_A; + if (OPTION_B) multipleChoices.OPTION_B = OPTION_B; + if (OPTION_C) multipleChoices.OPTION_C = OPTION_C; + if (OPTION_D) multipleChoices.OPTION_D = OPTION_D; + if (OPTION_E) multipleChoices.OPTION_E = OPTION_E; + if (ANSWER_KEY) multipleChoices.ANSWER_KEY = ANSWER_KEY; + + await multipleChoices.save({ transaction }); + + await transaction.commit(); + + const payload = { + exercise, + multipleChoices, + }; + + response(200, payload, "Exercise updated successfully", res); + } catch (error) { + console.log(error); + clearFileBuffers({ video, image, audio }); + await transaction.rollback(); + response(500, null, "Internal Server Error", res); + } +}; diff --git a/controllers/exerciseTypesControllers/trueFalse.js b/controllers/exerciseTypesControllers/trueFalse.js new file mode 100644 index 0000000..5549640 --- /dev/null +++ b/controllers/exerciseTypesControllers/trueFalse.js @@ -0,0 +1,222 @@ +import response from "../../response.js"; +import models from "../../models/index.js"; +import { + clearFileBuffers, + saveFileToDisk, +} from "../../middlewares/uploadExercise.js"; +import fs from "fs"; +import path from "path"; + +export const createTrueFalseExercise = async (req, res) => { + const { ID_LEVEL, TITLE, QUESTION, IS_TRUE, SCORE_WEIGHT } = req.body; + + const { video, image, audio } = req.filesToSave || {}; + + if (!ID_LEVEL) return response(400, null, "Level ID is required", res); + if (!QUESTION) return response(400, null, "Question is required", res); + if (typeof IS_TRUE === "undefined") + return response(400, null, "IS_TRUE is required", res); + if (!SCORE_WEIGHT) + return response(400, null, "Score weight is required", res); + + const transaction = await models.db.transaction(); + + try { + const level = await models.Level.findByPk(ID_LEVEL); + if (!level) { + clearFileBuffers({ video, image, audio }); + return response(404, null, "Level not found", res); + } + + let generatedTitle = TITLE; + if (!TITLE) { + const exerciseCount = await models.Exercise.count({ + where: { ID_LEVEL }, + }); + generatedTitle = `Soal ${exerciseCount + 1}`; + } + + const existingExercise = await models.Exercise.findOne({ + where: { ID_LEVEL, TITLE: generatedTitle }, + }); + if (existingExercise) { + clearFileBuffers({ video, image, audio }); + return response( + 400, + null, + "An exercise with the same title already exists in this level", + res + ); + } + + const newExercise = await models.Exercise.create( + { + ID_LEVEL, + TITLE: generatedTitle, + QUESTION, + SCORE_WEIGHT, + QUESTION_TYPE: "TFQ", + AUDIO: null, + VIDEO: null, + IMAGE: null, + }, + { transaction } + ); + + const videoFilename = video + ? saveFileToDisk(video, "video", ID_LEVEL, newExercise.ID_ADMIN_EXERCISE) + : null; + const audioFilename = audio + ? saveFileToDisk(audio, "audio", ID_LEVEL, newExercise.ID_ADMIN_EXERCISE) + : null; + const imageFilename = image + ? saveFileToDisk(image, "image", ID_LEVEL, newExercise.ID_ADMIN_EXERCISE) + : null; + + newExercise.VIDEO = videoFilename; + newExercise.AUDIO = audioFilename; + newExercise.IMAGE = imageFilename; + await newExercise.save({ transaction }); + + const newTrueFalse = await models.TrueFalse.create( + { + ID_ADMIN_EXERCISE: newExercise.ID_ADMIN_EXERCISE, + IS_TRUE, + }, + { transaction } + ); + + await transaction.commit(); + + const payload = { + exercise: newExercise, + trueFalse: newTrueFalse, + }; + + response(201, payload, "True/False exercise created successfully", res); + } catch (error) { + console.error(error); + await transaction.rollback(); + clearFileBuffers({ video, image, audio }); + response(500, null, "Internal Server Error", res); + } +}; + +export const updateTrueFalseExerciseById = async (req, res) => { + const { id } = req.params; + const { ID_LEVEL, QUESTION, IS_TRUE, SCORE_WEIGHT } = req.body; + + const { video, image, audio } = req.filesToSave || {}; + + const transaction = await models.db.transaction(); + + try { + const exercise = await models.Exercise.findByPk(id, { transaction }); + + if (!exercise) { + clearFileBuffers({ video, image, audio }); + await transaction.rollback(); + return response(404, null, "Exercise not found", res); + } + + if (ID_LEVEL) { + const level = await models.Level.findByPk(ID_LEVEL, { transaction }); + if (!level) { + clearFileBuffers({ video, image, audio }); + await transaction.rollback(); + return response(404, null, "Level not found", res); + } + exercise.ID_LEVEL = ID_LEVEL; + } + + if (QUESTION) exercise.QUESTION = QUESTION; + if (SCORE_WEIGHT) exercise.SCORE_WEIGHT = SCORE_WEIGHT; + + if (video) { + if (exercise.VIDEO) { + const oldVideoPath = path.join( + "public/uploads/exercise/video", + exercise.VIDEO + ); + if (fs.existsSync(oldVideoPath)) { + fs.unlinkSync(oldVideoPath); + } + } + exercise.VIDEO = saveFileToDisk( + video, + "video", + ID_LEVEL || exercise.ID_LEVEL, + exercise.ID_ADMIN_EXERCISE + ); + } + + if (audio) { + if (exercise.AUDIO) { + const oldAudioPath = path.join( + "public/uploads/exercise/audio", + exercise.AUDIO + ); + if (fs.existsSync(oldAudioPath)) { + fs.unlinkSync(oldAudioPath); + } + } + exercise.AUDIO = saveFileToDisk( + audio, + "audio", + ID_LEVEL || exercise.ID_LEVEL, + exercise.ID_ADMIN_EXERCISE + ); + } + + if (image) { + if (exercise.IMAGE) { + const oldImagePath = path.join( + "public/uploads/exercise/image", + exercise.IMAGE + ); + if (fs.existsSync(oldImagePath)) { + fs.unlinkSync(oldImagePath); + } + } + exercise.IMAGE = saveFileToDisk( + image, + "image", + ID_LEVEL || exercise.ID_LEVEL, + exercise.ID_ADMIN_EXERCISE + ); + } + + await exercise.save({ transaction }); + + const trueFalse = await models.TrueFalse.findOne({ + where: { ID_ADMIN_EXERCISE: exercise.ID_ADMIN_EXERCISE }, + transaction, + }); + + if (!trueFalse) { + clearFileBuffers({ video, image, audio }); + await transaction.rollback(); + return response(404, null, "True/False exercise not found", res); + } + + if (typeof IS_TRUE !== "undefined") { + trueFalse.IS_TRUE = IS_TRUE; + } + + await trueFalse.save({ transaction }); + + await transaction.commit(); + + const payload = { + exercise, + trueFalse, + }; + + response(200, payload, "True/False exercise updated successfully", res); + } catch (error) { + console.error(error); + clearFileBuffers({ video, image, audio }); + await transaction.rollback(); + response(500, null, "Internal Server Error", res); + } +}; diff --git a/controllers/learningControllers/stdExercise.js b/controllers/learningControllers/stdExercise.js new file mode 100644 index 0000000..67dbaf3 --- /dev/null +++ b/controllers/learningControllers/stdExercise.js @@ -0,0 +1,93 @@ +import response from "../../response.js"; +import models from "../../models/index.js"; + +export const getStdExercises = async (req, res) => { + try { + const stdExercise = await models.StdExercise.findAll(); + response(200, stdExercise, "Success", res); + } catch (error) { + console.log(error); + response(500, null, "Error retrieving student exercise data!", res); + } +}; + +export const getStdExerciseById = async (req, res) => { + try { + const { id } = req.params; + const stdExercise = await models.StdExercise.findByPk(id); + + if (!stdExercise) { + return response(404, null, "Student exercise data not found", res); + } + + response(200, stdExercise, "Success", res); + } catch (error) { + console.log(error); + res.status(500).json({ message: "Internal Server Error" }); + } +}; + +export const stdAnswerExercise = async (req, res, next) => { + try { + const { ID_STUDENT_LEARNING, ID_ADMIN_EXERCISE, ANSWER_STUDENT } = req.body; + + if (!ID_STUDENT_LEARNING) { + return response(400, null, "Id student learning is required", res); + } + + const existingStdLearning = await models.StdLearning.findByPk( + ID_STUDENT_LEARNING + ); + if (!existingStdLearning) { + return response(404, null, "Id student learning not found", res); + } + + if (!ID_ADMIN_EXERCISE) { + return response(400, null, "Id exercise is required", res); + } + + const exercise = await models.Exercise.findOne({ + where: { + ID_ADMIN_EXERCISE: ID_ADMIN_EXERCISE, + ID_LEVEL: existingStdLearning.ID_LEVEL, + }, + }); + if (!exercise) { + return response(404, null, "Exercise not found in this level", res); + } + + if (!ANSWER_STUDENT) { + return response(400, null, "Answer is required", res); + } + + const existingStdExercise = await models.StdExercise.findOne({ + where: { + ID_STUDENT_LEARNING: ID_STUDENT_LEARNING, + ID_ADMIN_EXERCISE: ID_ADMIN_EXERCISE, + }, + }); + + if (existingStdExercise) { + existingStdExercise.ANSWER_STUDENT = ANSWER_STUDENT; + + await existingStdExercise.save(); + } else { + await models.StdExercise.create({ + ID_STUDENT_LEARNING, + ID_ADMIN_EXERCISE, + ANSWER_STUDENT, + }); + } + + req.params.id = ID_STUDENT_LEARNING; + next(); + } catch (error) { + console.log(error); + return response( + 500, + null, + "Error creating or updating student exercise data!", + res + ); + } +}; diff --git a/controllers/learningControllers/stdLearning.js b/controllers/learningControllers/stdLearning.js new file mode 100644 index 0000000..431317a --- /dev/null +++ b/controllers/learningControllers/stdLearning.js @@ -0,0 +1,358 @@ +import response from "../../response.js"; +import models from "../../models/index.js"; +import { createMonitoring } from "../monitoringControllers/monitoring.js"; + +export const getStdLearnings = async (req, res) => { + try { + const stdLearning = await models.StdLearning.findAll(); + response(200, stdLearning, "Success", res); + } catch (error) { + console.log(error); + response(500, null, "Error retrieving student learning data!", res); + } +}; + +export const getStdLearningById = async (req, res) => { + try { + const { id } = req.params; + const stdLearning = await models.StdLearning.findByPk(id); + + if (!stdLearning) { + return response(404, null, "Student learning data not found", res); + } + + response(200, stdLearning, "Success", res); + } catch (error) { + response(500, null, "Internal Server Error", res); + } +}; + +export const createStdLearning = async (req, res) => { + const { ID_LEVEL } = req.body; + + if (!req.user) { + return response(401, null, "User not authenticated", res); + } + + if (!ID_LEVEL) { + return response(400, null, "Level ID is required", res); + } + + const ID = req.user.ID; + + try { + const level = await models.Level.findByPk(ID_LEVEL); + if (!level) { + return response(404, null, "Level not found", res); + } + + const newStdLearning = await models.StdLearning.create({ + ID, + ID_LEVEL, + }); + + response( + 201, + newStdLearning, + "Student Learning data created successfully", + res + ); + } catch (error) { + console.log(error); + response(500, null, "Internal Server Error", res); + } +}; + +export const updateStdLearningById = async (req, res) => { + const { id } = req.params; + const { FEEDBACK_STUDENT } = req.body; + + try { + const stdLearning = await models.StdLearning.findByPk(id, { + include: [ + { + model: models.Level, + as: "level", + }, + ], + }); + + if (!stdLearning) { + return response(404, null, "Student Learning record not found", res); + } + + stdLearning.STUDENT_FINISH = new Date(); + stdLearning.SCORE = req.body.SCORE; + stdLearning.NEXT_LEARNING = req.body.NEXT_LEARNING; + stdLearning.IS_PASS = req.body.IS_PASS; + + if (FEEDBACK_STUDENT) { + stdLearning.FEEDBACK_STUDENT = FEEDBACK_STUDENT; + } + + await stdLearning.save(); + + const level6 = await models.Level.findOne({ + where: { + NAME_LEVEL: "Level 6", + ID_TOPIC: stdLearning.level.ID_TOPIC, + }, + }); + + if (!level6) { + return response(404, null, "Level 6 not found", res); + } + + if ( + stdLearning.level.ID_LEVEL === level6.ID_LEVEL && + stdLearning.IS_PASS === 1 + ) { + req.body.ID_STUDENT_LEARNING = id; + + const existingMonitoring = await models.Monitoring.findOne({ + where: { ID_STUDENT_LEARNING: id }, + }); + + if (!existingMonitoring) { + const newMonitoring = await createMonitoring(req); + + const { level, ...responseData } = stdLearning.toJSON(); + + const combinedPayload = { + ...responseData, + MONITORING: newMonitoring, + }; + + return response( + 200, + combinedPayload, + "Student Learning record updated and monitoring created successfully", + res + ); + } + } else { + const monitoringToDelete = await models.Monitoring.findOne({ + where: { ID_STUDENT_LEARNING: id }, + }); + + if (monitoringToDelete) { + await monitoringToDelete.destroy(); + } + } + + const { level, ...responseData } = stdLearning.toJSON(); + + response( + 200, + responseData, + "Student Learning record updated successfully", + res + ); + } catch (error) { + console.log(error); + response(500, null, "Internal Server Error", res); + } +}; + +export const learningHistory = async (req, res) => { + try { + if (!req.user) { + return response(401, null, "User not authenticated", res); + } + + const { ID } = req.user; + + const stdLearnings = await models.StdLearning.findAll({ + where: { + ID: ID, + }, + include: [ + { + model: models.Level, + as: "level", + include: [ + { + model: models.Topic, + as: "levelTopic", + include: [ + { + model: models.Section, + as: "topicSection", + }, + ], + }, + ], + }, + ], + }); + + const result = await Promise.all( + stdLearnings.map(async (learning) => { + let nextLevelName = null; + if (learning.NEXT_LEARNING) { + const nextLevel = await models.Level.findOne({ + where: { ID_LEVEL: learning.NEXT_LEARNING }, + }); + if (nextLevel) { + nextLevelName = nextLevel.NAME_LEVEL; + } + } + + return { + SCORE: learning.SCORE, + CURRENT_LEVEL: learning.level.NAME_LEVEL, + NEXT_LEVEL: nextLevelName, + STUDENT_FINISH: learning.STUDENT_FINISH, + TOPIC_NAME: learning.level.levelTopic.NAME_TOPIC, + SECTION_NAME: learning.level.levelTopic.topicSection.NAME_SECTION, + }; + }) + ); + + response(200, result, "Success", res); + } catch (error) { + console.log(error); + response(500, null, "Internal Server Error", res); + } +}; + +export const learningHistoryBySectionId = async (req, res) => { + try { + if (!req.user) { + return response(401, null, "User not authenticated", res); + } + + const { ID } = req.user; + const { sectionId } = req.params; + + const stdLearnings = await models.StdLearning.findAll({ + where: { + ID: ID, + }, + include: [ + { + model: models.Level, + as: "level", + include: [ + { + model: models.Topic, + as: "levelTopic", + include: [ + { + model: models.Section, + as: "topicSection", + where: { ID_SECTION: sectionId }, + }, + ], + }, + ], + }, + ], + }); + + if (!stdLearnings.length) { + return response( + 404, + null, + "No learning history found for the specified section", + res + ); + } + + const result = await Promise.all( + stdLearnings.map(async (learning) => { + let nextLevelName = null; + if (learning.NEXT_LEARNING) { + const nextLevel = await models.Level.findOne({ + where: { ID_LEVEL: learning.NEXT_LEARNING }, + }); + if (nextLevel) { + nextLevelName = nextLevel.NAME_LEVEL; + } + } + + return { + SCORE: learning.SCORE, + CURRENT_LEVEL: learning.level.NAME_LEVEL, + NEXT_LEVEL: nextLevelName, + STUDENT_FINISH: learning.STUDENT_FINISH, + TOPIC_NAME: learning.level.levelTopic.NAME_TOPIC, + SECTION_NAME: learning.level.levelTopic.topicSection.NAME_SECTION, + }; + }) + ); + + response(200, result, "Success", res); + } catch (error) { + console.log(error); + response(500, null, "Internal Server Error", res); + } +}; + +export const learningHistoryByTopicId = async (req, res) => { + try { + if (!req.user) { + return response(401, null, "User not authenticated", res); + } + + const { ID } = req.user; + const { topicId } = req.params; + + if (!topicId) { + return response(400, null, "Topic ID is required", res); + } + + const stdLearnings = await models.StdLearning.findAll({ + where: { + ID: ID, + }, + include: [ + { + model: models.Level, + as: "level", + include: [ + { + model: models.Topic, + as: "levelTopic", + where: { ID_TOPIC: topicId }, + include: [ + { + model: models.Section, + as: "topicSection", + }, + ], + }, + ], + }, + ], + }); + + const result = await Promise.all( + stdLearnings.map(async (learning) => { + let nextLevelName = null; + if (learning.NEXT_LEARNING) { + const nextLevel = await models.Level.findOne({ + where: { ID_LEVEL: learning.NEXT_LEARNING }, + }); + if (nextLevel) { + nextLevelName = nextLevel.NAME_LEVEL; + } + } + + return { + SCORE: learning.SCORE, + CURRENT_LEVEL: learning.level.NAME_LEVEL, + NEXT_LEVEL: nextLevelName, + STUDENT_FINISH: learning.STUDENT_FINISH, + TOPIC_NAME: learning.level.levelTopic.NAME_TOPIC, + SECTION_NAME: learning.level.levelTopic.topicSection.NAME_SECTION, + }; + }) + ); + + response(200, result, "Success", res); + } catch (error) { + console.error(error); + response(500, null, "Internal Server Error", res); + } +}; diff --git a/controllers/level.js b/controllers/level.js deleted file mode 100644 index 2eeae64..0000000 --- a/controllers/level.js +++ /dev/null @@ -1,397 +0,0 @@ -import response from "../response.js"; -import models from "../models/index.js"; -import { - clearFileBuffers, - saveFileToDisk, - generateHash, -} from "../middlewares/uploadLevel.js"; -import fs from "fs"; -import path from "path"; -import crypto from "crypto"; - -export const getAllLevels = async (req, res) => { - try { - const levels = await models.Level.findAll(); - response(200, levels, "Success", res); - } catch (error) { - console.log(error); - response(500, null, "Error retrieving levels data!", res); - } -}; - -export const getAllLevelById = async (req, res) => { - try { - const { id } = req.params; - const level = await models.Level.findByPk(id); - - if (!level) { - return response(404, null, "Level not found", res); - } - - response(200, level, "Success", res); - } catch (error) { - console.log(error); - res.status(500).json({ message: "Internal Server Error" }); - } -}; - -export const getLevels = async (req, res) => { - try { - const levels = await models.Level.findAll({ - attributes: { - exclude: ["route1", "route2", "route3", "route4"], - }, - }); - - response(200, levels, "Success", res); - } catch (error) { - console.log(error); - res.status(500).json({ message: "Internal Server Error" }); - } -}; - -export const getLevelById = async (req, res) => { - try { - const { id } = req.params; - const level = await models.Level.findByPk(id, { - attributes: { - exclude: ["route1", "route2", "route3", "route4"], - }, - }); - - if (!level) { - return response(404, null, "Level not found", res); - } - - response(200, level, "Success", res); - } catch (error) { - console.log(error); - res.status(500).json({ message: "Internal Server Error" }); - } -}; - -export const createLevel = async (req, res) => { - const { title, subject_id, topic_id, is_pretest, content, youtube } = - req.body; - - // Files to be saved if everything else is okay - const { video, audio, image } = req.filesToSave || {}; - - // Validate title - if (!title) { - clearFileBuffers({ video, audio, image }); - return response(400, null, "Title is required", res); - } - - // Validate subject_id - if (!subject_id) { - clearFileBuffers({ video, audio, image }); - return response(400, null, "Subject ID is required", res); - } - - // Validate topic_id - if (!topic_id) { - clearFileBuffers({ video, audio, image }); - return response(400, null, "Topic ID is required", res); - } - - try { - // Check if the title already exists under the same topic_id - const existingLevel = await models.Level.findOne({ - where: { title, topic_id }, - }); - - if (existingLevel) { - clearFileBuffers({ video, audio, image }); - return response( - 409, - null, - "A level with this title already exists under this topic", - res - ); // 409 Conflict - } - - // Save files to disk - const videoFilename = video - ? saveFileToDisk(video, "video", title, topic_id, subject_id) - : null; - const audioFilename = audio - ? saveFileToDisk(audio, "audio", title, topic_id, subject_id) - : null; - const imageFilename = image - ? saveFileToDisk(image, "image", title, topic_id, subject_id) - : null; - - // Create the new level - const newLevel = await models.Level.create({ - title, - subject_id, - topic_id, - is_pretest: is_pretest || 0, - content, - video: videoFilename, - audio: audioFilename, - image: imageFilename, - youtube, - route1: 0, - route2: 0, - route3: 0, - route4: 0, - }); - - // Update routes with the newly created level's ID - await newLevel.update({ - route1: newLevel.id, - route2: newLevel.id, - route3: newLevel.id, - route4: newLevel.id, - }); - - response(201, newLevel, "Level created successfully", res); - } catch (error) { - console.log(error); - clearFileBuffers({ video, audio, image }); - return response(500, null, "Internal Server Error", res); - } -}; - -export const updateLevelById = async (req, res) => { - const { id } = req.params; - const { title, subject_id, topic_id, is_pretest, content, youtube } = - req.body; - - // Files to be saved if everything else is okay - const { video, audio, image } = req.filesToSave || {}; - - try { - // Find the existing level by ID - const level = await models.Level.findByPk(id); - - if (!level) { - clearFileBuffers({ video, audio, image }); - return response(404, null, "Level not found", res); - } - - // Check if a level with the same title under the same topic already exists - if (title && topic_id) { - const existingLevel = await models.Level.findOne({ - where: { - title, - topic_id, - id: { [models.Sequelize.Op.ne]: id }, // Exclude the current level from the check - }, - }); - - if (existingLevel) { - clearFileBuffers({ video, audio, image }); - return response( - 409, - null, - "A level with this title already exists under this topic", - res - ); // 409 Conflict - } - } - - // Update level fields - if (title) level.title = title; - if (subject_id) level.subject_id = subject_id; - if (topic_id) level.topic_id = topic_id; - if (is_pretest !== undefined) level.is_pretest = is_pretest; - if (content) level.content = content; - if (youtube) level.youtube = youtube; - - // Handle video update - if (video) { - if (level.video) { - const oldVideoPath = path.join( - "public/uploads/level/video", - level.video - ); - if (fs.existsSync(oldVideoPath)) { - fs.unlinkSync(oldVideoPath); - } - } - level.video = saveFileToDisk( - video, - "video", - title || level.title, - topic_id || level.topic_id, - subject_id || level.subject_id - ); - } - - // Handle audio update - if (audio) { - if (level.audio) { - const oldAudioPath = path.join( - "public/uploads/level/audio", - level.audio - ); - if (fs.existsSync(oldAudioPath)) { - fs.unlinkSync(oldAudioPath); - } - } - level.audio = saveFileToDisk( - audio, - "audio", - title || level.title, - topic_id || level.topic_id, - subject_id || level.subject_id - ); - } - - // Handle image update - if (image) { - if (level.image) { - const oldImagePath = path.join( - "public/uploads/level/image", - level.image - ); - if (fs.existsSync(oldImagePath)) { - fs.unlinkSync(oldImagePath); - } - } - level.image = saveFileToDisk( - image, - "image", - title || level.title, - topic_id || level.topic_id, - subject_id || level.subject_id - ); - } - - await level.save(); - - response(200, level, "Level updated successfully", res); - } catch (error) { - console.log(error); - clearFileBuffers({ video, audio, image }); - return response(500, null, "Internal Server Error", res); - } -}; - -export const deleteLevelById = async (req, res) => { - const { id } = req.params; - - try { - // Find the existing level by ID - const level = await models.Level.findByPk(id); - - if (!level) { - return response(404, null, "Level not found", res); - } - - // Delete associated files from disk if they exist - const deleteFile = (filePath) => { - if (fs.existsSync(filePath)) { - fs.unlinkSync(filePath); - } - }; - - if (level.video) { - const videoPath = path.join("public/uploads/level/video", level.video); - deleteFile(videoPath); - } - - if (level.audio) { - const audioPath = path.join("public/uploads/level/audio", level.audio); - deleteFile(audioPath); - } - - if (level.image) { - const imagePath = path.join("public/uploads/level/image", level.image); - deleteFile(imagePath); - } - - // Delete the level from the database - await level.destroy(); - - response(200, null, "Level deleted successfully", res); - } catch (error) { - console.log(error); - return response(500, null, "Internal Server Error", res); - } -}; - -export const getRoutes = async (req, res) => { - try { - const levels = await models.Level.findAll({ - attributes: { - exclude: [ - "subject_id", - "topic_id", - "is_pretest", - "content", - "video", - "audio", - "image", - "youtube", - "ts_entri", - ], - }, - }); - - response(200, levels, "Success", res); - } catch (error) { - console.log(error); - res.status(500).json({ message: "Internal Server Error" }); - } -}; - -export const getRouteById = async (req, res) => { - try { - const { id } = req.params; - const level = await models.Level.findByPk(id, { - attributes: { - exclude: [ - "subject_id", - "topic_id", - "is_pretest", - "content", - "video", - "audio", - "image", - "youtube", - "ts_entri", - ], - }, - }); - - if (!level) { - return response(404, null, "Level not found", res); - } - - response(200, level, "Success", res); - } catch (error) { - console.log(error); - res.status(500).json({ message: "Internal Server Error" }); - } -}; - -export const updateRouteById = async (req, res) => { - const { id } = req.params; - const { route1, route2, route3, route4 } = req.body; - - try { - // Find the existing level by ID - const level = await models.Level.findByPk(id); - - if (!level) { - return response(404, null, "Level not found", res); - } - - // Update only the route fields - await level.update({ - route1: route1 !== undefined ? route1 : level.route1, - route2: route2 !== undefined ? route2 : level.route2, - route3: route3 !== undefined ? route3 : level.route3, - route4: route4 !== undefined ? route4 : level.route4, - }); - - response(200, level, "Routes updated successfully", res); - } catch (error) { - console.log(error); - return response(500, null, "Internal Server Error", res); - } -}; diff --git a/controllers/monitoringControllers/class.js b/controllers/monitoringControllers/class.js new file mode 100644 index 0000000..1f42547 --- /dev/null +++ b/controllers/monitoringControllers/class.js @@ -0,0 +1,231 @@ +import response from "../../response.js"; +import models from "../../models/index.js"; +import { updateMonitoringClass } from "./monitoring.js"; + +export const getClasses = async (req, res) => { + try { + const classes = await models.Class.findAll(); + response(200, classes, "Success", res); + } catch (error) { + console.log(error); + response(500, null, "Error retrieving classes data!", res); + } +}; + +export const getClassById = async (req, res) => { + try { + const { id } = req.params; + const classes = await models.Class.findByPk(id); + + if (!classes) { + return response(404, null, "Class not found", res); + } + + response(200, classes, "Success", res); + } catch (error) { + console.log(error); + response(500, null, "Internal Server Error", res); + } +}; + +export const createClass = async (req, res) => { + const { NAME_CLASS, TOTAL_STUDENT } = req.body; + + if (!NAME_CLASS) { + return response(400, null, "Class name is required", res); + } + + if (!TOTAL_STUDENT) { + return response(400, null, "Total student is required", res); + } + + try { + const newClass = await models.Class.create({ + NAME_CLASS, + TOTAL_STUDENT, + }); + + response(201, newClass, "Class created successfully", res); + } catch (error) { + console.log(error); + response(500, null, "Internal Server Error", res); + } +}; + +export const updateClassById = async (req, res) => { + const { id } = req.params; + const { NAME_CLASS, TOTAL_STUDENT } = req.body; + + try { + const classes = await models.Class.findByPk(id); + + if (!classes) { + return response(404, null, "Class not found", res); + } + + if (NAME_CLASS) classes.NAME_CLASS = NAME_CLASS; + if (TOTAL_STUDENT) classes.TOTAL_STUDENT = TOTAL_STUDENT; + + await classes.save(); + + response(200, classes, "Class updated successfully", res); + } catch (error) { + console.log(error); + response(500, null, "Internal Server Error", res); + } +}; + +export const deleteClassById = async (req, res) => { + const { id } = req.params; + + try { + const classes = await models.Class.findByPk(id); + + if (!classes) { + return response(404, null, "Class not found", res); + } + + await classes.destroy(); + + response(200, null, "Class deleted successfully", res); + } catch (error) { + console.log(error); + response(500, null, "Internal Server Error", res); + } +}; + +export const updateStudentClassByName = async (req, res) => { + const { NAME_CLASS, STUDENTS } = req.body; + + if (!NAME_CLASS || !Array.isArray(STUDENTS) || STUDENTS.length === 0) { + return response( + 400, + null, + "Class name and students data are required", + res + ); + } + + try { + const classRecord = await models.Class.findOne({ + where: { NAME_CLASS }, + }); + + if (!classRecord) { + return response(404, null, "Class not found", res); + } + + const updateResults = []; + let hasError = false; + + for (const { NAME_USERS, NISN } of STUDENTS) { + if (!NAME_USERS || !NISN) { + updateResults.push({ + NAME_USERS, + NISN, + error: "User name and NISN are required for each student", + }); + hasError = true; + continue; + } + + try { + const student = await models.Student.findOne({ + include: [ + { + model: models.User, + as: "studentUser", + where: { NAME_USERS }, + }, + ], + where: { NISN }, + }); + + if (!student) { + updateResults.push({ + NAME_USERS, + NISN, + error: "Student with the given name and NISN not found", + }); + hasError = true; + continue; + } + + if (student.ID_CLASS === classRecord.ID_CLASS) { + updateResults.push({ + NAME_USERS, + NISN, + error: "Student is already in the selected class", + }); + hasError = true; + continue; + } + + student.ID_CLASS = classRecord.ID_CLASS; + await student.save(); + + const stdLearningRecords = await models.StdLearning.findAll({ + where: { ID: student.ID }, + include: [ + { + model: models.Level, + as: "level", + where: { NAME_LEVEL: "Level 6" }, + }, + ], + }); + + const studentUpdateResults = []; + + for (const stdLearning of stdLearningRecords) { + try { + const result = await updateMonitoringClass({ + ID_STUDENT_LEARNING: stdLearning.ID_STUDENT_LEARNING, + ID_CLASS: classRecord.ID_CLASS, + }); + studentUpdateResults.push(result); + } catch (error) { + console.error("Error updating monitoring class:", error.message); + studentUpdateResults.push({ error: error.message }); + } + } + + updateResults.push({ + NAME_USERS, + NISN, + message: + "Student's class and related monitoring updated successfully", + studentUpdateResults, + }); + } catch (error) { + console.error("Error processing student:", error.message); + updateResults.push({ + NAME_USERS, + NISN, + error: "Error processing student", + details: error.message, + }); + hasError = true; + } + } + + if (hasError) { + return response( + 400, + { updateResults }, + "Some students could not be updated due to errors", + res + ); + } else { + return response( + 200, + { updateResults }, + "Students classes updated successfully", + res + ); + } + } catch (error) { + console.error(error); + response(500, null, "Internal Server Error", res); + } +}; diff --git a/controllers/monitoringControllers/monitoring.js b/controllers/monitoringControllers/monitoring.js new file mode 100644 index 0000000..2c319e9 --- /dev/null +++ b/controllers/monitoringControllers/monitoring.js @@ -0,0 +1,347 @@ +import response from "../../response.js"; +import models from "../../models/index.js"; + +export const getMonitorings = async (req, res) => { + try { + const monitoring = await models.Monitoring.findAll(); + response(200, monitoring, "Success", res); + } catch (error) { + console.log(error); + response(500, null, "Error retrieving monitoring data!", res); + } +}; + +export const getMonitoringById = async (req, res) => { + try { + const { id } = req.params; + const monitoring = await models.Monitoring.findByPk(id); + + if (!monitoring) { + return response(404, null, "Monitoring data not found", res); + } + + response(200, monitoring, "Success", res); + } catch (error) { + console.log(error); + response(500, null, "Internal Server Error", res); + } +}; + +export const createMonitoring = async (req) => { + const { ID_STUDENT_LEARNING } = req.body; + + if (!req.user) { + throw new Error("User not authenticated"); + } + + if (!ID_STUDENT_LEARNING) { + throw new Error("Student Learning ID is required"); + } + + try { + const existingMonitoring = await models.Monitoring.findOne({ + where: { ID_STUDENT_LEARNING: ID_STUDENT_LEARNING }, + }); + + if (existingMonitoring) { + return existingMonitoring.toJSON(); + } + + const stdLearning = await models.StdLearning.findByPk(ID_STUDENT_LEARNING); + + if (!stdLearning) { + throw new Error("Student learning data not found"); + } + + const userID = stdLearning.ID; + + const student = await models.Student.findOne({ where: { ID: userID } }); + + if (!student) { + throw new Error("Student data not found"); + } + + const { ID_SISWA } = student; + + const studentClass = await models.Student.findOne({ + where: { ID_SISWA: ID_SISWA }, + attributes: ["ID_CLASS"], + }); + + const ID_CLASS = + studentClass && studentClass.ID_CLASS ? studentClass.ID_CLASS : null; + + const newMonitoring = await models.Monitoring.create({ + ID_STUDENT_LEARNING, + ID_CLASS, + }); + + return newMonitoring.toJSON(); + } catch (error) { + console.error(error); + throw new Error("Internal Server Error"); + } +}; + +export const updateMonitoringClass = async ({ + ID_STUDENT_LEARNING, + ID_CLASS, +}) => { + if (!ID_STUDENT_LEARNING || !ID_CLASS) { + throw new Error("Student Learning ID and Class ID are required"); + } + + try { + const monitoring = await models.Monitoring.findOne({ + where: { ID_STUDENT_LEARNING }, + }); + + if (!monitoring) { + throw new Error("Monitoring data not found"); + } + + monitoring.ID_CLASS = ID_CLASS; + await monitoring.save(); + + return monitoring; + } catch (error) { + console.error(error); + throw error; + } +}; + +export const monitoringStudentsProgress = async (req, res) => { + try { + const result = await models.Monitoring.findAll({ + include: [ + { + model: models.StdLearning, + as: "stdLearningMonitoring", + include: [ + { + model: models.User, + as: "learningUser", + attributes: ["NAME_USERS"], + include: [ + { + model: models.Student, + as: "students", + attributes: ["NISN"], + include: [ + { + model: models.Class, + as: "studentClass", + attributes: ["NAME_CLASS"], + }, + ], + }, + ], + }, + { + model: models.Level, + as: "level", + attributes: ["NAME_LEVEL"], + include: [ + { + model: models.Topic, + as: "levelTopic", + attributes: ["NAME_TOPIC"], + include: [ + { + model: models.Section, + as: "topicSection", + attributes: ["NAME_SECTION"], + }, + ], + }, + ], + }, + ], + }, + ], + }); + + const formattedResult = result.map((monitoring) => { + return { + ID_MONITORING: monitoring.ID_MONITORING, + NISN: monitoring.stdLearningMonitoring?.learningUser?.students?.NISN, + NAME_USERS: monitoring.stdLearningMonitoring?.learningUser?.NAME_USERS, + NAME_SECTION: + monitoring.stdLearningMonitoring?.level?.levelTopic?.topicSection + ?.NAME_SECTION, + NAME_TOPIC: + monitoring.stdLearningMonitoring?.level?.levelTopic?.NAME_TOPIC, + NAME_CLASS: + monitoring.stdLearningMonitoring?.learningUser?.students?.studentClass + ?.NAME_CLASS ?? null, + }; + }); + + response( + 200, + formattedResult, + "Success retrieving student monitoring progress!", + res + ); + } catch (error) { + console.error(error); + response(500, null, "Error retrieving student monitoring progress!", res); + } +}; + +export const monitoringStudentProgressById = async (req, res) => { + const { id } = req.params; + + try { + const monitoring = await models.Monitoring.findOne({ + where: { ID_MONITORING: id }, + include: [ + { + model: models.StdLearning, + as: "stdLearningMonitoring", + attributes: [ + "ID", + "ID_LEVEL", + "SCORE", + "FEEDBACK_STUDENT", + "STUDENT_START", + "STUDENT_FINISH", + "ID_STUDENT_LEARNING", + ], + include: [ + { + model: models.Level, + as: "level", + attributes: ["ID_TOPIC", "NAME_LEVEL"], + }, + ], + }, + ], + }); + + if (!monitoring) { + return response(404, null, "Monitoring data not found!", res); + } + + const stdLearning = monitoring.stdLearningMonitoring; + + if (!stdLearning || stdLearning.length === 0) { + return response(404, null, "No student learning data found!", res); + } + + const { ID } = stdLearning; + + const levels = await models.StdLearning.findAll({ + where: { + ID, + }, + include: [ + { + model: models.Level, + as: "level", + attributes: ["NAME_LEVEL", "ID_TOPIC"], + }, + ], + attributes: [ + "SCORE", + "FEEDBACK_STUDENT", + "STUDENT_START", + "STUDENT_FINISH", + ], + }); + + const levelArray = levels.map((learning) => ({ + NAME_LEVEL: learning.level.NAME_LEVEL, + SCORE: learning.SCORE, + FEEDBACK_STUDENT: learning.FEEDBACK_STUDENT, + STUDENT_START: learning.STUDENT_START, + STUDENT_FINISH: learning.STUDENT_FINISH, + })); + + const result = { + ID_MONITORING: monitoring.ID_MONITORING, + levels: levelArray, + }; + + response(200, result, "Success retrieving student progress!", res); + } catch (error) { + console.error(error); + response(500, null, "Error retrieving student progress!", res); + } +}; + +export const monitoringFeedback = async (req, res) => { + const { id } = req.params; + const { feedback } = req.body; + + if (!req.user) { + return response(401, null, "User not authenticated", res); + } + + const { ID } = req.user; + + if (!ID) { + return response(401, null, "Unauthorized: User ID not provided", res); + } + + try { + const teacher = await models.Teacher.findOne({ + where: { ID: ID }, + attributes: ["ID_GURU"], + }); + + if (!teacher) { + return response(404, null, "Teacher not found!", res); + } + + const [updatedRows] = await models.Monitoring.update( + { + ID_GURU: teacher.ID_GURU, + FEEDBACK_GURU: feedback, + }, + { + where: { ID_MONITORING: id }, + returning: true, + } + ); + + if (updatedRows === 0) { + return response(404, null, "Monitoring data not found!", res); + } + + const monitoringWithRelations = await models.Monitoring.findOne({ + where: { ID_MONITORING: id }, + include: [ + { + model: models.Teacher, + as: "monitoringTeacher", + attributes: ["ID_GURU"], + include: [ + { + model: models.User, + as: "teacherUser", + attributes: ["NAME_USERS"], + }, + ], + }, + ], + }); + + if (!monitoringWithRelations) { + return response(404, null, "Updated monitoring data not found!", res); + } + + const result = { + ID_MONITORING: monitoringWithRelations.ID_MONITORING, + FEEDBACK_GURU: monitoringWithRelations.FEEDBACK_GURU, + ID_GURU: monitoringWithRelations.monitoringTeacher?.ID_GURU, + TEACHER_NAME: + monitoringWithRelations.monitoringTeacher?.teacherUser?.NAME_USERS, + }; + + response(200, result, "Success updating teacher feedback!", res); + } catch (error) { + console.error("Error in monitoringFeedback:", error); + response(500, null, "Error updating teacher feedback!", res); + } +}; diff --git a/controllers/subject.js b/controllers/subject.js deleted file mode 100644 index 102be1e..0000000 --- a/controllers/subject.js +++ /dev/null @@ -1,167 +0,0 @@ -import response from "../response.js"; -import models from "../models/index.js"; -import fs from "fs"; -import path from "path"; -import { - clearFileBuffers, - saveFileToDisk, -} from "../middlewares/uploadSubject.js"; - -export const getSubjects = async (req, res) => { - try { - const subjects = await models.Subject.findAll(); - response(200, subjects, "Success", res); - } catch (error) { - console.log(error); - response(500, null, "Error retrieving subjects data!", res); - } -}; - -export const getSubjectById = async (req, res) => { - try { - const { id } = req.params; - const subject = await models.Subject.findByPk(id); - - if (!subject) { - return response(404, null, "Subject not found", res); - } - - response(200, subject, "Success", res); - } catch (error) { - console.log(error); - res.status(500).json({ message: "Internal Server Error" }); - } -}; - -export const createSubject = async (req, res) => { - const { name, description } = req.body; - - // Files to be saved if everything else is okay - const { icon, thumbnail } = req.filesToSave || {}; - - // Validate name - if (!name) { - clearFileBuffers({ icon, thumbnail }); - return response(400, null, "Name is required", res); - } - - // Validate description - if (!description) { - clearFileBuffers({ icon, thumbnail }); - return response(400, null, "Description is required", res); - } - - try { - const iconFilename = icon - ? saveFileToDisk(icon, `${name}-icon`, name) - : null; - const thumbnailFilename = thumbnail - ? saveFileToDisk(thumbnail, `${name}-thumbnail`, name) - : null; - - const newSubject = await models.Subject.create({ - name, - description, - icon: iconFilename, - thumbnail: thumbnailFilename, - }); - - response(201, newSubject, "Subject created successfully", res); - } catch (error) { - console.log(error); - clearFileBuffers({ icon, thumbnail }); - res.status(500).json({ message: "Internal Server Error" }); - } -}; - -export const updateSubjectById = async (req, res) => { - const { id } = req.params; - const { name, description } = req.body; - - // Files to be saved if everything else is okay - const { icon, thumbnail } = req.filesToSave || {}; - - try { - const subject = await models.Subject.findByPk(id); - - if (!subject) { - clearFileBuffers({ icon, thumbnail }); - return response(404, null, "Subject not found", res); - } - - // Update subject fields - if (name) subject.name = name; - if (description) subject.description = description; - - // Handle icon update - if (icon) { - if (subject.icon) { - const oldIconPath = path.join("public/uploads/subject", subject.icon); - if (fs.existsSync(oldIconPath)) { - fs.unlinkSync(oldIconPath); - } - } - subject.icon = saveFileToDisk(icon, `${name}-icon`, name); - } - - // Handle thumbnail update - if (thumbnail) { - if (subject.thumbnail) { - const oldThumbnailPath = path.join( - "public/uploads/subject", - subject.thumbnail - ); - if (fs.existsSync(oldThumbnailPath)) { - fs.unlinkSync(oldThumbnailPath); - } - } - subject.thumbnail = saveFileToDisk(thumbnail, `${name}-thumbnail`, name); - } - - await subject.save(); - - response(200, subject, "Subject updated successfully", res); - } catch (error) { - console.log(error); - clearFileBuffers({ icon, thumbnail }); - response(500, null, "Internal Server Error", res); - } -}; - -export const deleteSubjectById = async (req, res) => { - const { id } = req.params; - - try { - const subject = await models.Subject.findByPk(id); - - if (!subject) { - return response(404, null, "Subject not found", res); - } - - // Remove associated icon if it exists - if (subject.icon) { - const iconPath = path.join("public/uploads/subject", subject.icon); - if (fs.existsSync(iconPath)) { - fs.unlinkSync(iconPath); - } - } - - // Remove associated thumbnail if it exists - if (subject.thumbnail) { - const thumbnailPath = path.join( - "public/uploads/subject", - subject.thumbnail - ); - if (fs.existsSync(thumbnailPath)) { - fs.unlinkSync(thumbnailPath); - } - } - - await subject.destroy(); - - response(200, null, "Subject deleted successfully", res); - } catch (error) { - console.log(error); - response(500, null, "Internal Server Error", res); - } -}; diff --git a/controllers/user.js b/controllers/user.js deleted file mode 100644 index eb64407..0000000 --- a/controllers/user.js +++ /dev/null @@ -1,102 +0,0 @@ -import response from "../response.js"; -import models from "../models/index.js"; -import bcrypt from "bcrypt"; - -export const getUsers = async (req, res) => { - try { - const users = await models.User.findAll({ - attributes: { - exclude: ["password"], - }, - }); - response(200, users, "Success", res); - } catch (error) { - console.log(error); - response(500, null, "Error retrieving users data!", res); - } -}; - -export const getUserById = async (req, res) => { - try { - const { id } = req.params; - const user = await models.User.findByPk(id, { - attributes: { - exclude: ["password"], - }, - }); - - if (!user) { - return response(404, null, "User not found", res); - } - - response(200, user, "Success", res); - } catch (error) { - console.log(error); - res.status(500).json({ message: "Internal Server Error" }); - } -}; - -export const updateUserById = async (req, res) => { - try { - const { id } = req.params; - const { name, email, password, roles } = req.body; - - // Find the user by ID - const user = await models.User.findByPk(id); - - if (!user) { - return response(404, null, "User not found", res); - } - - // Check if the email is unique if it is being updated - if (email && email !== user.email) { - const emailExists = await models.User.findOne({ where: { email } }); - if (emailExists) { - return response(400, null, "Email already in use", res); - } - user.email = email; - } - - // Hash the password if it is being updated - if (password) { - const salt = await bcrypt.genSalt(10); - user.password = await bcrypt.hash(password, salt); - } - - // Update other user information - user.name = name || user.name; - user.roles = roles || user.roles; - - // Manually update the updated_at field - user.updated_at = new Date(); - - // Save the updated user information - await user.save(); - - response(200, user, "User updated successfully", res); - } catch (error) { - console.log(error); - res.status(500).json({ message: "Internal Server Error" }); - } -}; - -export const deleteUserById = async (req, res) => { - try { - const { id } = req.params; - - // Find the user by ID - const user = await models.User.findByPk(id); - - if (!user) { - return response(404, null, "User not found", res); - } - - // Delete the user - await user.destroy(); - - response(200, null, "User deleted successfully", res); - } catch (error) { - console.log(error); - res.status(500).json({ message: "Internal Server Error" }); - } -}; \ No newline at end of file diff --git a/controllers/usersControllers/user.js b/controllers/usersControllers/user.js new file mode 100644 index 0000000..d5ffa91 --- /dev/null +++ b/controllers/usersControllers/user.js @@ -0,0 +1,473 @@ +import response from "../../response.js"; +import models from "../../models/index.js"; +import bcrypt from "bcrypt"; +import fs from "fs"; +import path from "path"; +import { + clearFileBuffers, + saveFileToDisk, +} from "../../middlewares/User/uploadUser.js"; + +export const getUsers = async (req, res) => { + try { + const users = await models.User.findAll({ + attributes: { + exclude: ["PASSWORD"], + include: [ + [ + models.Sequelize.literal( + `COALESCE(\`teachers\`.\`NIP\`, \`students\`.\`NISN\`)` + ), + "NIP/NISN", + ], + ], + }, + include: [ + { + model: models.Teacher, + as: "teachers", + attributes: [], + }, + { + model: models.Student, + as: "students", + attributes: [], + }, + ], + }); + response(200, users, "Success", res); + } catch (error) { + console.log(error); + response(500, null, "Error retrieving users data!", res); + } +}; + +export const getAdmins = async (req, res) => { + try { + const admins = await models.User.findAll({ + where: { + ROLE: "admin", + }, + attributes: { + exclude: ["PASSWORD"], + }, + }); + response(200, admins, "Success", res); + } catch (error) { + console.log(error); + response(500, null, "Error retrieving admin data!", res); + } +}; + +export const getTeachers = async (req, res) => { + try { + const teachers = await models.User.findAll({ + where: { + ROLE: "teacher", + }, + attributes: ["ID", "NAME_USERS", "EMAIL", "ROLE"], + include: [ + { + model: models.Teacher, + as: "teachers", + attributes: ["NIP"], + }, + ], + raw: true, + nest: true, + }); + + const formattedTeachers = teachers.map((teacher) => ({ + ID: teacher.ID, + NAME_USERS: teacher.NAME_USERS, + EMAIL: teacher.EMAIL, + NISN: teacher.teachers.NIP, + ROLE: teacher.ROLE, + })); + + response(200, formattedTeachers, "Success", res); + } catch (error) { + console.log(error); + response(500, null, "Error retrieving teacher data!", res); + } +}; + +export const getStudents = async (req, res) => { + try { + const students = await models.User.findAll({ + where: { + ROLE: "student", + }, + attributes: ["ID", "NAME_USERS", "EMAIL", "ROLE"], + include: [ + { + model: models.Student, + as: "students", + attributes: ["NISN"], + }, + ], + raw: true, + nest: true, + }); + + const formattedStudents = students.map((student) => ({ + ID: student.ID, + NAME_USERS: student.NAME_USERS, + EMAIL: student.EMAIL, + NISN: student.students.NISN, + ROLE: student.ROLE, + })); + + response(200, formattedStudents, "Success", res); + } catch (error) { + console.log(error); + response(500, null, "Error retrieving student data!", res); + } +}; + +export const getUserById = async (req, res) => { + try { + const { id } = req.params; + + const user = await models.User.findByPk(id, { + attributes: { + exclude: ["PASSWORD"], + }, + include: [ + { + model: models.Teacher, + as: "teachers", + attributes: ["NIP"], + }, + { + model: models.Student, + as: "students", + attributes: ["NISN"], + }, + ], + }); + + if (!user) { + return response(404, null, "User not found", res); + } + + let additionalField = null; + if (user.ROLE === "teacher") { + additionalField = { NIP: user.teachers.NIP }; + } else if (user.ROLE === "student") { + additionalField = { NISN: user.students.NISN }; + } + + const responseObject = { + ID: user.ID, + NAME_USERS: user.NAME_USERS, + EMAIL: user.EMAIL, + ROLE: user.ROLE, + ...additionalField, + }; + + response(200, responseObject, "Success", res); + } catch (error) { + console.log(error); + response(500, null, "Error retrieving user data!", res); + } +}; + +export const getUserByName = async (req, res) => { + try { + const { name } = req.params; + + const user = await models.User.findOne({ + where: { NAME_USERS: name }, + attributes: { + exclude: ["PASSWORD"], + }, + include: [ + { + model: models.Teacher, + as: "teachers", + attributes: ["NIP"], + }, + { + model: models.Student, + as: "students", + attributes: ["NISN"], + }, + ], + }); + + if (!user) { + return response(404, null, "User not found", res); + } + + let additionalField = null; + if (user.ROLE === "teacher" ) { + additionalField = { NIP: user.teachers.NIP }; + } else if (user.ROLE === "student") { + additionalField = { NISN: user.students.NISN }; + } + + const responseObject = { + ID: user.ID, + NAME_USERS: user.NAME_USERS, + EMAIL: user.EMAIL, + ROLE: user.ROLE, + ...additionalField, + }; + + return response(200, responseObject, "Success", res); + } catch (error) { + console.log(error); + return response(500, null, "Error retrieving user data!", res); + } +}; + +export const updateUserById = async (req, res) => { + const transaction = await models.db.transaction(); + + const { picture } = req.filesToSave || {}; + + try { + const { id } = req.params; + const { name, email, nip, nisn } = req.body; + + const user = await models.User.findByPk(id, { + include: [ + { + model: models.Teacher, + as: "teachers", + attributes: ["NIP"], + }, + { + model: models.Student, + as: "students", + attributes: ["NISN"], + }, + ], + transaction, + }); + + if (!user) { + clearFileBuffers({ picture }); + await transaction.rollback(); + return response(404, null, "User not found", res); + } + + if (user.ROLE === "teacher" && nisn) { + clearFileBuffers({ picture }); + await transaction.rollback(); + return response(400, null, "Role is teacher, but NISN is provided", res); + } + if (user.ROLE === "student" && nip) { + clearFileBuffers({ picture }); + await transaction.rollback(); + return response(400, null, "Role is student, but NIP is provided", res); + } + + if (email && email !== user.EMAIL) { + const emailExists = await models.User.findOne({ + where: { EMAIL: email }, + transaction, + }); + if (emailExists) { + clearFileBuffers({ picture }); + await transaction.rollback(); + return response(400, null, "Email already in use", res); + } + user.EMAIL = email; + } + + user.NAME_USERS = name || user.NAME_USERS; + + if (user.ROLE === "teacher" && nip) { + let teacher = await models.Teacher.findOne({ + where: { ID: id }, + transaction, + }); + if (teacher) { + teacher.NIP = nip; + await teacher.save({ transaction }); + } else { + teacher = await models.Teacher.create( + { ID: id, NIP: nip }, + { transaction } + ); + } + } + + if (user.ROLE === "student" && nisn) { + let student = await models.Student.findOne({ + where: { ID: id }, + transaction, + }); + if (student) { + student.NISN = nisn; + await student.save({ transaction }); + } else { + student = await models.Student.create( + { ID: id, NISN: nisn }, + { transaction } + ); + } + } + + if (picture) { + if (user.PICTURE) { + const oldPicturePath = path.join("public/uploads/avatar", user.PICTURE); + if (fs.existsSync(oldPicturePath)) { + fs.unlinkSync(oldPicturePath); + } + } + user.PICTURE = saveFileToDisk(picture, user.ID, user.NAME_USERS); + } + + await user.save({ transaction }); + + await user.reload({ + include: [ + { + model: models.Teacher, + as: "teachers", + attributes: ["NIP"], + }, + { + model: models.Student, + as: "students", + attributes: ["NISN"], + }, + ], + transaction, + }); + + await transaction.commit(); + + return response(200, user, "User updated successfully", res); + } catch (error) { + clearFileBuffers({ picture }); + await transaction.rollback(); + console.log(error); + return response(500, null, "Internal Server Error", res); + } +}; + +export const updateUserPasswordById = async (req, res) => { + try { + const { id } = req.params; + const { oldPassword, password, confirmPassword } = req.body; + + if (!oldPassword || !password || !confirmPassword) { + return response(400, null, "All fields must be filled.", res); + } + + if (password !== confirmPassword) { + return response( + 400, + null, + "New password and confirm password do not match.", + res + ); + } + + const user = await models.User.findByPk(id); + if (!user) { + return response(404, null, "User not found.", res); + } + + const isMatch = await bcrypt.compare(oldPassword, user.PASSWORD); + if (!isMatch) { + return response(400, null, "Incorrect old password.", res); + } + + const salt = await bcrypt.genSalt(10); + const hashedPassword = await bcrypt.hash(password, salt); + + user.PASSWORD = hashedPassword; + await user.save(); + + response(200, null, "Password updated successfully.", res); + } catch (error) { + response(500, null, "Internal Server Error", res); + } +}; + +export const deleteUserById = async (req, res) => { + const transaction = await models.db.transaction(); + + try { + const { id } = req.params; + + const user = await models.User.findByPk(id, { transaction }); + + if (!user) { + await transaction.rollback(); + return response(404, null, "User not found", res); + } + + if (user.ROLE === "teacher") { + await models.Teacher.destroy({ where: { ID: id }, transaction }); + } else if (user.ROLE === "student") { + await models.Student.destroy({ where: { ID: id }, transaction }); + } + + await user.destroy({ transaction }); + + await transaction.commit(); + return response(200, null, "User deleted successfully", res); + } catch (error) { + await transaction.rollback(); + console.log(error); + return response(500, null, "Internal Server Error", res); + } +}; + +export const getMe = async (req, res) => { + try { + const user = req.user; // User object from verifyLoginUser middleware + + // Retrieve teacher or student details based on the user's role + const userWithDetails = await models.User.findByPk(user.ID, { + attributes: { + exclude: ["PASSWORD"], // Exclude sensitive information + }, + include: [ + { + model: models.Teacher, + as: "teachers", + attributes: ["NIP"], // Include NIP for teacher + }, + { + model: models.Student, + as: "students", + attributes: ["NISN"], // Include NISN for student + }, + ], + }); + + if (!userWithDetails) { + return response(404, null, "User not found", res); + } + + // Determine additional field based on user role + let additionalField = null; + if (userWithDetails.ROLE === "teacher") { + additionalField = { NIP: userWithDetails.teachers.NIP }; + } else if (userWithDetails.ROLE === "student") { + additionalField = { NISN: userWithDetails.students.NISN }; + } + + // Construct the response object + const responseObject = { + ID: userWithDetails.ID, + NAME_USERS: userWithDetails.NAME_USERS, + EMAIL: userWithDetails.EMAIL, + ROLE: userWithDetails.ROLE, + ...additionalField, + }; + + // Send the response + response(200, responseObject, "Success", res); + } catch (error) { + console.error(error); + response(500, null, "Error retrieving user data!", res); + } +}; \ No newline at end of file diff --git a/database/db.js b/database/db.js index 2d49933..0b3024f 100644 --- a/database/db.js +++ b/database/db.js @@ -12,6 +12,7 @@ const db = new Sequelize(name, username, password, { host: host, dialect: "mysql", logging: false, + timezone: '+07:00', }); const testConnection = async () => { diff --git a/index.js b/index.js index b9efeb5..f1ec28e 100644 --- a/index.js +++ b/index.js @@ -1,23 +1,33 @@ -import express from 'express'; -import cors from 'cors'; -import dotenv from 'dotenv'; +import express from "express"; +import cors from "cors"; +import dotenv from "dotenv"; import { testConnection } from "./database/db.js"; import router from "./routes/index.js"; -import cookieParser from 'cookie-parser'; +import cookieParser from "cookie-parser"; dotenv.config(); const app = express(); -app.use(cors()); +const corsOptions = { + origin: "*", + methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], + allowedHeaders: ["Content-Type", "Authorization"], + credentials: true, +}; + +app.use(cors(corsOptions)); + app.use(cookieParser()); app.use(express.json()); -app.use(express.urlencoded({ extended: true })); +app.use(express.urlencoded({ extended: true })); app.use(router); // Serve static files from the uploads directory -app.use(express.static('public')); +app.use(express.static("public")); app.listen(process.env.APP_PORT, () => { - testConnection(); - console.log(`Server running on port http://localhost:${process.env.APP_PORT}`); -}) \ No newline at end of file + testConnection(); + console.log( + `Server running on port http://localhost:${process.env.APP_PORT}` + ); +}); diff --git a/middlewares/Level/checkLevel.js b/middlewares/Level/checkLevel.js new file mode 100644 index 0000000..f131dfa --- /dev/null +++ b/middlewares/Level/checkLevel.js @@ -0,0 +1,316 @@ +import models from "../../models/index.js"; +import response from "../../response.js"; + +export const checkLevelsPerTopic = async (req, res, next) => { + const { ID_TOPIC } = req.body; + + try { + const levelCount = await models.Level.count({ + where: { ID_TOPIC }, + }); + + if (levelCount >= 7) { + return response( + 400, + null, + "Cannot add more than 6 levels to a single topic", + res + ); + } + + next(); + } catch (error) { + console.log(error); + return response(500, null, "Internal Server Error", res); + } +}; + +export const getSectionAndTopicByLevelId = async (req, res, next) => { + const { id } = req.params; + + try { + const level = await models.Level.findByPk(id, { + attributes: ["ID_SECTION", "ID_TOPIC"], + }); + + if (!level) { + return response(404, null, "Level not found", res); + } + + req.body.ID_SECTION = level.ID_SECTION; + req.body.ID_TOPIC = level.ID_TOPIC; + + next(); + } catch (error) { + console.log(error); + return response(500, null, "Internal Server Error", res); + } +}; + +export const autoCalculateRoutes = async (req, res, next) => { + const { NAME_LEVEL, ID_TOPIC } = req.body; + + try { + let routes = { + ROUTE_1: 0, + ROUTE_2: 0, + ROUTE_3: 0, + ROUTE_4: 0, + ROUTE_5: 0, + ROUTE_6: 0, + }; + + const levelTitles = [ + "Pretest", + "Level 1", + "Level 2", + "Level 3", + "Level 4", + "Level 5", + "Level 6", + ]; + + const levels = await models.Level.findAll({ + where: { + ID_TOPIC, + NAME_LEVEL: { + [models.Sequelize.Op.in]: levelTitles, + }, + }, + }); + + levels.sort( + (a, b) => + levelTitles.indexOf(a.NAME_LEVEL) - levelTitles.indexOf(b.NAME_LEVEL) + ); + + const levelMap = {}; + levels.forEach((level) => { + levelMap[level.NAME_LEVEL] = level.ID_LEVEL; + }); + + switch (NAME_LEVEL) { + case "Pretest": + req.body.IS_PRETEST = 1; + routes.ROUTE_1 = levelMap["Level 1"] || 0; + routes.ROUTE_2 = levelMap["Level 2"] || 0; + routes.ROUTE_3 = levelMap["Level 3"] || 0; + routes.ROUTE_4 = levelMap["Level 4"] || 0; + routes.ROUTE_5 = levelMap["Level 5"] || 0; + routes.ROUTE_6 = levelMap["Level 6"] || 0; + break; + case "Level 1": + routes.ROUTE_1 = levelMap["Pretest"] || 0; + routes.ROUTE_2 = levelMap["Level 2"] || 0; + routes.ROUTE_3 = levelMap["Level 3"] || 0; + routes.ROUTE_4 = levelMap["Level 4"] || 0; + routes.ROUTE_5 = levelMap["Level 5"] || 0; + routes.ROUTE_6 = levelMap["Level 6"] || 0; + break; + case "Level 2": + routes.ROUTE_1 = levelMap["Pretest"] || 0; + routes.ROUTE_2 = levelMap["Level 1"] || 0; + routes.ROUTE_3 = levelMap["Level 3"] || 0; + routes.ROUTE_4 = levelMap["Level 4"] || 0; + routes.ROUTE_5 = levelMap["Level 5"] || 0; + routes.ROUTE_6 = levelMap["Level 6"] || 0; + break; + case "Level 3": + routes.ROUTE_1 = levelMap["Pretest"] || 0; + routes.ROUTE_2 = levelMap["Level 1"] || 0; + routes.ROUTE_3 = levelMap["Level 2"] || 0; + routes.ROUTE_4 = levelMap["Level 4"] || 0; + routes.ROUTE_5 = levelMap["Level 5"] || 0; + routes.ROUTE_6 = levelMap["Level 6"] || 0; + break; + case "Level 4": + routes.ROUTE_1 = levelMap["Pretest"] || 0; + routes.ROUTE_2 = levelMap["Level 1"] || 0; + routes.ROUTE_3 = levelMap["Level 2"] || 0; + routes.ROUTE_4 = levelMap["Level 3"] || 0; + routes.ROUTE_5 = levelMap["Level 5"] || 0; + routes.ROUTE_6 = levelMap["Level 6"] || 0; + break; + case "Level 5": + routes.ROUTE_1 = levelMap["Pretest"] || 0; + routes.ROUTE_2 = levelMap["Level 1"] || 0; + routes.ROUTE_3 = levelMap["Level 2"] || 0; + routes.ROUTE_4 = levelMap["Level 3"] || 0; + routes.ROUTE_5 = levelMap["Level 4"] || 0; + routes.ROUTE_6 = levelMap["Level 6"] || 0; + break; + case "Level 6": + routes.ROUTE_1 = levelMap["Pretest"] || 0; + routes.ROUTE_2 = levelMap["Level 1"] || 0; + routes.ROUTE_3 = levelMap["Level 2"] || 0; + routes.ROUTE_4 = levelMap["Level 3"] || 0; + routes.ROUTE_5 = levelMap["Level 4"] || 0; + routes.ROUTE_6 = levelMap["Level 5"] || 0; + break; + } + + req.body.ROUTE_1 = routes.ROUTE_1; + req.body.ROUTE_2 = routes.ROUTE_2; + req.body.ROUTE_3 = routes.ROUTE_3; + req.body.ROUTE_4 = routes.ROUTE_4; + req.body.ROUTE_5 = routes.ROUTE_5; + req.body.ROUTE_6 = routes.ROUTE_6; + + next(); + } catch (error) { + console.log(error); + return response(500, null, "Internal Server Error", res); + } +}; + +export const updateOtherLevelsRoutes = async (req, res, next) => { + const { NAME_LEVEL, ID_TOPIC, newLevelId } = req.body; + + try { + const levelTitles = [ + "Pretest", + "Level 1", + "Level 2", + "Level 3", + "Level 4", + "Level 5", + "Level 6", + ]; + const currentLevelIndex = levelTitles.indexOf(NAME_LEVEL); + + if (currentLevelIndex !== -1) { + const levels = await models.Level.findAll({ + where: { + ID_TOPIC, + NAME_LEVEL: { + [models.Sequelize.Op.in]: levelTitles, + }, + }, + }); + + levels.sort( + (a, b) => + levelTitles.indexOf(a.NAME_LEVEL) - levelTitles.indexOf(b.NAME_LEVEL) + ); + + const levelMap = {}; + levels.forEach((level) => { + levelMap[level.NAME_LEVEL] = level.ID_LEVEL; + }); + + if (newLevelId) { + levelMap[NAME_LEVEL] = newLevelId; + } + + for (let i = 0; i < levelTitles.length; i++) { + if (i === currentLevelIndex) continue; + + const levelTitle = levelTitles[i]; + const updateData = {}; + + if (i === 0) { + updateData.ROUTE_1 = levelMap["Level 1"] || 0; + updateData.ROUTE_2 = levelMap["Level 2"] || 0; + updateData.ROUTE_3 = levelMap["Level 3"] || 0; + updateData.ROUTE_4 = levelMap["Level 4"] || 0; + updateData.ROUTE_5 = levelMap["Level 5"] || 0; + updateData.ROUTE_6 = levelMap["Level 6"] || 0; + } else if (i === 1) { + updateData.ROUTE_1 = levelMap["Pretest"] || 0; + updateData.ROUTE_2 = levelMap["Level 2"] || 0; + updateData.ROUTE_3 = levelMap["Level 3"] || 0; + updateData.ROUTE_4 = levelMap["Level 4"] || 0; + updateData.ROUTE_5 = levelMap["Level 5"] || 0; + updateData.ROUTE_6 = levelMap["Level 6"] || 0; + } else if (i === 2) { + updateData.ROUTE_1 = levelMap["Pretest"] || 0; + updateData.ROUTE_2 = levelMap["Level 1"] || 0; + updateData.ROUTE_3 = levelMap["Level 3"] || 0; + updateData.ROUTE_4 = levelMap["Level 4"] || 0; + updateData.ROUTE_5 = levelMap["Level 5"] || 0; + updateData.ROUTE_6 = levelMap["Level 6"] || 0; + } else if (i === 3) { + updateData.ROUTE_1 = levelMap["Pretest"] || 0; + updateData.ROUTE_2 = levelMap["Level 1"] || 0; + updateData.ROUTE_3 = levelMap["Level 2"] || 0; + updateData.ROUTE_4 = levelMap["Level 4"] || 0; + updateData.ROUTE_5 = levelMap["Level 5"] || 0; + updateData.ROUTE_6 = levelMap["Level 6"] || 0; + } else if (i === 4) { + updateData.ROUTE_1 = levelMap["Pretest"] || 0; + updateData.ROUTE_2 = levelMap["Level 1"] || 0; + updateData.ROUTE_3 = levelMap["Level 2"] || 0; + updateData.ROUTE_4 = levelMap["Level 3"] || 0; + updateData.ROUTE_5 = levelMap["Level 5"] || 0; + updateData.ROUTE_6 = levelMap["Level 6"] || 0; + } else if (i === 5) { + updateData.ROUTE_1 = levelMap["Pretest"] || 0; + updateData.ROUTE_2 = levelMap["Level 1"] || 0; + updateData.ROUTE_3 = levelMap["Level 2"] || 0; + updateData.ROUTE_4 = levelMap["Level 3"] || 0; + updateData.ROUTE_5 = levelMap["Level 4"] || 0; + updateData.ROUTE_6 = levelMap["Level 6"] || 0; + } else if (i === 6) { + updateData.ROUTE_1 = levelMap["Pretest"] || 0; + updateData.ROUTE_2 = levelMap["Level 1"] || 0; + updateData.ROUTE_3 = levelMap["Level 2"] || 0; + updateData.ROUTE_4 = levelMap["Level 3"] || 0; + updateData.ROUTE_5 = levelMap["Level 4"] || 0; + updateData.ROUTE_6 = levelMap["Level 5"] || 0; + } + + await models.Level.update(updateData, { + where: { + ID_TOPIC, + NAME_LEVEL: levelTitle, + }, + }); + } + } + + next(); + } catch (error) { + console.log(error); + return response(500, null, "Internal Server Error", res); + } +}; + +export const updateOtherLevelsRoutesOnDelete = async (req, res, next) => { + const { newLevelId } = req.body; + + try { + if (!newLevelId) { + return response(400, null, "No level provided for deletion!", res); + } + + const levelsToUpdate = await models.Level.findAll({ + where: { + [models.Sequelize.Op.or]: [ + { ROUTE_1: newLevelId }, + { ROUTE_2: newLevelId }, + { ROUTE_3: newLevelId }, + { ROUTE_4: newLevelId }, + { ROUTE_5: newLevelId }, + { ROUTE_6: newLevelId }, + ], + }, + }); + + for (const level of levelsToUpdate) { + const updateData = {}; + if (level.ROUTE_1 === newLevelId) updateData.ROUTE_1 = 0; + if (level.ROUTE_2 === newLevelId) updateData.ROUTE_2 = 0; + if (level.ROUTE_3 === newLevelId) updateData.ROUTE_3 = 0; + if (level.ROUTE_4 === newLevelId) updateData.ROUTE_4 = 0; + if (level.ROUTE_5 === newLevelId) updateData.ROUTE_5 = 0; + if (level.ROUTE_6 === newLevelId) updateData.ROUTE_6 = 0; + + await level.update(updateData); + } + + next(); + } catch (error) { + console.log(error); + return response(500, null, "Internal Server Error", res); + } +}; diff --git a/middlewares/uploadLevel.js b/middlewares/Level/uploadLevel.js similarity index 79% rename from middlewares/uploadLevel.js rename to middlewares/Level/uploadLevel.js index c157159..d8362c5 100644 --- a/middlewares/uploadLevel.js +++ b/middlewares/Level/uploadLevel.js @@ -2,12 +2,10 @@ import multer from "multer"; import crypto from "crypto"; import path from "path"; import fs from "fs"; -import response from "../response.js"; +import response from "../../response.js"; -// Setup memory storage for Multer const memoryStorage = multer.memoryStorage(); -// Filter untuk membatasi tipe file dan ukuran file const fileFilter = (req, file, cb) => { const ext = path.extname(file.originalname).toLowerCase(); @@ -56,12 +54,11 @@ const fileFilter = (req, file, cb) => { } }; -// Set up Multer untuk menangani upload const upload = multer({ storage: memoryStorage, fileFilter, limits: { - fileSize: 100 * 1024 * 1024, // Total file size limit if needed + fileSize: 100 * 1024 * 1024, }, }).fields([ { name: "video", maxCount: 1 }, @@ -69,7 +66,6 @@ const upload = multer({ { name: "image", maxCount: 1 }, ]); -// Middleware untuk menangani upload dan pengecekan file size const handleUpload = (req, res, next) => { upload(req, res, (err) => { if (err) { @@ -85,7 +81,6 @@ const handleUpload = (req, res, next) => { let validFiles = true; let errorMessages = []; - // Validate file sizes if (video && video.size > 30 * 1024 * 1024) { validFiles = false; video.buffer = null; @@ -105,11 +100,9 @@ const handleUpload = (req, res, next) => { } if (validFiles) { - // Attach files to the request object for further processing req.filesToSave = { video, audio, image }; next(); } else { - // Clear file buffers and return error response with specific messages clearFileBuffers({ video, audio, image }); return response(400, null, errorMessages.join("; "), res); } @@ -121,7 +114,6 @@ const handleUpload = (req, res, next) => { }); }; -// Function to clear file buffers export const clearFileBuffers = (files) => { for (const file of Object.values(files)) { if (file && file.buffer) { @@ -130,19 +122,17 @@ export const clearFileBuffers = (files) => { } }; -export const generateHash = (subjectId, filename, bufferLength) => { +export const generateHash = (sectionId, topicId, filename, bufferLength) => { return crypto .createHash("md5") - .update(subjectId + filename + bufferLength) + .update(sectionId + topicId + filename + bufferLength) .digest("hex"); }; -// Function to save files to disk -export const saveFileToDisk = (file, type, title, topicId, subjectId) => { - const formattedTitle = title.replace(/\s+/g, '').toLowerCase(); +export const saveFileToDisk = (file, type, topicId, sectionId, levelId) => { const ext = path.extname(file.originalname); - const hash = generateHash(subjectId, file.originalname, file.buffer.length); - const filename = `${topicId}-${formattedTitle}-${type}-${hash}${ext}`; + const hash = generateHash(sectionId, topicId, file.originalname, file.buffer.length); + const filename = `${type}-${levelId}-${hash}${ext}`; let folderPath; switch (type) { diff --git a/middlewares/User/authUser.js b/middlewares/User/authUser.js new file mode 100644 index 0000000..de925ca --- /dev/null +++ b/middlewares/User/authUser.js @@ -0,0 +1,73 @@ +import jwt from "jsonwebtoken"; +import response from "../../response.js"; +import models from "../../models/index.js"; + +export const verifyLoginUser = async (req, res, next) => { + const authHeader = req.headers.authorization; + + if (!authHeader || !authHeader.startsWith("Bearer ")) { + return response(401, null, "Please log in to your account first!", res); + } + + const token = authHeader.split(" ")[1]; + + try { + const decoded = jwt.verify(token, process.env.ACCESS_TOKEN_SECRET); + + const user = await models.User.findByPk(decoded.id, { + attributes: { + exclude: ["PASSWORD"], + }, + }); + + if (!user) { + return response(404, null, "User not found!", res); + } + + req.user = user; + + next(); + } catch (error) { + if (error.name === "JsonWebTokenError") { + return response(403, null, "Invalid token!", res); + } else { + return response(500, null, "An error occurred on the server!", res); + } + } +}; + +export const adminOnly = (req, res, next) => { + if (!req.user || req.user.ROLE !== "admin") { + return response( + 403, + null, + "Access denied! You do not have admin access.", + res + ); + } + next(); +}; + +export const teacherOnly = (req, res, next) => { + if (!req.user || req.user.ROLE !== "teacher") { + return response( + 403, + null, + "Access denied! You do not have teacher access.", + res + ); + } + next(); +}; + +export const adminOrTeacherOnly = (req, res, next) => { + if (!req.user || (req.user.ROLE !== "admin" && req.user.ROLE !== "teacher")) { + return response( + 403, + null, + "Access denied! You do not have access for this feature.", + res + ); + } + next(); +}; diff --git a/middlewares/User/uploadUser.js b/middlewares/User/uploadUser.js new file mode 100644 index 0000000..dbb0046 --- /dev/null +++ b/middlewares/User/uploadUser.js @@ -0,0 +1,91 @@ +import multer from "multer"; +import crypto from "crypto"; +import path from "path"; +import fs from "fs"; +import response from "../../response.js"; + +const memoryStorage = multer.memoryStorage(); + +const fileFilter = (req, file, cb) => { + const ext = path.extname(file.originalname).toLowerCase(); + if (ext === ".png" || ext === ".jpg" || ext === ".jpeg") { + cb(null, true); + } else { + cb( + new Error( + "Invalid file type, only .png, .jpg, and .jpeg files are allowed!" + ), + false + ); + } +}; + +const upload = multer({ + storage: memoryStorage, + fileFilter, + limits: { fileSize: 5 * 1024 * 1024 }, +}).fields([{ name: "picture", maxCount: 1 }]); + +const handleUpload = (req, res, next) => { + upload(req, res, (err) => { + if (err) { + return response(400, null, err.message, res); + } + + const files = req.files; + const picture = files?.picture ? files.picture[0] : null; + + try { + let validFiles = true; + let errorMessages = []; + + if (picture && picture.size > 5 * 1024 * 1024) { + validFiles = false; + picture.buffer = null; + errorMessages.push("Picture file exceeds the size limit of 5MB"); + } + + if (validFiles) { + req.filesToSave = { picture }; + next(); + } else { + clearFileBuffers({ picture }); + return response(400, null, errorMessages.join(", "), res); + } + } catch (error) { + console.log(error); + clearFileBuffers(file); + return response(500, null, "Internal Server Error", res); + } + }); +}; + +export const clearFileBuffers = (files) => { + for (const file of Object.values(files)) { + if (file && file.buffer) { + file.buffer = null; + } + } +}; + +export const saveFileToDisk = (file, userId, userName) => { + const ext = path.extname(file.originalname); + const hash = crypto + .createHash("md5") + .update( + userId + userName + file.originalname + file.buffer.length.toString() + ) + .digest("hex"); + const filename = `user-${hash}${ext}`; + const folderPath = path.join("public/uploads/avatar"); + + if (!fs.existsSync(folderPath)) { + fs.mkdirSync(folderPath, { recursive: true }); + } + + const filepath = path.join(folderPath, filename); + fs.writeFileSync(filepath, file.buffer); + return filename; +}; + +export default handleUpload; diff --git a/middlewares/authUser.js b/middlewares/authUser.js deleted file mode 100644 index 9e57e68..0000000 --- a/middlewares/authUser.js +++ /dev/null @@ -1,64 +0,0 @@ -import jwt from "jsonwebtoken"; -import models from "../models/index.js"; - -export const verifyLoginUser = async (req, res, next) => { - const { accessToken } = req.cookies; - - if (!accessToken) { - return res - .status(401) - .json({ message: "Please log in to your account first!" }); - } - - try { - // Verifikasi token dan dapatkan payload yang didekode - const decoded = jwt.verify(accessToken, process.env.ACCESS_TOKEN_SECRET); - - // Cari user berdasarkan id yang ada di token - const user = await models.User.findByPk(decoded.id, { - attributes: { - exclude: ["password"], - }, - }); - - if (!user) { - return res.status(404).json({ message: "User not found!" }); - } - - // Simpan informasi user di req.user untuk penggunaan selanjutnya - req.user = user; - - // Lanjutkan ke route handler berikutnya - next(); - } catch (error) { - if (error.name === "JsonWebTokenError") { - return res.status(403).json({ message: "Invalid token!" }); - } else { - return res - .status(500) - .json({ message: "An error occurred on the server!" }); - } - } -}; - -// Middleware untuk memverifikasi apakah pengguna adalah admin -export const adminOnly = (req, res, next) => { - if (!req.user || req.user.roles !== "admin") { - return res.status(403).json({ - message: - "Access denied! You do not have admin access.", - }); - } - next(); -}; - -// Middleware untuk memverifikasi apakah pengguna adalah teacher -export const teacherOnly = (req, res, next) => { - if (!req.user || req.user.roles !== "teacher") { - return res.status(403).json({ - message: - "Access denied! You do not have teacher access.", - }); - } - next(); -}; diff --git a/middlewares/autoGrading.js b/middlewares/autoGrading.js new file mode 100644 index 0000000..884ffa6 --- /dev/null +++ b/middlewares/autoGrading.js @@ -0,0 +1,275 @@ +import models from "../models/index.js"; +import response from "../response.js"; + +export const checkCorrectAnswers = async (req, res, next) => { + const { id } = req.params; + + try { + const stdExercises = await models.StdExercise.findAll({ + where: { ID_STUDENT_LEARNING: id }, + }); + + if (!stdExercises || stdExercises.length === 0) { + return response(404, null, "No student exercises found", res); + } + + for (const stdExercise of stdExercises) { + const { ID_ADMIN_EXERCISE, ANSWER_STUDENT } = stdExercise; + + const exercise = await models.Exercise.findByPk(ID_ADMIN_EXERCISE); + if (!exercise) continue; + + const weight = parseFloat(exercise.SCORE_WEIGHT); // Ensure weight is a float + const questionType = exercise.QUESTION_TYPE; + + switch (questionType) { + case "MCQ": { + const multipleChoice = await models.MultipleChoices.findOne({ + where: { ID_ADMIN_EXERCISE }, + }); + if (multipleChoice) { + stdExercise.IS_CORRECT = + ANSWER_STUDENT === multipleChoice.ANSWER_KEY ? 1 : 0; + stdExercise.RESULT_SCORE_STUDENT = + ANSWER_STUDENT === multipleChoice.ANSWER_KEY ? weight : 0.0; + } + break; + } + + case "TFQ": { + const trueFalse = await models.TrueFalse.findOne({ + where: { ID_ADMIN_EXERCISE }, + }); + if (trueFalse) { + const isTrueStudent = ANSWER_STUDENT === "1"; + stdExercise.IS_CORRECT = + isTrueStudent === Boolean(trueFalse.IS_TRUE) ? 1 : 0; + stdExercise.RESULT_SCORE_STUDENT = + isTrueStudent === Boolean(trueFalse.IS_TRUE) ? weight : 0.0; + } + break; + } + + case "MPQ": { + const matchingPairs = await models.MatchingPairs.findAll({ + where: { ID_ADMIN_EXERCISE }, + }); + if (matchingPairs && matchingPairs.length > 0) { + const studentAnswers = ANSWER_STUDENT.split(",").map((pair) => { + const [left, right] = pair.split("-"); + + return { + LEFT_PAIR: left ? left.trim() : "", + RIGHT_PAIR: right ? right.trim() : "", + }; + }); + + let correctCount = 0; + for (const studentAnswer of studentAnswers) { + if ( + matchingPairs.some( + (pair) => + pair.LEFT_PAIR === studentAnswer.LEFT_PAIR && + pair.RIGHT_PAIR === studentAnswer.RIGHT_PAIR + ) + ) { + correctCount++; + } + } + + const correctPercentage = correctCount / matchingPairs.length; + stdExercise.IS_CORRECT = correctCount > 0 ? 1 : 0; + stdExercise.RESULT_SCORE_STUDENT = correctPercentage * weight; // Use float arithmetic + } + break; + } + + default: + break; + } + + await stdExercise.save(); + } + + next(); + } catch (error) { + console.error("Error checking correct answers:", error); + response(500, null, "Internal Server Error", res); + } +}; + +export const calculateScore = async (req, res, next) => { + const { id } = req.params; + + try { + const stdLearning = await models.StdLearning.findByPk(id); + + if (!stdLearning) { + return response(404, null, "Student Learning record not found", res); + } + + const allExercises = await models.Exercise.findAll({ + where: { ID_LEVEL: stdLearning.ID_LEVEL }, + }); + + if (!allExercises || allExercises.length === 0) { + return response(404, null, "No exercises found for this level", res); + } + + const stdExercises = await models.StdExercise.findAll({ + where: { ID_STUDENT_LEARNING: id }, + }); + + let totalWeight = 0; + for (const exercise of allExercises) { + totalWeight += exercise.SCORE_WEIGHT; + } + + let totalScore = 0; + for (const stdExercise of stdExercises) { + const exercise = allExercises.find( + (ex) => ex.ID_ADMIN_EXERCISE === stdExercise.ID_ADMIN_EXERCISE + ); + + if (exercise && stdExercise.RESULT_SCORE_STUDENT !== null) { + totalScore += stdExercise.RESULT_SCORE_STUDENT; + } + } + + const finalScore = Math.round((totalScore / totalWeight) * 100); + + req.body.SCORE = finalScore; + + next(); + } catch (error) { + console.log(error); + response(500, null, "Internal Server Error", res); + } +}; + +export const checkFirstFiveCorrect = async (req, res, next) => { + const { id } = req.params; + + try { + const stdLearning = await models.StdLearning.findByPk(id); + + if (!stdLearning) { + return response(404, null, "Student Learning record not found", res); + } + + const firstFiveExercises = await models.Exercise.findAll({ + where: { ID_LEVEL: stdLearning.ID_LEVEL }, + order: [["TITLE", "ASC"]], + limit: 5, + }); + + if (!firstFiveExercises || firstFiveExercises.length < 5) { + return response(404, null, "Not enough exercises for this level", res); + } + + const stdExercises = await models.StdExercise.findAll({ + where: { ID_STUDENT_LEARNING: id }, + }); + + const allCorrect = firstFiveExercises.every((exercise) => { + const stdExercise = stdExercises.find( + (se) => se.ID_ADMIN_EXERCISE === exercise.ID_ADMIN_EXERCISE + ); + return stdExercise && stdExercise.IS_CORRECT === 1; + }); + + req.body.FIRST_FIVE_CORRECT = allCorrect; + + next(); + } catch (error) { + console.error("Error checking first five correct answers:", error); + res.status(500).json({ message: "Internal Server Error" }); + } +}; + +export const nextLearning = async (req, res, next) => { + const { SCORE, FIRST_FIVE_CORRECT } = req.body; + + try { + const stdLearning = await models.StdLearning.findByPk(req.params.id, { + include: [ + { + model: models.Level, + as: "level", + }, + ], + }); + + if (!stdLearning) { + return response(404, null, "Student Learning record not found", res); + } + + const topic_id = stdLearning.level.ID_TOPIC; + + const levels = await models.Level.findAll({ + where: { ID_TOPIC: topic_id }, + order: [["NAME_LEVEL", "ASC"]], + }); + + const levelMap = levels.reduce((map, level) => { + map[level.NAME_LEVEL] = level; + return map; + }, {}); + + let nextLearningLevel = null; + let currentLevelNumber = parseInt( + stdLearning.level.NAME_LEVEL.split(" ")[1] + ); + + if (stdLearning.level.IS_PRETEST) { + if (SCORE >= 0 && SCORE <= 50) { + nextLearningLevel = levelMap["Level 1"]; + } else if (SCORE >= 51 && SCORE <= 60) { + nextLearningLevel = levelMap["Level 2"]; + } else if (SCORE >= 61 && SCORE <= 70) { + nextLearningLevel = levelMap["Level 3"]; + } else if (SCORE >= 71 && SCORE <= 80) { + nextLearningLevel = levelMap["Level 4"]; + } else if (SCORE >= 81 && SCORE <= 90) { + nextLearningLevel = levelMap["Level 5"]; + } else if (SCORE >= 91 && SCORE <= 100) { + nextLearningLevel = levelMap["Level 6"]; + } + } else { + if (SCORE >= 75 && FIRST_FIVE_CORRECT) { + currentLevelNumber += SCORE >= 85 ? 2 : 1; + currentLevelNumber = Math.min(6, currentLevelNumber); + } else if ( + (SCORE >= 75 && !FIRST_FIVE_CORRECT) || + (SCORE >= 51 && SCORE <= 74) + ) { + currentLevelNumber = currentLevelNumber; + } else if (SCORE >= 41 && SCORE <= 50) { + currentLevelNumber = Math.max(1, currentLevelNumber - 1); + } else if (SCORE >= 0 && SCORE <= 40) { + currentLevelNumber = Math.max(1, currentLevelNumber - 2); + } + + currentLevelNumber = Math.max(1, Math.min(6, currentLevelNumber)); + + nextLearningLevel = levelMap[`Level ${currentLevelNumber}`]; + } + + if (!nextLearningLevel) { + return response(404, null, "Next learning level not found", res); + } + + req.body.NEXT_LEARNING = nextLearningLevel.ID_LEVEL; + req.body.IS_PASS = + SCORE > 85 && + stdLearning.level.NAME_LEVEL === "Level 6" && + FIRST_FIVE_CORRECT + ? 1 + : 0; + + next(); + } catch (error) { + console.log(error); + response(500, null, "Error determining next learning path", res); + } +}; diff --git a/middlewares/checkLevel.js b/middlewares/checkLevel.js deleted file mode 100644 index 106487f..0000000 --- a/middlewares/checkLevel.js +++ /dev/null @@ -1,29 +0,0 @@ -import models from "../models/index.js"; -import response from "../response.js"; - -export const checkMaxLevelsPerTopic = async (req, res, next) => { - const { topic_id } = req.body; - - try { - // Hitung jumlah level yang ada pada topic_id yang diberikan - const levelCount = await models.Level.count({ - where: { topic_id }, - }); - - // Periksa apakah jumlah level sudah mencapai 5 - if (levelCount >= 5) { - return response( - 400, - null, - "Cannot add more than 5 levels to a single topic", - res - ); - } - - // Lanjutkan ke middleware atau route handler berikutnya jika belum mencapai 5 - next(); - } catch (error) { - console.log(error); - return response(500, null, "Internal Server Error", res); - } -}; diff --git a/middlewares/uploadExercise.js b/middlewares/uploadExercise.js new file mode 100644 index 0000000..ce3365b --- /dev/null +++ b/middlewares/uploadExercise.js @@ -0,0 +1,161 @@ +import multer from "multer"; +import crypto from "crypto"; +import path from "path"; +import fs from "fs"; +import response from "../response.js"; + +const memoryStorage = multer.memoryStorage(); + +const fileFilter = (req, file, cb) => { + const ext = path.extname(file.originalname).toLowerCase(); + + switch (file.fieldname) { + case "video": + if (ext === ".mp4") { + cb(null, true); + } else { + cb( + new Error( + "Invalid file type, only .mp4 files are allowed for video!" + ), + false + ); + } + break; + + case "audio": + if (ext === ".mp3") { + cb(null, true); + } else { + cb( + new Error( + "Invalid file type, only .mp3 files are allowed for audio!" + ), + false + ); + } + break; + + case "image": + if (ext === ".jpg" || ext === ".jpeg" || ext === ".png") { + cb(null, true); + } else { + cb( + new Error( + "Invalid file type, only .jpg, .jpeg, and .png files are allowed for image!" + ), + false + ); + } + break; + + default: + cb(new Error("Invalid file type!"), false); + } +}; + +const upload = multer({ + storage: memoryStorage, + fileFilter, + limits: { + fileSize: 100 * 1024 * 1024, + }, +}).fields([ + { name: "video", maxCount: 1 }, + { name: "audio", maxCount: 1 }, + { name: "image", maxCount: 1 }, +]); + +const handleUpload = (req, res, next) => { + upload(req, res, (err) => { + if (err) { + return response(400, null, err.message, res); + } + + const files = req.files; + const video = files?.video ? files.video[0] : null; + const audio = files?.audio ? files.audio[0] : null; + const image = files?.image ? files.image[0] : null; + + try { + let validFiles = true; + let errorMessages = []; + + if (video && video.size > 30 * 1024 * 1024) { + validFiles = false; + video.buffer = null; + errorMessages.push("Video file exceeds the size limit of 30MB"); + } + + if (audio && audio.size > 10 * 1024 * 1024) { + validFiles = false; + audio.buffer = null; + errorMessages.push("Audio file exceeds the size limit of 10MB"); + } + + if (image && image.size > 5 * 1024 * 1024) { + validFiles = false; + image.buffer = null; + errorMessages.push("Image file exceeds the size limit of 5MB"); + } + + if (validFiles) { + req.filesToSave = { video, audio, image }; + next(); + } else { + clearFileBuffers({ video, audio, image }); + return response(400, null, errorMessages.join("; "), res); + } + } catch (error) { + console.log(error); + clearFileBuffers({ video, audio, image }); + return response(500, null, "Internal Server Error", res); + } + }); +}; + +export const clearFileBuffers = (files) => { + for (const file of Object.values(files)) { + if (file && file.buffer) { + file.buffer = null; + } + } +}; + +export const generateHash = (levelId, filename, bufferLength) => { + return crypto + .createHash("md5") + .update(levelId + filename + bufferLength) + .digest("hex"); +}; + +export const saveFileToDisk = (file, type, levelId, exerciseId) => { + const ext = path.extname(file.originalname); + const hash = generateHash(levelId, file.originalname, file.buffer.length); + const filename = `${type}-${exerciseId}-${hash}${ext}`; + + let folderPath; + switch (type) { + case "video": + folderPath = path.join("public/uploads/exercise/video"); + break; + case "audio": + folderPath = path.join("public/uploads/exercise/audio"); + break; + case "image": + folderPath = path.join("public/uploads/exercise/image"); + break; + default: + folderPath = path.join("public/uploads/exercise"); + } + + if (!fs.existsSync(folderPath)) { + fs.mkdirSync(folderPath, { recursive: true }); + } + + const filepath = path.join(folderPath, filename); + fs.writeFileSync(filepath, file.buffer); + return filename; +}; + +export default handleUpload; diff --git a/middlewares/uploadSubject.js b/middlewares/uploadSection.js similarity index 68% rename from middlewares/uploadSubject.js rename to middlewares/uploadSection.js index f58fc45..62c388c 100644 --- a/middlewares/uploadSubject.js +++ b/middlewares/uploadSection.js @@ -23,11 +23,8 @@ const fileFilter = (req, file, cb) => { const upload = multer({ storage: memoryStorage, fileFilter, - limits: { fileSize: 10 * 1024 * 1024 }, // Limit file size to 5MB -}).fields([ - { name: "icon", maxCount: 1 }, - { name: "thumbnail", maxCount: 1 }, -]); + limits: { fileSize: 5 * 1024 * 1024 }, +}).fields([{ name: "thumbnail", maxCount: 1 }]); const handleUpload = (req, res, next) => { upload(req, res, (err) => { @@ -36,20 +33,12 @@ const handleUpload = (req, res, next) => { } const files = req.files; - const icon = files?.icon ? files.icon[0] : null; const thumbnail = files?.thumbnail ? files.thumbnail[0] : null; try { let validFiles = true; let errorMessages = []; - // Validate file sizes - if (icon && icon.size > 5 * 1024 * 1024) { - validFiles = false; - icon.buffer = null; - errorMessages.push("Icon file exceeds the size limit of 5MB"); - } - if (thumbnail && thumbnail.size > 5 * 1024 * 1024) { validFiles = false; thumbnail.buffer = null; @@ -57,15 +46,15 @@ const handleUpload = (req, res, next) => { } if (validFiles) { - req.filesToSave = { icon, thumbnail }; + req.filesToSave = { thumbnail }; next(); } else { - clearFileBuffers({ icon, thumbnail }); + clearFileBuffers({ thumbnail }); return response(400, null, errorMessages.join(", "), res); } } catch (error) { console.log(error); - clearFileBuffers({ icon, thumbnail }); + clearFileBuffers({ thumbnail }); return response(500, null, "Internal Server Error", res); } }); @@ -79,15 +68,14 @@ export const clearFileBuffers = (files) => { } }; -export const saveFileToDisk = (file, fieldName, subjectName) => { +export const saveFileToDisk = (file, fieldName, idSection) => { const ext = path.extname(file.originalname); const hash = crypto .createHash("md5") - .update(subjectName + file.originalname + file.buffer.length.toString()) - .digest("hex") - .slice(0, 8); - const filename = `${fieldName}-${hash}${ext}`; - const folderPath = path.join("public/uploads/subject"); + .update(file.originalname + file.buffer.length.toString()) + .digest("hex"); + const filename = `${fieldName}-${idSection}-${hash}${ext}`; + const folderPath = path.join("public/uploads/section"); if (!fs.existsSync(folderPath)) { fs.mkdirSync(folderPath, { recursive: true }); diff --git a/models/contentModels/exerciseModel.js b/models/contentModels/exerciseModel.js new file mode 100644 index 0000000..d4945c7 --- /dev/null +++ b/models/contentModels/exerciseModel.js @@ -0,0 +1,83 @@ +import db from "../../database/db.js"; + +const ExerciseModel = (DataTypes) => { + const Exercises = db.define( + "admin_exercise", + { + ID_ADMIN_EXERCISE: { + type: DataTypes.UUID, + primaryKey: true, + defaultValue: DataTypes.UUIDV4, + allowNull: false, + validate: { + notEmpty: true, + }, + }, + ID_LEVEL: { + type: DataTypes.UUID, + allowNull: false, + references: { + model: "level", + key: "ID_LEVEL", + }, + validate: { + notEmpty: true, + }, + }, + TITLE: { + type: DataTypes.STRING(100), + allowNull: false, + validate: { + notEmpty: true, + }, + }, + QUESTION: { + type: DataTypes.TEXT, + allowNull: false, + validate: { + notEmpty: true, + }, + }, + SCORE_WEIGHT: { + type: DataTypes.INTEGER(6), + allowNull: false, + defaultValue: 1, + validate: { + notEmpty: true, + }, + }, + QUESTION_TYPE: { + type: DataTypes.STRING(100), + allowNull: false, + validate: { + notEmpty: true, + }, + }, + AUDIO: { + type: DataTypes.STRING(1024), + allowNull: true, + }, + VIDEO: { + type: DataTypes.STRING(1024), + allowNull: true, + }, + IMAGE: { + type: DataTypes.STRING(1024), + allowNull: true, + }, + TIME_ADMIN_EXC: { + type: DataTypes.DATE, + allowNull: true, + defaultValue: DataTypes.NOW, + }, + }, + { + timestamps: false, + tableName: "admin_exercise", + } + ); + + return Exercises; +}; + +export default ExerciseModel; diff --git a/models/contentModels/levelModel.js b/models/contentModels/levelModel.js new file mode 100644 index 0000000..c0505df --- /dev/null +++ b/models/contentModels/levelModel.js @@ -0,0 +1,107 @@ +import db from "../../database/db.js"; + +const LevelModel = (DataTypes) => { + const Levels = db.define( + "level", + { + ID_LEVEL: { + type: DataTypes.UUID, + primaryKey: true, + defaultValue: DataTypes.UUIDV4, + validate: { + notEmpty: true, + }, + }, + ID_TOPIC: { + type: DataTypes.UUID, + allowNull: false, + validate: { + notEmpty: true, + }, + references: { + model: "topic", + key: "ID_TOPIC", + }, + }, + ID_SECTION: { + type: DataTypes.UUID, + allowNull: false, + validate: { + notEmpty: true, + }, + references: { + model: "section", + key: "ID_SECTION", + }, + }, + NAME_LEVEL: { + type: DataTypes.STRING(100), + allowNull: false, + validate: { + notEmpty: true, + }, + }, + CONTENT: { + type: DataTypes.STRING(1024), + allowNull: true, + }, + AUDIO: { + type: DataTypes.STRING(1024), + allowNull: true, + }, + IMAGE: { + type: DataTypes.STRING(1024), + allowNull: true, + }, + VIDEO: { + type: DataTypes.STRING(1024), + allowNull: true, + }, + IS_PRETEST: { + type: DataTypes.INTEGER(1), + allowNull: true, + defaultValue: 0, + validate: { + min: 0, + max: 1, + }, + }, + ROUTE_1: { + type: DataTypes.UUID, + allowNull: false, + }, + ROUTE_2: { + type: DataTypes.UUID, + allowNull: false, + }, + ROUTE_3: { + type: DataTypes.UUID, + allowNull: false, + }, + ROUTE_4: { + type: DataTypes.UUID, + allowNull: false, + }, + ROUTE_5: { + type: DataTypes.UUID, + allowNull: false, + }, + ROUTE_6: { + type: DataTypes.UUID, + allowNull: false, + }, + TIME_LEVEL: { + type: DataTypes.DATE, + allowNull: true, + defaultValue: DataTypes.NOW, + }, + }, + { + timestamps: false, + tableName: "level", + } + ); + return Levels; +}; + +export default LevelModel; diff --git a/models/contentModels/sectionModel.js b/models/contentModels/sectionModel.js new file mode 100644 index 0000000..3249ade --- /dev/null +++ b/models/contentModels/sectionModel.js @@ -0,0 +1,47 @@ +import db from "../../database/db.js"; + +const SectionModel = (DataTypes) => { + const Sections = db.define( + "section", + { + ID_SECTION: { + type: DataTypes.UUID, + primaryKey: true, + defaultValue: DataTypes.UUIDV4, + validate: { + notEmpty: true, + }, + }, + NAME_SECTION: { + type: DataTypes.STRING(100), + allowNull: false, + validate: { + notEmpty: true, + }, + }, + DESCRIPTION_SECTION: { + type: DataTypes.STRING(1024), + allowNull: false, + validate: { + notEmpty: true, + }, + }, + THUMBNAIL: { + type: DataTypes.STRING(255), + allowNull: true, + }, + TIME_SECTION: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + }, + }, + { + timestamps: false, + tableName: "section", + } + ); + return Sections; +}; + +export default SectionModel; diff --git a/models/contentModels/topicModel.js b/models/contentModels/topicModel.js new file mode 100644 index 0000000..8488580 --- /dev/null +++ b/models/contentModels/topicModel.js @@ -0,0 +1,54 @@ +import db from "../../database/db.js"; + +const TopicModel = (DataTypes) => { + const Topics = db.define( + "topic", + { + ID_TOPIC: { + type: DataTypes.UUID, + primaryKey: true, + defaultValue: DataTypes.UUIDV4, + validate: { + notEmpty: true, + }, + }, + ID_SECTION: { + type: DataTypes.UUID, + allowNull: false, + validate: { + notEmpty: true, + }, + references: { + model: "section", + key: "ID_SECTION", + }, + }, + NAME_TOPIC: { + type: DataTypes.STRING(100), + allowNull: false, + validate: { + notEmpty: true, + }, + }, + DESCRIPTION_TOPIC: { + type: DataTypes.STRING(1024), + allowNull: false, + validate: { + notEmpty: true, + }, + }, + TIME_TOPIC: { + type: DataTypes.DATE, + allowNull: true, + defaultValue: DataTypes.NOW, + }, + }, + { + timestamps: false, + tableName: "topic", + } + ); + return Topics; +}; + +export default TopicModel; diff --git a/models/exerciseTypesModels/matchingPairsModel.js b/models/exerciseTypesModels/matchingPairsModel.js new file mode 100644 index 0000000..4f4133b --- /dev/null +++ b/models/exerciseTypesModels/matchingPairsModel.js @@ -0,0 +1,50 @@ +import db from "../../database/db.js"; + +const MatchingPairsModel = (DataTypes) => { + const MatchingPairs = db.define( + "matching_pairs", + { + ID_MATCHING_PAIRS: { + type: DataTypes.UUID, + primaryKey: true, + defaultValue: DataTypes.UUIDV4, + allowNull: false, + validate: { + notEmpty: true, + }, + }, + ID_ADMIN_EXERCISE: { + type: DataTypes.UUID, + allowNull: false, + references: { + model: "admin_exercise", + key: "ID_ADMIN_EXERCISE", + }, + validate: { + notEmpty: true, + }, + }, + LEFT_PAIR: { + type: DataTypes.TEXT, + allowNull: true, + }, + RIGHT_PAIR: { + type: DataTypes.TEXT, + allowNull: true, + }, + TIME_MATCHING_PAIRS: { + type: DataTypes.DATE, + allowNull: true, + defaultValue: DataTypes.NOW, + }, + }, + { + timestamps: false, + tableName: "matching_pairs", + } + ); + + return MatchingPairs; +}; + +export default MatchingPairsModel; diff --git a/models/exerciseTypesModels/multipleChoicesModel.js b/models/exerciseTypesModels/multipleChoicesModel.js new file mode 100644 index 0000000..0f91789 --- /dev/null +++ b/models/exerciseTypesModels/multipleChoicesModel.js @@ -0,0 +1,66 @@ +import db from "../../database/db.js"; + +const MultipleChoicesModel = (DataTypes) => { + const MultipleChoices = db.define( + "multiple_choices", + { + ID_MULTIPLE_CHOICES: { + type: DataTypes.UUID, + primaryKey: true, + defaultValue: DataTypes.UUIDV4, + allowNull: false, + validate: { + notEmpty: true, + }, + }, + ID_ADMIN_EXERCISE: { + type: DataTypes.UUID, + allowNull: false, + references: { + model: "admin_exercise", + key: "ID_ADMIN_EXERCISE", + }, + validate: { + notEmpty: true, + }, + }, + OPTION_A: { + type: DataTypes.STRING(200), + allowNull: true, + }, + OPTION_B: { + type: DataTypes.STRING(200), + allowNull: true, + }, + OPTION_C: { + type: DataTypes.STRING(200), + allowNull: true, + }, + OPTION_D: { + type: DataTypes.STRING(200), + allowNull: true, + }, + OPTION_E: { + type: DataTypes.STRING(200), + allowNull: true, + }, + ANSWER_KEY: { + type: DataTypes.STRING(5), + allowNull: true, + }, + TIME_MULTIPLE_CHOICES: { + type: DataTypes.DATE, + allowNull: true, + defaultValue: DataTypes.NOW, + }, + }, + { + timestamps: false, + tableName: "multiple_choices", + } + ); + + return MultipleChoices; +}; + +export default MultipleChoicesModel; diff --git a/models/exerciseTypesModels/trueFalseModel.js b/models/exerciseTypesModels/trueFalseModel.js new file mode 100644 index 0000000..d960a4e --- /dev/null +++ b/models/exerciseTypesModels/trueFalseModel.js @@ -0,0 +1,46 @@ +import db from "../../database/db.js"; + +const TrueFalseModel = (DataTypes) => { + const TrueFalse = db.define( + "true_false", + { + ID_TRUE_FALSE: { + type: DataTypes.UUID, + primaryKey: true, + defaultValue: DataTypes.UUIDV4, + allowNull: false, + validate: { + notEmpty: true, + }, + }, + ID_ADMIN_EXERCISE: { + type: DataTypes.UUID, + allowNull: false, + references: { + model: "admin_exercise", + key: "ID_ADMIN_EXERCISE", + }, + validate: { + notEmpty: true, + }, + }, + IS_TRUE: { + type: DataTypes.TINYINT(1), + allowNull: true, + }, + TIME_TRUE_FALSE: { + type: DataTypes.DATE, + allowNull: true, + defaultValue: DataTypes.NOW, + }, + }, + { + timestamps: false, + tableName: "true_false", + } + ); + + return TrueFalse; +}; + +export default TrueFalseModel; \ No newline at end of file diff --git a/models/index.js b/models/index.js index 4203e92..536d254 100644 --- a/models/index.js +++ b/models/index.js @@ -1,19 +1,149 @@ import { Sequelize } from "sequelize"; -import UserModel from "./userModel.js"; -import SubjectModel from "./subjectModel.js"; -import TopicModel from "./topicModel.js"; -import LevelModel from "./levelModel.js"; +import db from "../database/db.js"; +import UserModel from "./usersModels/userModel.js"; +import TeacherModel from "./usersModels/teacherModel.js"; +import StudentModel from "./usersModels/studentModel.js"; +import SectionModel from "./contentModels/sectionModel.js"; +import TopicModel from "./contentModels/topicModel.js"; +import LevelModel from "./contentModels/levelModel.js"; +import ExerciseModel from "./contentModels/exerciseModel.js"; +import MultipleChoicesModel from "./exerciseTypesModels/multipleChoicesModel.js"; +import MatchingPairsModel from "./exerciseTypesModels/matchingPairsModel.js"; +import TrueFalseModel from "./exerciseTypesModels/trueFalseModel.js"; +import StdLearningModel from "./learningModels/stdLearningModel.js"; +import StdExerciseModel from "./learningModels/stdExerciseModel.js"; +import ClassModel from "./monitoringModels/classModel.js"; +import MonitoringModel from "./monitoringModels/monitoringModel.js"; -// Impor operator Op const Op = Sequelize.Op; +const User = UserModel(Sequelize.DataTypes); +const Teacher = TeacherModel(Sequelize.DataTypes); +const Student = StudentModel(Sequelize.DataTypes); +const Section = SectionModel(Sequelize.DataTypes); +const Topic = TopicModel(Sequelize.DataTypes); +const Level = LevelModel(Sequelize.DataTypes); +const Exercise = ExerciseModel(Sequelize.DataTypes); +const MultipleChoices = MultipleChoicesModel(Sequelize.DataTypes); +const MatchingPairs = MatchingPairsModel(Sequelize.DataTypes); +const TrueFalse = TrueFalseModel(Sequelize.DataTypes); +const StdLearning = StdLearningModel(Sequelize.DataTypes); +const StdExercise = StdExerciseModel(Sequelize.DataTypes); +const Class = ClassModel(Sequelize.DataTypes); +const Monitoring = MonitoringModel(Sequelize.DataTypes); + +User.hasOne(Teacher, { foreignKey: "ID", as: "teachers" }); +Teacher.belongsTo(User, { foreignKey: "ID", as: "teacherUser" }); + +User.hasOne(Student, { foreignKey: "ID", as: "students" }); +Student.belongsTo(User, { foreignKey: "ID", as: "studentUser" }); + +Section.hasMany(Topic, { foreignKey: "ID_SECTION", as: "topics" }); +Topic.belongsTo(Section, { foreignKey: "ID_SECTION", as: "topicSection" }); + +Topic.hasMany(Level, { foreignKey: "ID_TOPIC", as: "levels" }); +Level.belongsTo(Topic, { foreignKey: "ID_TOPIC", as: "levelTopic" }); + +Level.hasMany(Exercise, { foreignKey: "ID_LEVEL", as: "exercises" }); +Exercise.belongsTo(Level, { foreignKey: "ID_LEVEL", as: "levelExercise" }); + +Exercise.hasMany(MultipleChoices, { + foreignKey: "ID_ADMIN_EXERCISE", + as: "multipleChoices", +}); +MultipleChoices.belongsTo(Exercise, { + foreignKey: "ID_ADMIN_EXERCISE", + as: "exerciseMultipleChoices", +}); + +Exercise.hasMany(MatchingPairs, { + foreignKey: "ID_ADMIN_EXERCISE", + as: "matchingPairs", +}); +MatchingPairs.belongsTo(Exercise, { + foreignKey: "ID_ADMIN_EXERCISE", + as: "exerciseMatchingPairs", +}); + +Exercise.hasMany(TrueFalse, { + foreignKey: "ID_ADMIN_EXERCISE", + as: "trueFalse", +}); +TrueFalse.belongsTo(Exercise, { + foreignKey: "ID_ADMIN_EXERCISE", + as: "exerciseTrueFalse", +}); + +User.hasMany(StdLearning, { foreignKey: "ID", as: "userLearning" }); +StdLearning.belongsTo(User, { foreignKey: "ID", as: "learningUser" }); + +Level.hasMany(StdLearning, { foreignKey: "ID_LEVEL", as: "stdLearning" }); +StdLearning.belongsTo(Level, { foreignKey: "ID_LEVEL", as: "level" }); + +Level.hasOne(StdLearning, { foreignKey: "NEXT_LEARNING", as: "nextLevel" }); +StdLearning.belongsTo(Level, { foreignKey: "NEXT_LEARNING", as: "nextLevel" }); + +StdLearning.hasMany(StdExercise, { + foreignKey: "ID_STUDENT_LEARNING", + as: "stdExercises", +}); +StdExercise.belongsTo(StdLearning, { + foreignKey: "ID_STUDENT_LEARNING", + as: "stdLearningExercise", +}); + +StdExercise.belongsTo(Exercise, { + foreignKey: "ID_ADMIN_EXERCISE", + as: "stdExerciseExercises", +}); +Exercise.hasMany(StdExercise, { + foreignKey: "ID_ADMIN_EXERCISE", + as: "exerciseStdExercises", +}); + +Class.hasMany(Student, { foreignKey: "ID_CLASS", as: "ClassStudents" }); +Student.belongsTo(Class, { foreignKey: "ID_CLASS", as: "studentClass" }); + +Monitoring.hasOne(StdLearning, { + foreignKey: "ID_STUDENT_LEARNING", + sourceKey: "ID_STUDENT_LEARNING", + as: "stdLearningMonitoring", +}); +StdLearning.belongsTo(Monitoring, { + foreignKey: "ID_STUDENT_LEARNING", + as: "monitoringLearning", +}); + +Monitoring.hasMany(Class, { foreignKey: "ID_CLASS", as: "monitoringClasses" }); +Class.belongsTo(Monitoring, { foreignKey: "ID_CLASS", as: "monitoringClass" }); + +Teacher.hasMany(Monitoring, { + foreignKey: "ID_GURU", + as: "teacherMonitorings", +}); +Monitoring.belongsTo(Teacher, { + foreignKey: "ID_GURU", + as: "monitoringTeacher", +}); + const models = { - User: UserModel(Sequelize.DataTypes), - Subject: SubjectModel(Sequelize.DataTypes), - Topic: TopicModel(Sequelize.DataTypes), - Level: LevelModel(Sequelize.DataTypes), + User, + Teacher, + Student, + Section, + Topic, + Level, + Exercise, + MultipleChoices, + MatchingPairs, + TrueFalse, + StdLearning, + StdExercise, + Class, + Monitoring, Sequelize, Op, + db, }; -export default models; \ No newline at end of file +export default models; diff --git a/models/learningModels/stdExerciseModel.js b/models/learningModels/stdExerciseModel.js new file mode 100644 index 0000000..e1cfca7 --- /dev/null +++ b/models/learningModels/stdExerciseModel.js @@ -0,0 +1,75 @@ +import db from "../../database/db.js"; + +const StdExerciseModel = (DataTypes) => { + const StdExercise = db.define( + "student_exercise", + { + ID_STUDENT_EXERCISE: { + type: DataTypes.UUID, + primaryKey: true, + defaultValue: DataTypes.UUIDV4, + allowNull: false, + validate: { + notEmpty: true, + }, + }, + ID_ADMIN_EXERCISE: { + type: DataTypes.UUID, + allowNull: false, + validate: { + notEmpty: true, + }, + references: { + model: "admin_exercise", + key: "ID_ADMIN_EXERCISE", + }, + }, + ID_STUDENT_LEARNING: { + type: DataTypes.UUID, + allowNull: false, + validate: { + notEmpty: true, + }, + references: { + model: "student_learning", + key: "ID_STUDENT_LEARNING", + }, + }, + ANSWER_STUDENT: { + type: DataTypes.CHAR(1), + allowNull: false, + validate: { + notEmpty: true, + }, + }, + IS_CORRECT: { + type: DataTypes.INTEGER(1), + allowNull: false, + defaultValue: 0, + validate: { + min: 0, + max: 1, + }, + }, + RESULT_SCORE_STUDENT: { + type: DataTypes.INTEGER, + allowNull: true, + defaultValue: null, + }, + TIME_STUDENT_EXC: { + type: DataTypes.DATE, + allowNull: true, + defaultValue: DataTypes.NOW, + onUpdate: DataTypes.NOW, + }, + }, + { + timestamps: false, + tableName: "student_exercise", + } + ); + + return StdExercise; +}; + +export default StdExerciseModel; diff --git a/models/learningModels/stdLearningModel.js b/models/learningModels/stdLearningModel.js new file mode 100644 index 0000000..26d7c67 --- /dev/null +++ b/models/learningModels/stdLearningModel.js @@ -0,0 +1,92 @@ +import db from "../../database/db.js"; + +const StdLearningModel = (DataTypes) => { + const StdLearning = db.define( + "student_learning", + { + ID_STUDENT_LEARNING: { + type: DataTypes.UUID, + primaryKey: true, + defaultValue: DataTypes.UUIDV4, + allowNull: false, + validate: { + notEmpty: true, + }, + }, + ID: { + type: DataTypes.UUID, + allowNull: false, + validate: { + notEmpty: true, + }, + references: { + model: "users", + key: "ID", + }, + }, + ID_LEVEL: { + type: DataTypes.UUID, + allowNull: false, + validate: { + notEmpty: true, + }, + references: { + model: "level", + key: "ID_LEVEL", + }, + }, + STUDENT_START: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + }, + STUDENT_FINISH: { + type: DataTypes.DATE, + allowNull: true, + defaultValue: null, + }, + SCORE: { + type: DataTypes.INTEGER, + allowNull: true, + defaultValue: null, + }, + IS_PASS: { + type: DataTypes.INTEGER(1), + allowNull: false, + defaultValue: 0, + validate: { + min: 0, + max: 1, + }, + }, + NEXT_LEARNING: { + type: DataTypes.UUID, + allowNull: true, + defaultValue: null, + references: { + model: "level", + key: "ID_LEVEL", + }, + }, + FEEDBACK_STUDENT: { + type: DataTypes.STRING(200), + allowNull: true, + defaultValue: null, + }, + TIME_LEARNING: { + type: DataTypes.DATE, + allowNull: true, + defaultValue: DataTypes.NOW, + onUpdate: DataTypes.NOW, + }, + }, + { + timestamps: false, + tableName: "student_learning", + } + ); + + return StdLearning; +}; + +export default StdLearningModel; diff --git a/models/levelModel.js b/models/levelModel.js deleted file mode 100644 index 383c1a2..0000000 --- a/models/levelModel.js +++ /dev/null @@ -1,103 +0,0 @@ -import db from "../database/db.js"; - -const LevelModel = (DataTypes) => { - const Levels = db.define( - "m_levels", - { - id: { - type: DataTypes.INTEGER, - primaryKey: true, - autoIncrement: true, - validate: { - notEmpty: true, - }, - }, - title: { - type: DataTypes.STRING(100), - allowNull: false, - validate: { - notEmpty: true, - }, - }, - subject_id: { - type: DataTypes.INTEGER, - allowNull: false, - validate: { - notEmpty: true, - }, - references: { - model: 'm_subjects', // Name of the referenced table - key: 'id', // Key in the referenced table - }, - }, - topic_id: { - type: DataTypes.INTEGER, - allowNull: false, - validate: { - notEmpty: true, - }, - references: { - model: 'm_topics', // Name of the referenced table - key: 'id', // Key in the referenced table - }, - }, - is_pretest: { - type: DataTypes.TINYINT(1), - allowNull: true, - defaultValue: 0, - validate: { - min: 0, - max: 1, - }, - }, - content: { - type: DataTypes.STRING(200), - allowNull: true, - }, - video: { - type: DataTypes.STRING(200), - allowNull: true, - }, - audio: { - type: DataTypes.STRING(200), - allowNull: true, - }, - image: { - type: DataTypes.STRING(200), - allowNull: true, - }, - youtube: { - type: DataTypes.STRING(200), - allowNull: true, - }, - route1: { - type: DataTypes.INTEGER, - allowNull: false, - }, - route2: { - type: DataTypes.INTEGER, - allowNull: false, - }, - route3: { - type: DataTypes.INTEGER, - allowNull: false, - }, - route4: { - type: DataTypes.INTEGER, - allowNull: false, - }, - ts_entri: { - type: DataTypes.DATE, - allowNull: true, - defaultValue: DataTypes.NOW, - }, - }, - { - timestamps: false, // Disable Sequelize's automatic timestamp fields (createdAt, updatedAt) - tableName: "m_levels", // Ensure the table name matches the actual table name - } - ); - return Levels; -}; - -export default LevelModel; diff --git a/models/monitoringModels/classModel.js b/models/monitoringModels/classModel.js new file mode 100644 index 0000000..646c2fb --- /dev/null +++ b/models/monitoringModels/classModel.js @@ -0,0 +1,36 @@ +import db from "../../database/db.js"; + +const ClassModel = (DataTypes) => { + const Classes = db.define( + "class", + { + ID_CLASS: { + type: DataTypes.UUID, + primaryKey: true, + defaultValue: DataTypes.UUIDV4, + allowNull: false, + validate: { + notEmpty: true, + }, + }, + NAME_CLASS: { + type: DataTypes.STRING(100), + allowNull: false, + validate: { + notEmpty: true, + }, + }, + TOTAL_STUDENT: { + type: DataTypes.INTEGER, + allowNull: true, + }, + }, + { + timestamps: false, + tableName: "class", + } + ); + return Classes; +}; + +export default ClassModel; diff --git a/models/monitoringModels/monitoringModel.js b/models/monitoringModels/monitoringModel.js new file mode 100644 index 0000000..80336c7 --- /dev/null +++ b/models/monitoringModels/monitoringModel.js @@ -0,0 +1,56 @@ +import db from "../../database/db.js"; + +const MonitoringModel = (DataTypes) => { + const Monitorings = db.define( + "monitoring", + { + ID_MONITORING: { + type: DataTypes.UUID, + primaryKey: true, + defaultValue: DataTypes.UUIDV4, + allowNull: false, + validate: { + notEmpty: true, + }, + }, + ID_STUDENT_LEARNING: { + type: DataTypes.UUID, + allowNull: false, + validate: { + notEmpty: true, + }, + references: { + model: "student_learning", + key: "ID_STUDENT_LEARNING", + }, + }, + ID_CLASS: { + type: DataTypes.UUID, + allowNull: true, + references: { + model: "class", + key: "ID_CLASS", + }, + }, + ID_GURU: { + type: DataTypes.UUID, + allowNull: true, + references: { + model: "teacher", + key: "ID_GURU", + }, + }, + FEEDBACK_GURU: { + type: DataTypes.STRING(200), + allowNull: true, + }, + }, + { + timestamps: false, + tableName: "monitoring", + } + ); + return Monitorings; +}; + +export default MonitoringModel; diff --git a/models/subjectModel.js b/models/subjectModel.js deleted file mode 100644 index e8b3da5..0000000 --- a/models/subjectModel.js +++ /dev/null @@ -1,51 +0,0 @@ -import db from "../database/db.js"; - -const SubjectModel = (DataTypes) => { - const Subjects = db.define( - "m_subjects", - { - id: { - type: DataTypes.INTEGER, - primaryKey: true, - autoIncrement: true, - validate: { - notEmpty: true, - }, - }, - name: { - type: DataTypes.STRING(50), - allowNull: false, - validate: { - notEmpty: true, - }, - }, - description: { - type: DataTypes.TEXT, - allowNull: false, - validate: { - notEmpty: true, - }, - }, - icon: { - type: DataTypes.STRING(255), - allowNull: true, - }, - thumbnail: { - type: DataTypes.STRING(255), - allowNull: true, - }, - ts_entri: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: DataTypes.NOW, - }, - }, - { - timestamps: false, // Disable Sequelize's automatic timestamp fields (createdAt, updatedAt) - tableName: "m_subjects", // Ensure the table name matches the actual table name - } - ); - return Subjects; -}; - -export default SubjectModel; \ No newline at end of file diff --git a/models/topicModel.js b/models/topicModel.js deleted file mode 100644 index 7d5f784..0000000 --- a/models/topicModel.js +++ /dev/null @@ -1,47 +0,0 @@ -import db from "../database/db.js"; - -const TopicModel = (DataTypes) => { - const Topics = db.define( - "m_topics", - { - id: { - type: DataTypes.INTEGER, - primaryKey: true, - autoIncrement: true, - validate: { - notEmpty: true, - }, - }, - subject_id: { - type: DataTypes.INTEGER, - allowNull: false, - validate: { - notEmpty: true, - }, - references: { - model: 'm_subjects', // Name of the referenced table - key: 'id', // Key in the referenced table - }, - }, - title: { - type: DataTypes.STRING(255), - allowNull: false, - validate: { - notEmpty: true, - }, - }, - ts_entri: { - type: DataTypes.DATE, - allowNull: true, - defaultValue: DataTypes.NOW, - }, - }, - { - timestamps: false, // Disable Sequelize's automatic timestamp fields (createdAt, updatedAt) - tableName: "m_topics", // Ensure the table name matches the actual table name - } - ); - return Topics; -}; - -export default TopicModel; \ No newline at end of file diff --git a/models/usersModels/studentModel.js b/models/usersModels/studentModel.js new file mode 100644 index 0000000..752e053 --- /dev/null +++ b/models/usersModels/studentModel.js @@ -0,0 +1,44 @@ +import db from "../../database/db.js"; + +const StudentModel = (DataTypes) => { + const Students = db.define( + "student", + { + ID_SISWA: { + type: DataTypes.UUID, + primaryKey: true, + defaultValue: DataTypes.UUIDV4, + allowNull: false, + validate: { + notEmpty: true, + }, + }, + ID: { + type: DataTypes.UUID, + allowNull: false, + validate: { + notEmpty: true, + }, + references: { + model: "users", + key: "ID", + }, + }, + NISN: { + type: DataTypes.INTEGER, + allowNull: false, + unique: true, + validate: { + notEmpty: true, + }, + }, + }, + { + timestamps: false, + tableName: "student", + } + ); + return Students; +}; + +export default StudentModel; diff --git a/models/usersModels/teacherModel.js b/models/usersModels/teacherModel.js new file mode 100644 index 0000000..2131d43 --- /dev/null +++ b/models/usersModels/teacherModel.js @@ -0,0 +1,44 @@ +import db from "../../database/db.js"; + +const TeacherModel = (DataTypes) => { + const Teachers = db.define( + "teacher", + { + ID_GURU: { + type: DataTypes.UUID, + primaryKey: true, + defaultValue: DataTypes.UUIDV4, + allowNull: false, + validate: { + notEmpty: true, + }, + }, + ID: { + type: DataTypes.UUID, + allowNull: false, + validate: { + notEmpty: true, + }, + references: { + model: "users", + key: "ID", + }, + }, + NIP: { + type: DataTypes.INTEGER, + allowNull: false, + unique: true, + validate: { + notEmpty: true, + }, + }, + }, + { + timestamps: false, + tableName: "teacher", + } + ); + return Teachers; +}; + +export default TeacherModel; diff --git a/models/userModel.js b/models/usersModels/userModel.js similarity index 54% rename from models/userModel.js rename to models/usersModels/userModel.js index bb1669c..f6a5b2e 100644 --- a/models/userModel.js +++ b/models/usersModels/userModel.js @@ -1,10 +1,10 @@ -import db from "../database/db.js"; +import db from "../../database/db.js"; const UserModel = (DataTypes) => { const Users = db.define( "users", { - id: { + ID: { type: DataTypes.UUID, primaryKey: true, defaultValue: DataTypes.UUIDV4, @@ -13,14 +13,14 @@ const UserModel = (DataTypes) => { notEmpty: true, }, }, - name: { + NAME_USERS: { type: DataTypes.STRING, allowNull: false, validate: { notEmpty: true, }, }, - email: { + EMAIL: { type: DataTypes.STRING, allowNull: false, unique: true, @@ -29,36 +29,22 @@ const UserModel = (DataTypes) => { isEmail: true, }, }, - email_verified_at: { - type: DataTypes.DATE, - allowNull: true, - }, - password: { + PASSWORD: { type: DataTypes.STRING, allowNull: false, }, - roles: { + ROLE: { type: DataTypes.STRING, allowNull: true, }, - remember_token: { - type: DataTypes.STRING(100), + PICTURE: { + type: DataTypes.STRING, allowNull: true, }, - created_at: { - type: DataTypes.DATE, - allowNull: true, - defaultValue: DataTypes.NOW, - }, - updated_at: { - type: DataTypes.DATE, - allowNull: true, - defaultValue: DataTypes.NOW, - }, }, { - timestamps: false, // Disable Sequelize's automatic timestamp fields (createdAt, updatedAt) - tableName: "users", // Ensure the table name matches the actual table name + timestamps: false, + tableName: "users", } ); return Users; diff --git a/package-lock.json b/package-lock.json index b3d8ac4..5bc2034 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "dotenv": "^16.4.5", "express": "^4.19.2", "jsonwebtoken": "^9.0.2", + "moment-timezone": "^0.5.45", "multer": "^1.4.5-lts.1", "mysql2": "^3.11.0", "nodemailer": "^6.9.14", diff --git a/package.json b/package.json index 6fb0bce..7f6212f 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "dotenv": "^16.4.5", "express": "^4.19.2", "jsonwebtoken": "^9.0.2", + "moment-timezone": "^0.5.45", "multer": "^1.4.5-lts.1", "mysql2": "^3.11.0", "nodemailer": "^6.9.14", diff --git a/public/uploads/level/audio/14-level1-audio-bc8a8de59edc4085e9edff682505b158.mp3 b/public/uploads/level/audio/14-level1-audio-bc8a8de59edc4085e9edff682505b158.mp3 deleted file mode 100644 index 03b707c..0000000 Binary files a/public/uploads/level/audio/14-level1-audio-bc8a8de59edc4085e9edff682505b158.mp3 and /dev/null differ diff --git a/public/uploads/level/image/14-level1-image-746ab602308a6ef95716b44cb76d0949.jpeg b/public/uploads/level/image/14-level1-image-746ab602308a6ef95716b44cb76d0949.jpeg deleted file mode 100644 index b2db4e7..0000000 Binary files a/public/uploads/level/image/14-level1-image-746ab602308a6ef95716b44cb76d0949.jpeg and /dev/null differ diff --git a/public/uploads/level/video/14-level1-video-0b5c3cdc65ce23582fc08b433263ee8d.mp4 b/public/uploads/level/video/14-level1-video-0b5c3cdc65ce23582fc08b433263ee8d.mp4 deleted file mode 100644 index 1e71281..0000000 Binary files a/public/uploads/level/video/14-level1-video-0b5c3cdc65ce23582fc08b433263ee8d.mp4 and /dev/null differ diff --git a/public/uploads/subject/Menulis-icon-2802b5de.png b/public/uploads/subject/Menulis-icon-2802b5de.png deleted file mode 100644 index 73653c9..0000000 Binary files a/public/uploads/subject/Menulis-icon-2802b5de.png and /dev/null differ diff --git a/public/uploads/subject/Menulis-thumbnail-448b4bd5.jpeg b/public/uploads/subject/Menulis-thumbnail-448b4bd5.jpeg deleted file mode 100644 index 953f28d..0000000 Binary files a/public/uploads/subject/Menulis-thumbnail-448b4bd5.jpeg and /dev/null differ diff --git a/response.js b/response.js index ccc7f58..d37fc03 100644 --- a/response.js +++ b/response.js @@ -1,8 +1,9 @@ const response = (statusCode, data, message, res) => { res.status(statusCode).json({ payload: data, + statusCode: statusCode, message: message, }); }; -export default response; +export default response; \ No newline at end of file diff --git a/routes/auth.js b/routes/auth.js deleted file mode 100644 index 980cd82..0000000 --- a/routes/auth.js +++ /dev/null @@ -1,16 +0,0 @@ -import express from "express"; -import { registerUser, loginUser, logoutUser, forgotPassword, resetPassword } from "../controllers/auth.js"; - -const router = express.Router(); - -router.post("/register", registerUser); - -router.post("/login", loginUser); - -router.post("/logout", logoutUser); - -router.post("/forgotPassword", forgotPassword) - -router.post("/resetPassword", resetPassword) - -export default router; \ No newline at end of file diff --git a/routes/auth/auth.js b/routes/auth/auth.js new file mode 100644 index 0000000..d98f4c1 --- /dev/null +++ b/routes/auth/auth.js @@ -0,0 +1,21 @@ +import express from "express"; +import { registerTeacher, registerStudent, registerAdmin, loginUser, logoutUser, forgotPassword, resetPassword } from "../../controllers/auth/auth.js"; +import { verifyLoginUser, adminOnly } from "../../middlewares/User/authUser.js"; + +const router = express.Router(); + +router.post("/register/teacher", registerTeacher); + +router.post("/register/student", registerStudent); + +router.post("/register/admin", verifyLoginUser, adminOnly, registerAdmin); + +router.post("/login", loginUser); + +router.post("/logout", logoutUser); + +router.post("/forgotPassword", forgotPassword) + +router.post("/resetPassword", resetPassword) + +export default router; \ No newline at end of file diff --git a/routes/contents/exercise.js b/routes/contents/exercise.js new file mode 100644 index 0000000..03cc01f --- /dev/null +++ b/routes/contents/exercise.js @@ -0,0 +1,34 @@ +import express from "express"; +import { getExercises, getExercisesForAdmin, getExerciseById, deleteExerciseById, deleteExerciseFileById } from "../../controllers/contentControllers/exercise.js"; +import { createMultipleChoicesExercise, updateMultipleChoicesExerciseById } from "../../controllers/exerciseTypesControllers/multipleChoices.js"; +import { createMatchingPairsExercise, updateMatchingPairsExerciseById } from "../../controllers/exerciseTypesControllers/matchingPairs.js"; +import { createTrueFalseExercise, updateTrueFalseExerciseById } from "../../controllers/exerciseTypesControllers/trueFalse.js"; +import { verifyLoginUser, adminOnly } from "../../middlewares/User/authUser.js"; +import handleUpload from '../../middlewares/uploadExercise.js'; + + +const router = express.Router(); + +router.get("/exercise", verifyLoginUser, getExercises); + +router.get("/exercise/admin", verifyLoginUser, adminOnly, getExercisesForAdmin); + +router.get("/exercise/:id", verifyLoginUser, getExerciseById); + +router.post("/exercise/multiple-choices", verifyLoginUser, adminOnly, handleUpload, createMultipleChoicesExercise); + +router.post("/exercise/matching-pairs", verifyLoginUser, adminOnly, handleUpload, createMatchingPairsExercise); + +router.post("/exercise/true-false", verifyLoginUser, adminOnly, handleUpload, createTrueFalseExercise); + +router.put("/exercise/multiple-choices/:id", verifyLoginUser, adminOnly, handleUpload, updateMultipleChoicesExerciseById); + +router.put("/exercise/matching-pairs/:id", verifyLoginUser, adminOnly, handleUpload, updateMatchingPairsExerciseById); + +router.put("/exercise/true-false/:id", verifyLoginUser, adminOnly, handleUpload, updateTrueFalseExerciseById); + +router.delete("/exercise/:id", verifyLoginUser, adminOnly, deleteExerciseById); + +router.delete("/exercise/file/:id", verifyLoginUser, adminOnly, deleteExerciseFileById); + +export default router \ No newline at end of file diff --git a/routes/contents/level.js b/routes/contents/level.js new file mode 100644 index 0000000..55a0b85 --- /dev/null +++ b/routes/contents/level.js @@ -0,0 +1,25 @@ +import express from "express"; +// import { getAllLevels, getAllLevelById, getLevels, getLevelById, createLevel, updateLevelById, deleteLevelById, getRoutes, getRouteById, updateRouteById } from "../controllers/level.js"; +import { getLevels, getLevelById, createLevel, updateLevelById, deleteLevelById } from "../../controllers/contentControllers/level.js"; +import { verifyLoginUser, adminOnly } from "../../middlewares/User/authUser.js"; +import handleUpload from '../../middlewares/Level/uploadLevel.js'; +import {checkLevelsPerTopic, autoCalculateRoutes, getSectionAndTopicByLevelId } from '../../middlewares/Level/checkLevel.js'; + + +const router = express.Router(); + +// router.get("/levels", verifyLoginUser, adminOnly, getAllLevels); + +// router.get("/levels/:id", verifyLoginUser, adminOnly, getAllLevelById); + +router.get("/level", verifyLoginUser, getLevels); + +router.get("/level/:id", verifyLoginUser, getLevelById); + +router.post("/level", verifyLoginUser, adminOnly, handleUpload, checkLevelsPerTopic, autoCalculateRoutes, createLevel); + +router.put("/level/:id", verifyLoginUser, adminOnly, handleUpload, getSectionAndTopicByLevelId, autoCalculateRoutes, updateLevelById); + +router.delete("/level/:id", verifyLoginUser, adminOnly, getSectionAndTopicByLevelId, deleteLevelById); + +export default router \ No newline at end of file diff --git a/routes/contents/section.js b/routes/contents/section.js new file mode 100644 index 0000000..60ae065 --- /dev/null +++ b/routes/contents/section.js @@ -0,0 +1,19 @@ +import express from "express"; +import handleUpload from '../../middlewares/uploadSection.js'; +import { getSections, getSectionById, createSection, updateSectionById, deleteSectionById } from "../../controllers/contentControllers/section.js"; +import { verifyLoginUser, adminOnly } from "../../middlewares/User/authUser.js"; + + +const router = express.Router(); + +router.get("/section", verifyLoginUser, getSections); + +router.get("/section/:id", verifyLoginUser, getSectionById); + +router.post("/section", verifyLoginUser, adminOnly, handleUpload, createSection); + +router.put('/section/update/:id', verifyLoginUser, adminOnly, handleUpload, updateSectionById); + +router.delete('/section/delete/:id', verifyLoginUser, adminOnly, deleteSectionById); + +export default router \ No newline at end of file diff --git a/routes/contents/topic.js b/routes/contents/topic.js new file mode 100644 index 0000000..a0748eb --- /dev/null +++ b/routes/contents/topic.js @@ -0,0 +1,18 @@ +import express from "express"; +import { getTopics, getTopicById, createTopic, updateTopicById, deleteTopicById } from "../../controllers/contentControllers/topic.js"; +import { verifyLoginUser, adminOnly } from "../../middlewares/User/authUser.js"; + + +const router = express.Router(); + +router.get("/topic", verifyLoginUser, getTopics); + +router.get("/topic/:id", verifyLoginUser, getTopicById); + +router.post("/topic", verifyLoginUser, adminOnly, createTopic); + +router.put("/topic/:id", verifyLoginUser, adminOnly, updateTopicById); + +router.delete("/topic/:id", verifyLoginUser, adminOnly, deleteTopicById); + +export default router \ No newline at end of file diff --git a/routes/index.js b/routes/index.js index 888767c..8613e7b 100644 --- a/routes/index.js +++ b/routes/index.js @@ -1,15 +1,25 @@ import express from "express"; -import user_routes from "./user.js"; -import auth_routes from "./auth.js"; -import subject_routes from "./subject.js"; -import topic_routes from "./topic.js"; -import level_routes from "./level.js"; +import user_routes from "./user/user.js"; +import auth_routes from "./auth/auth.js"; +import section_routes from "./contents/section.js"; +import topic_routes from "./contents/topic.js"; +import level_routes from "./contents/level.js"; +import exercise_routes from "./contents/exercise.js"; +import stdLearning_routes from "./learning/stdLearning.js"; +import stdExercise_routes from "./learning/stdExercise.js"; +import class_routes from "./monitoring/class.js"; +import monitoring_routes from "./monitoring/monitoring.js"; const route = express(); route.use(user_routes); route.use(auth_routes); -route.use(subject_routes); +route.use(section_routes); route.use(topic_routes); route.use(level_routes); +route.use(exercise_routes); +route.use(stdLearning_routes); +route.use(stdExercise_routes); +route.use(class_routes); +route.use(monitoring_routes); export default route; diff --git a/routes/learning/stdExercise.js b/routes/learning/stdExercise.js new file mode 100644 index 0000000..403db07 --- /dev/null +++ b/routes/learning/stdExercise.js @@ -0,0 +1,15 @@ +import express from "express"; +import { getStdExercises, getStdExerciseById, stdAnswerExercise } from "../../controllers/learningControllers/stdExercise.js"; +import { verifyLoginUser } from "../../middlewares/User/authUser.js"; +import { updateStdLearningById } from "../../controllers/learningControllers/stdLearning.js"; +import { checkCorrectAnswers,calculateScore, checkFirstFiveCorrect, nextLearning } from "../../middlewares/autoGrading.js"; + +const router = express.Router(); + +router.get("/stdExercise", verifyLoginUser, getStdExercises); + +router.get("/stdExercise/:id", verifyLoginUser, getStdExerciseById); + +router.post("/stdExercise", verifyLoginUser, stdAnswerExercise, checkCorrectAnswers, calculateScore, checkFirstFiveCorrect, nextLearning, updateStdLearningById); + +export default router \ No newline at end of file diff --git a/routes/learning/stdLearning.js b/routes/learning/stdLearning.js new file mode 100644 index 0000000..201bb53 --- /dev/null +++ b/routes/learning/stdLearning.js @@ -0,0 +1,19 @@ +import express from "express"; +import { getStdLearnings, getStdLearningById, createStdLearning, learningHistory, learningHistoryBySectionId, learningHistoryByTopicId } from "../../controllers/learningControllers/stdLearning.js"; +import { verifyLoginUser } from "../../middlewares/User/authUser.js"; + +const router = express.Router(); + +router.get("/stdLearning", verifyLoginUser, getStdLearnings); + +router.get("/stdLearning/:id", verifyLoginUser, getStdLearningById); + +router.get("/learningHistory", verifyLoginUser, learningHistory); + +router.get("/learningHistory/section/:sectionId", verifyLoginUser, learningHistoryBySectionId); + +router.get("/learningHistory/topic/:topicId", verifyLoginUser, learningHistoryByTopicId); + +router.post("/stdLearning", verifyLoginUser, createStdLearning); + +export default router \ No newline at end of file diff --git a/routes/level.js b/routes/level.js deleted file mode 100644 index 140a104..0000000 --- a/routes/level.js +++ /dev/null @@ -1,30 +0,0 @@ -import express from "express"; -import { getAllLevels, getAllLevelById, getLevels, getLevelById, createLevel, updateLevelById, deleteLevelById, getRoutes, getRouteById, updateRouteById } from "../controllers/level.js"; -import { verifyLoginUser, adminOnly, teacherOnly } from "../middlewares/authUser.js"; -import handleUpload from '../middlewares/uploadLevel.js'; -import {checkMaxLevelsPerTopic } from '../middlewares/checkLevel.js'; - - -const router = express.Router(); - -router.get("/levels", getAllLevels); - -router.get("/levels/:id", getAllLevelById); - -router.get("/level", getLevels); - -router.get("/level/:id", getLevelById); - -router.post("/level", handleUpload, checkMaxLevelsPerTopic, createLevel); - -router.put("/level/:id", handleUpload, updateLevelById); - -router.delete("/level/:id", deleteLevelById); - -router.get("/route", getRoutes); - -router.get("/route/:id", getRouteById); - -router.put("/route/:id", updateRouteById); - -export default router \ No newline at end of file diff --git a/routes/monitoring/class.js b/routes/monitoring/class.js new file mode 100644 index 0000000..51cb5a7 --- /dev/null +++ b/routes/monitoring/class.js @@ -0,0 +1,19 @@ +import express from "express"; +import { getClasses, getClassById, createClass, updateClassById, deleteClassById, updateStudentClassByName } from "../../controllers/monitoringControllers/class.js"; +import { verifyLoginUser } from "../../middlewares/User/authUser.js"; + +const router = express.Router(); + +router.get("/classes", verifyLoginUser, getClasses); + +router.get("/class/:id", verifyLoginUser, getClassById); + +router.post("/class", verifyLoginUser, createClass); + +router.put("/class/update/:id", verifyLoginUser, updateClassById); + +router.delete("/class/delete/:id", verifyLoginUser, deleteClassById); + +router.post("/class/student/update", verifyLoginUser, updateStudentClassByName); + +export default router \ No newline at end of file diff --git a/routes/monitoring/monitoring.js b/routes/monitoring/monitoring.js new file mode 100644 index 0000000..03398d3 --- /dev/null +++ b/routes/monitoring/monitoring.js @@ -0,0 +1,13 @@ +import express from "express"; +import { monitoringStudentsProgress, monitoringStudentProgressById, monitoringFeedback } from "../../controllers/monitoringControllers/monitoring.js"; +import { verifyLoginUser, adminOrTeacherOnly } from "../../middlewares/User/authUser.js"; + +const router = express.Router(); + +router.get("/monitoring/progress", verifyLoginUser, adminOrTeacherOnly, monitoringStudentsProgress); + +router.get("/monitoring/progress/:id", verifyLoginUser, adminOrTeacherOnly, monitoringStudentProgressById); + +router.post("/monitoring/feedback/:id", verifyLoginUser, adminOrTeacherOnly, monitoringFeedback); + +export default router \ No newline at end of file diff --git a/routes/subject.js b/routes/subject.js deleted file mode 100644 index 9cff2de..0000000 --- a/routes/subject.js +++ /dev/null @@ -1,19 +0,0 @@ -import express from "express"; -import handleUpload from '../middlewares/uploadSubject.js'; -import { getSubjects, getSubjectById, createSubject, updateSubjectById, deleteSubjectById } from "../controllers/subject.js"; -import { verifyLoginUser, adminOnly, teacherOnly } from "../middlewares/authUser.js"; - - -const router = express.Router(); - -router.get("/subject", verifyLoginUser, adminOnly, getSubjects); - -router.get("/subject/:id", getSubjectById); - -router.post("/subject", handleUpload, createSubject); - -router.put('/subject/:id', handleUpload, updateSubjectById); - -router.delete('/subject/:id', deleteSubjectById); - -export default router \ No newline at end of file diff --git a/routes/topic.js b/routes/topic.js deleted file mode 100644 index 623cae3..0000000 --- a/routes/topic.js +++ /dev/null @@ -1,18 +0,0 @@ -import express from "express"; -import { getTopics, getTopicById, createTopic, updateTopicById, deleteTopicById } from "../controllers/topic.js"; -import { verifyLoginUser, adminOnly, teacherOnly } from "../middlewares/authUser.js"; - - -const router = express.Router(); - -router.get("/topic", getTopics); - -router.get("/topic/:id", getTopicById); - -router.post("/topic", createTopic); - -router.put("/topic/:id", updateTopicById); - -router.delete("/topic/:id", deleteTopicById); - -export default router \ No newline at end of file diff --git a/routes/user.js b/routes/user.js deleted file mode 100644 index f473bff..0000000 --- a/routes/user.js +++ /dev/null @@ -1,16 +0,0 @@ -import express from "express"; -import { getUsers, getUserById, updateUserById, deleteUserById } from "../controllers/user.js"; -import { verifyLoginUser, adminOnly, teacherOnly } from "../middlewares/authUser.js"; - - -const router = express.Router(); - -router.get("/user", verifyLoginUser, adminOnly, getUsers); - -router.get("/user/:id", getUserById); - -router.post("/user/update/:id", updateUserById); - -router.delete("/user/delete/:id", deleteUserById); - -export default router diff --git a/routes/user/user.js b/routes/user/user.js new file mode 100644 index 0000000..81c061f --- /dev/null +++ b/routes/user/user.js @@ -0,0 +1,29 @@ +import express from "express"; +import { getUsers, getAdmins, getTeachers, getStudents, getUserById, getUserByName, updateUserById, updateUserPasswordById, deleteUserById, getMe } from "../../controllers/usersControllers/user.js"; +import { verifyLoginUser, adminOnly, adminOrTeacherOnly } from "../../middlewares/User/authUser.js"; +import handleUpload from "../../middlewares/User/uploadUser.js"; + + +const router = express.Router(); + +router.get("/user", verifyLoginUser, adminOnly, getUsers); + +router.get("/user/admin", verifyLoginUser, adminOnly, getAdmins); + +router.get("/user/teacher", verifyLoginUser, adminOnly, getTeachers); + +router.get("/user/student", verifyLoginUser, adminOrTeacherOnly, getStudents); + +router.get("/user/:id", verifyLoginUser, getUserById); + +router.get("/user/name/:name", verifyLoginUser, adminOrTeacherOnly, getUserByName); + +router.get("/getMe", verifyLoginUser, getMe); + +router.post("/user/update/:id", verifyLoginUser, handleUpload, updateUserById); + +router.post("/user/update/password/:id", verifyLoginUser, updateUserPasswordById); + +router.delete("/user/delete/:id", verifyLoginUser, adminOnly, deleteUserById); + +export default router