feat: student learning API

This commit is contained in:
elangptra 2024-09-13 20:03:35 +07:00
parent a27ea19c5b
commit 56dad54d89
70 changed files with 5577 additions and 1381 deletions

View File

@ -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" });
}
}
};

371
controllers/auth/auth.js Normal file
View File

@ -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);
}
}
};

View File

@ -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);
}
};

View File

@ -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;
};

View File

@ -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);
}
};

View File

@ -1,5 +1,5 @@
import response from "../response.js"; import response from "../../response.js";
import models from "../models/index.js"; import models from "../../models/index.js";
export const getTopics = async (req, res) => { export const getTopics = async (req, res) => {
try { try {
@ -23,33 +23,35 @@ export const getTopicById = async (req, res) => {
response(200, topic, "Success", res); response(200, topic, "Success", res);
} catch (error) { } catch (error) {
console.log(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) => { 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 (!ID_SECTION) {
if (!subject_id) { return response(400, null, "Section ID is required", res);
return response(400, null, "Subject ID is required", res);
} }
// Validate title if (!NAME_TOPIC) {
if (!title) { return response(400, null, "Topic name is required", res);
return response(400, null, "Title is required", res); }
if (!DESCRIPTION_TOPIC) {
return response(400, null, "Topic Description is required", res);
} }
try { try {
// Verify that the subject_id exists in the m_subjects table const section = await models.Section.findByPk(ID_SECTION);
const subject = await models.Subject.findByPk(subject_id); if (!section) {
if (!subject) { return response(404, null, "Section not found", res);
return response(404, null, "Subject not found", res);
} }
const newTopic = await models.Topic.create({ const newTopic = await models.Topic.create({
subject_id, ID_SECTION,
title, NAME_TOPIC,
DESCRIPTION_TOPIC,
}); });
response(201, newTopic, "Topic created successfully", res); response(201, newTopic, "Topic created successfully", res);
@ -61,31 +63,31 @@ export const createTopic = async (req, res) => {
export const updateTopicById = async (req, res) => { export const updateTopicById = async (req, res) => {
const { id } = req.params; const { id } = req.params;
const { subject_id, title } = req.body; const { ID_SECTION, NAME_TOPIC, DESCRIPTION_TOPIC } = req.body;
try { try {
// Find the topic by its ID
const topic = await models.Topic.findByPk(id); const topic = await models.Topic.findByPk(id);
if (!topic) { if (!topic) {
return response(404, null, "Topic not found", res); return response(404, null, "Topic not found", res);
} }
// Validate and update subject_id if provided if (ID_SECTION) {
if (subject_id) { const section = await models.Section.findByPk(ID_SECTION);
const subject = await models.Subject.findByPk(subject_id); if (!section) {
if (!subject) { return response(404, null, "Section not found", res);
return response(404, null, "Subject not found", res);
} }
topic.subject_id = subject_id; topic.ID_SECTION = ID_SECTION;
} }
// Validate and update title if provided if (NAME_TOPIC) {
if (title) { topic.NAME_TOPIC = NAME_TOPIC;
topic.title = title; }
if (DESCRIPTION_TOPIC) {
topic.DESCRIPTION_TOPIC = DESCRIPTION_TOPIC;
} }
// Save the updated topic
await topic.save(); await topic.save();
response(200, topic, "Topic updated successfully", res); response(200, topic, "Topic updated successfully", res);
@ -99,14 +101,12 @@ export const deleteTopicById = async (req, res) => {
const { id } = req.params; const { id } = req.params;
try { try {
// Find the topic by its ID
const topic = await models.Topic.findByPk(id); const topic = await models.Topic.findByPk(id);
if (!topic) { if (!topic) {
return response(404, null, "Topic not found", res); return response(404, null, "Topic not found", res);
} }
// Delete the topic
await topic.destroy(); await topic.destroy();
response(200, null, "Topic deleted successfully", res); response(200, null, "Topic deleted successfully", res);

View File

@ -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);
}
};

View File

@ -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);
}
};

View File

@ -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);
}
};

View File

@ -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
);
}
};

View File

@ -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);
}
};

View File

@ -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);
}
};

View File

@ -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);
}
};

View File

@ -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);
}
};

View File

@ -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);
}
};

View File

@ -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" });
}
};

View File

@ -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);
}
};

View File

@ -12,6 +12,7 @@ const db = new Sequelize(name, username, password, {
host: host, host: host,
dialect: "mysql", dialect: "mysql",
logging: false, logging: false,
timezone: '+07:00',
}); });
const testConnection = async () => { const testConnection = async () => {

View File

@ -1,23 +1,33 @@
import express from 'express'; import express from "express";
import cors from 'cors'; import cors from "cors";
import dotenv from 'dotenv'; import dotenv from "dotenv";
import { testConnection } from "./database/db.js"; import { testConnection } from "./database/db.js";
import router from "./routes/index.js"; import router from "./routes/index.js";
import cookieParser from 'cookie-parser'; import cookieParser from "cookie-parser";
dotenv.config(); dotenv.config();
const app = express(); 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(cookieParser());
app.use(express.json()); app.use(express.json());
app.use(express.urlencoded({ extended: true })); app.use(express.urlencoded({ extended: true }));
app.use(router); app.use(router);
// Serve static files from the uploads directory // Serve static files from the uploads directory
app.use(express.static('public')); app.use(express.static("public"));
app.listen(process.env.APP_PORT, () => { app.listen(process.env.APP_PORT, () => {
testConnection(); testConnection();
console.log(`Server running on port http://localhost:${process.env.APP_PORT}`); console.log(
}) `Server running on port http://localhost:${process.env.APP_PORT}`
);
});

View File

@ -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);
}
};

View File

@ -2,12 +2,10 @@ import multer from "multer";
import crypto from "crypto"; import crypto from "crypto";
import path from "path"; import path from "path";
import fs from "fs"; import fs from "fs";
import response from "../response.js"; import response from "../../response.js";
// Setup memory storage for Multer
const memoryStorage = multer.memoryStorage(); const memoryStorage = multer.memoryStorage();
// Filter untuk membatasi tipe file dan ukuran file
const fileFilter = (req, file, cb) => { const fileFilter = (req, file, cb) => {
const ext = path.extname(file.originalname).toLowerCase(); 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({ const upload = multer({
storage: memoryStorage, storage: memoryStorage,
fileFilter, fileFilter,
limits: { limits: {
fileSize: 100 * 1024 * 1024, // Total file size limit if needed fileSize: 100 * 1024 * 1024,
}, },
}).fields([ }).fields([
{ name: "video", maxCount: 1 }, { name: "video", maxCount: 1 },
@ -69,7 +66,6 @@ const upload = multer({
{ name: "image", maxCount: 1 }, { name: "image", maxCount: 1 },
]); ]);
// Middleware untuk menangani upload dan pengecekan file size
const handleUpload = (req, res, next) => { const handleUpload = (req, res, next) => {
upload(req, res, (err) => { upload(req, res, (err) => {
if (err) { if (err) {
@ -85,7 +81,6 @@ const handleUpload = (req, res, next) => {
let validFiles = true; let validFiles = true;
let errorMessages = []; let errorMessages = [];
// Validate file sizes
if (video && video.size > 30 * 1024 * 1024) { if (video && video.size > 30 * 1024 * 1024) {
validFiles = false; validFiles = false;
video.buffer = null; video.buffer = null;
@ -105,11 +100,9 @@ const handleUpload = (req, res, next) => {
} }
if (validFiles) { if (validFiles) {
// Attach files to the request object for further processing
req.filesToSave = { video, audio, image }; req.filesToSave = { video, audio, image };
next(); next();
} else { } else {
// Clear file buffers and return error response with specific messages
clearFileBuffers({ video, audio, image }); clearFileBuffers({ video, audio, image });
return response(400, null, errorMessages.join("; "), res); 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) => { export const clearFileBuffers = (files) => {
for (const file of Object.values(files)) { for (const file of Object.values(files)) {
if (file && file.buffer) { 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 return crypto
.createHash("md5") .createHash("md5")
.update(subjectId + filename + bufferLength) .update(sectionId + topicId + filename + bufferLength)
.digest("hex"); .digest("hex");
}; };
// Function to save files to disk export const saveFileToDisk = (file, type, topicId, sectionId, levelId) => {
export const saveFileToDisk = (file, type, title, topicId, subjectId) => {
const formattedTitle = title.replace(/\s+/g, '').toLowerCase();
const ext = path.extname(file.originalname); const ext = path.extname(file.originalname);
const hash = generateHash(subjectId, file.originalname, file.buffer.length); const hash = generateHash(sectionId, topicId, file.originalname, file.buffer.length);
const filename = `${topicId}-${formattedTitle}-${type}-${hash}${ext}`; const filename = `${type}-${levelId}-${hash}${ext}`;
let folderPath; let folderPath;
switch (type) { switch (type) {

View File

@ -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();
};

View File

@ -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;

View File

@ -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();
};

275
middlewares/autoGrading.js Normal file
View File

@ -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);
}
};

View File

@ -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);
}
};

View File

@ -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;

View File

@ -23,11 +23,8 @@ const fileFilter = (req, file, cb) => {
const upload = multer({ const upload = multer({
storage: memoryStorage, storage: memoryStorage,
fileFilter, fileFilter,
limits: { fileSize: 10 * 1024 * 1024 }, // Limit file size to 5MB limits: { fileSize: 5 * 1024 * 1024 },
}).fields([ }).fields([{ name: "thumbnail", maxCount: 1 }]);
{ name: "icon", maxCount: 1 },
{ name: "thumbnail", maxCount: 1 },
]);
const handleUpload = (req, res, next) => { const handleUpload = (req, res, next) => {
upload(req, res, (err) => { upload(req, res, (err) => {
@ -36,20 +33,12 @@ const handleUpload = (req, res, next) => {
} }
const files = req.files; const files = req.files;
const icon = files?.icon ? files.icon[0] : null;
const thumbnail = files?.thumbnail ? files.thumbnail[0] : null; const thumbnail = files?.thumbnail ? files.thumbnail[0] : null;
try { try {
let validFiles = true; let validFiles = true;
let errorMessages = []; 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) { if (thumbnail && thumbnail.size > 5 * 1024 * 1024) {
validFiles = false; validFiles = false;
thumbnail.buffer = null; thumbnail.buffer = null;
@ -57,15 +46,15 @@ const handleUpload = (req, res, next) => {
} }
if (validFiles) { if (validFiles) {
req.filesToSave = { icon, thumbnail }; req.filesToSave = { thumbnail };
next(); next();
} else { } else {
clearFileBuffers({ icon, thumbnail }); clearFileBuffers({ thumbnail });
return response(400, null, errorMessages.join(", "), res); return response(400, null, errorMessages.join(", "), res);
} }
} catch (error) { } catch (error) {
console.log(error); console.log(error);
clearFileBuffers({ icon, thumbnail }); clearFileBuffers({ thumbnail });
return response(500, null, "Internal Server Error", res); 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 ext = path.extname(file.originalname);
const hash = crypto const hash = crypto
.createHash("md5") .createHash("md5")
.update(subjectName + file.originalname + file.buffer.length.toString()) .update(file.originalname + file.buffer.length.toString())
.digest("hex") .digest("hex");
.slice(0, 8); const filename = `${fieldName}-${idSection}-${hash}${ext}`;
const filename = `${fieldName}-${hash}${ext}`; const folderPath = path.join("public/uploads/section");
const folderPath = path.join("public/uploads/subject");
if (!fs.existsSync(folderPath)) { if (!fs.existsSync(folderPath)) {
fs.mkdirSync(folderPath, { recursive: true }); fs.mkdirSync(folderPath, { recursive: true });

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -1,19 +1,149 @@
import { Sequelize } from "sequelize"; import { Sequelize } from "sequelize";
import UserModel from "./userModel.js"; import db from "../database/db.js";
import SubjectModel from "./subjectModel.js"; import UserModel from "./usersModels/userModel.js";
import TopicModel from "./topicModel.js"; import TeacherModel from "./usersModels/teacherModel.js";
import LevelModel from "./levelModel.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 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 = { const models = {
User: UserModel(Sequelize.DataTypes), User,
Subject: SubjectModel(Sequelize.DataTypes), Teacher,
Topic: TopicModel(Sequelize.DataTypes), Student,
Level: LevelModel(Sequelize.DataTypes), Section,
Topic,
Level,
Exercise,
MultipleChoices,
MatchingPairs,
TrueFalse,
StdLearning,
StdExercise,
Class,
Monitoring,
Sequelize, Sequelize,
Op, Op,
db,
}; };
export default models; export default models;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -1,10 +1,10 @@
import db from "../database/db.js"; import db from "../../database/db.js";
const UserModel = (DataTypes) => { const UserModel = (DataTypes) => {
const Users = db.define( const Users = db.define(
"users", "users",
{ {
id: { ID: {
type: DataTypes.UUID, type: DataTypes.UUID,
primaryKey: true, primaryKey: true,
defaultValue: DataTypes.UUIDV4, defaultValue: DataTypes.UUIDV4,
@ -13,14 +13,14 @@ const UserModel = (DataTypes) => {
notEmpty: true, notEmpty: true,
}, },
}, },
name: { NAME_USERS: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: false,
validate: { validate: {
notEmpty: true, notEmpty: true,
}, },
}, },
email: { EMAIL: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: false,
unique: true, unique: true,
@ -29,36 +29,22 @@ const UserModel = (DataTypes) => {
isEmail: true, isEmail: true,
}, },
}, },
email_verified_at: { PASSWORD: {
type: DataTypes.DATE,
allowNull: true,
},
password: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: false,
}, },
roles: { ROLE: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: true, allowNull: true,
}, },
remember_token: { PICTURE: {
type: DataTypes.STRING(100), type: DataTypes.STRING,
allowNull: true, 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) timestamps: false,
tableName: "users", // Ensure the table name matches the actual table name tableName: "users",
} }
); );
return Users; return Users;

1
package-lock.json generated
View File

@ -15,6 +15,7 @@
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"express": "^4.19.2", "express": "^4.19.2",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"moment-timezone": "^0.5.45",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"mysql2": "^3.11.0", "mysql2": "^3.11.0",
"nodemailer": "^6.9.14", "nodemailer": "^6.9.14",

View File

@ -18,6 +18,7 @@
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"express": "^4.19.2", "express": "^4.19.2",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"moment-timezone": "^0.5.45",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"mysql2": "^3.11.0", "mysql2": "^3.11.0",
"nodemailer": "^6.9.14", "nodemailer": "^6.9.14",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

View File

@ -1,8 +1,9 @@
const response = (statusCode, data, message, res) => { const response = (statusCode, data, message, res) => {
res.status(statusCode).json({ res.status(statusCode).json({
payload: data, payload: data,
statusCode: statusCode,
message: message, message: message,
}); });
}; };
export default response; export default response;

View File

@ -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;

21
routes/auth/auth.js Normal file
View File

@ -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;

View File

@ -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

25
routes/contents/level.js Normal file
View File

@ -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

View File

@ -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

18
routes/contents/topic.js Normal file
View File

@ -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

View File

@ -1,15 +1,25 @@
import express from "express"; import express from "express";
import user_routes from "./user.js"; import user_routes from "./user/user.js";
import auth_routes from "./auth.js"; import auth_routes from "./auth/auth.js";
import subject_routes from "./subject.js"; import section_routes from "./contents/section.js";
import topic_routes from "./topic.js"; import topic_routes from "./contents/topic.js";
import level_routes from "./level.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(); const route = express();
route.use(user_routes); route.use(user_routes);
route.use(auth_routes); route.use(auth_routes);
route.use(subject_routes); route.use(section_routes);
route.use(topic_routes); route.use(topic_routes);
route.use(level_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; export default route;

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

29
routes/user/user.js Normal file
View File

@ -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