initial commit
This commit is contained in:
commit
ea0d3f85d7
12
.envexample
Normal file
12
.envexample
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
APP_PORT = 3001
|
||||||
|
|
||||||
|
DB_HOST = localhost
|
||||||
|
DB_USER = root
|
||||||
|
DB_PASSWORD =
|
||||||
|
DB_NAME = project_siswa
|
||||||
|
|
||||||
|
ACCESS_TOKEN_SECRET =
|
||||||
|
RESET_PASSWORD_SECRET =
|
||||||
|
|
||||||
|
EMAIL_USER =
|
||||||
|
EMAIL_PASS =
|
||||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
node_modules
|
||||||
|
.env
|
||||||
202
controllers/auth.js
Normal file
202
controllers/auth.js
Normal file
|
|
@ -0,0 +1,202 @@
|
||||||
|
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" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
137
controllers/subject.js
Normal file
137
controllers/subject.js
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
import response from "../response.js";
|
||||||
|
import models from "../models/index.js";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
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, icon, thumbnail } = req.body;
|
||||||
|
|
||||||
|
// Validate name
|
||||||
|
if (!name) {
|
||||||
|
return response(400, null, "Name is required", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate description
|
||||||
|
if (!description) {
|
||||||
|
return response(400, null, "Description is required", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newSubject = await models.Subject.create({
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
icon,
|
||||||
|
thumbnail,
|
||||||
|
});
|
||||||
|
|
||||||
|
response(201, newSubject, "Subject created successfully", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
res.status(500).json({ message: "Internal Server Error" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateSubjectById = async (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { name, description } = req.body;
|
||||||
|
const icon = req.body.icon;
|
||||||
|
const thumbnail = req.body.thumbnail;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const subject = await models.Subject.findByPk(id);
|
||||||
|
|
||||||
|
if (!subject) {
|
||||||
|
return response(404, null, "Subject not found", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update subject fields
|
||||||
|
if (name) subject.name = name;
|
||||||
|
if (description) subject.description = description;
|
||||||
|
if (icon) {
|
||||||
|
// Remove old icon if it exists
|
||||||
|
if (
|
||||||
|
subject.icon &&
|
||||||
|
fs.existsSync(path.join("public/uploads", subject.icon))
|
||||||
|
) {
|
||||||
|
fs.unlinkSync(path.join("public/uploads", subject.icon));
|
||||||
|
}
|
||||||
|
subject.icon = icon;
|
||||||
|
}
|
||||||
|
if (thumbnail) {
|
||||||
|
// Remove old thumbnail if it exists
|
||||||
|
if (
|
||||||
|
subject.thumbnail &&
|
||||||
|
fs.existsSync(path.join("public/uploads", subject.thumbnail))
|
||||||
|
) {
|
||||||
|
fs.unlinkSync(path.join("public/uploads", subject.thumbnail));
|
||||||
|
}
|
||||||
|
subject.thumbnail = thumbnail;
|
||||||
|
}
|
||||||
|
|
||||||
|
await subject.save();
|
||||||
|
|
||||||
|
response(200, subject, "Subject updated successfully", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
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 files if they exist
|
||||||
|
if (
|
||||||
|
subject.icon &&
|
||||||
|
fs.existsSync(path.join("public/uploads", subject.icon))
|
||||||
|
) {
|
||||||
|
fs.unlinkSync(path.join("public/uploads", subject.icon));
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
subject.thumbnail &&
|
||||||
|
fs.existsSync(path.join("public/uploads", subject.thumbnail))
|
||||||
|
) {
|
||||||
|
fs.unlinkSync(path.join("public/uploads", subject.thumbnail));
|
||||||
|
}
|
||||||
|
|
||||||
|
await subject.destroy();
|
||||||
|
|
||||||
|
response(200, null, "Subject deleted successfully", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
response(500, null, "Internal Server Error", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
117
controllers/topic.js
Normal file
117
controllers/topic.js
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
import response from "../response.js";
|
||||||
|
import models from "../models/index.js";
|
||||||
|
|
||||||
|
export const getTopics = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const topics = await models.Topic.findAll();
|
||||||
|
response(200, topics, "Success", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
response(500, null, "Error retrieving topics data!", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTopicById = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const topic = await models.Topic.findByPk(id);
|
||||||
|
|
||||||
|
if (!topic) {
|
||||||
|
return response(404, null, "Topic not found", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
response(200, topic, "Success", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
res.status(500).json({ message: "Internal Server Error" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createTopic = async (req, res) => {
|
||||||
|
const { subject_id, title } = req.body;
|
||||||
|
|
||||||
|
// Validate subject_id
|
||||||
|
if (!subject_id) {
|
||||||
|
return response(400, null, "Subject ID is required", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate title
|
||||||
|
if (!title) {
|
||||||
|
return response(400, null, "Title is required", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Verify that the subject_id exists in the m_subjects table
|
||||||
|
const subject = await models.Subject.findByPk(subject_id);
|
||||||
|
if (!subject) {
|
||||||
|
return response(404, null, "Subject not found", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newTopic = await models.Topic.create({
|
||||||
|
subject_id,
|
||||||
|
title,
|
||||||
|
});
|
||||||
|
|
||||||
|
response(201, newTopic, "Topic created successfully", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
response(500, null, "Internal Server Error", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateTopicById = async (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { subject_id, title } = req.body;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Find the topic by its ID
|
||||||
|
const topic = await models.Topic.findByPk(id);
|
||||||
|
|
||||||
|
if (!topic) {
|
||||||
|
return response(404, null, "Topic not found", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate and update subject_id if provided
|
||||||
|
if (subject_id) {
|
||||||
|
const subject = await models.Subject.findByPk(subject_id);
|
||||||
|
if (!subject) {
|
||||||
|
return response(404, null, "Subject not found", res);
|
||||||
|
}
|
||||||
|
topic.subject_id = subject_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate and update title if provided
|
||||||
|
if (title) {
|
||||||
|
topic.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the updated topic
|
||||||
|
await topic.save();
|
||||||
|
|
||||||
|
response(200, topic, "Topic updated successfully", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
response(500, null, "Internal Server Error", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteTopicById = async (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Find the topic by its ID
|
||||||
|
const topic = await models.Topic.findByPk(id);
|
||||||
|
|
||||||
|
if (!topic) {
|
||||||
|
return response(404, null, "Topic not found", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the topic
|
||||||
|
await topic.destroy();
|
||||||
|
|
||||||
|
response(200, null, "Topic deleted successfully", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
response(500, null, "Internal Server Error", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
94
controllers/user.js
Normal file
94
controllers/user.js
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
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();
|
||||||
|
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);
|
||||||
|
|
||||||
|
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" });
|
||||||
|
}
|
||||||
|
};
|
||||||
37
database/db.js
Normal file
37
database/db.js
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { Sequelize } from "sequelize";
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const host = process.env.DB_HOST;
|
||||||
|
const name = process.env.DB_NAME;
|
||||||
|
const username = process.env.DB_USER;
|
||||||
|
const password = process.env.DB_PASSWORD;
|
||||||
|
|
||||||
|
const db = new Sequelize(name, username, password, {
|
||||||
|
host: host,
|
||||||
|
dialect: "mysql",
|
||||||
|
logging: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const testConnection = async () => {
|
||||||
|
try {
|
||||||
|
await db.authenticate();
|
||||||
|
console.log("Database connected");
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error connecting to database", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const query = async (query, value) => {
|
||||||
|
try {
|
||||||
|
const [rows] = await db.query(query, value);
|
||||||
|
return rows;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { testConnection, query };
|
||||||
|
|
||||||
|
export default db;
|
||||||
22
index.js
Normal file
22
index.js
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import express from 'express';
|
||||||
|
import cors from 'cors';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
import { testConnection } from "./database/db.js";
|
||||||
|
import router from "./routes/index.js";
|
||||||
|
import cookieParser from 'cookie-parser';
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
app.use(cors());
|
||||||
|
app.use(cookieParser());
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(router);
|
||||||
|
|
||||||
|
// Serve static files from the uploads directory
|
||||||
|
app.use(express.static('public'));
|
||||||
|
|
||||||
|
app.listen(process.env.APP_PORT, () => {
|
||||||
|
testConnection();
|
||||||
|
console.log(`Server running on port http://localhost:${process.env.APP_PORT}`);
|
||||||
|
})
|
||||||
60
middlewares/authUser.js
Normal file
60
middlewares/authUser.js
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
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);
|
||||||
|
|
||||||
|
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();
|
||||||
|
};
|
||||||
74
middlewares/upload.js
Normal file
74
middlewares/upload.js
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
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 }, // Limit file size to 5MB
|
||||||
|
}).fields([
|
||||||
|
{ name: "icon", maxCount: 1 },
|
||||||
|
{ name: "thumbnail", maxCount: 1 },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const saveFileToDisk = (file) => {
|
||||||
|
const md5sum = crypto
|
||||||
|
.createHash("md5")
|
||||||
|
.update(Date.now().toString())
|
||||||
|
.digest("hex");
|
||||||
|
const ext = path.extname(file.originalname);
|
||||||
|
const filename = `${md5sum}${ext}`;
|
||||||
|
const filepath = path.join("public/uploads", filename);
|
||||||
|
fs.writeFileSync(filepath, file.buffer);
|
||||||
|
return filename;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpload = (req, res, next) => {
|
||||||
|
upload(req, res, (err) => {
|
||||||
|
if (err) {
|
||||||
|
return response(400, null, err.message, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = req.files;
|
||||||
|
const icon = files?.icon ? files.icon[0] : null;
|
||||||
|
const thumbnail = files?.thumbnail ? files.thumbnail[0] : null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Validate icon and thumbnail before saving
|
||||||
|
if (icon && thumbnail) {
|
||||||
|
const iconFilename = saveFileToDisk(icon);
|
||||||
|
const thumbnailFilename = saveFileToDisk(thumbnail);
|
||||||
|
|
||||||
|
// Update the filenames in the request object for further processing
|
||||||
|
req.body.icon = iconFilename;
|
||||||
|
req.body.thumbnail = thumbnailFilename;
|
||||||
|
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
return response(400, null, "Both icon and thumbnail are required", res);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return response(500, null, "Internal Server Error", res);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default handleUpload;
|
||||||
12
models/index.js
Normal file
12
models/index.js
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { Sequelize } from "sequelize";
|
||||||
|
import UserModel from "./userModel.js";
|
||||||
|
import SubjectModel from "./subjectModel.js";
|
||||||
|
import TopicModel from "./topicModel.js";
|
||||||
|
|
||||||
|
const models = {
|
||||||
|
User: UserModel(Sequelize.DataTypes),
|
||||||
|
Subject: SubjectModel(Sequelize.DataTypes),
|
||||||
|
Topic: TopicModel(Sequelize.DataTypes),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default models;
|
||||||
51
models/subjectModel.js
Normal file
51
models/subjectModel.js
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
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;
|
||||||
47
models/topicModel.js
Normal file
47
models/topicModel.js
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
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;
|
||||||
66
models/userModel.js
Normal file
66
models/userModel.js
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
import db from "../database/db.js";
|
||||||
|
|
||||||
|
const UserModel = (DataTypes) => {
|
||||||
|
const Users = db.define(
|
||||||
|
"users",
|
||||||
|
{
|
||||||
|
id: {
|
||||||
|
type: DataTypes.BIGINT,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true,
|
||||||
|
validate: {
|
||||||
|
notEmpty: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
validate: {
|
||||||
|
notEmpty: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
unique: true,
|
||||||
|
validate: {
|
||||||
|
notEmpty: true,
|
||||||
|
isEmail: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
email_verified_at: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
roles: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
remember_token: {
|
||||||
|
type: DataTypes.STRING(100),
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
created_at: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: DataTypes.NOW,
|
||||||
|
},
|
||||||
|
updated_at: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: DataTypes.NOW,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: false, // Disable Sequelize's automatic timestamp fields (createdAt, updatedAt)
|
||||||
|
tableName: "users", // Ensure the table name matches the actual table name
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return Users;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserModel;
|
||||||
2122
package-lock.json
generated
Normal file
2122
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
package.json
Normal file
27
package.json
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"name": "backend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"api-start": "nodemon index.js"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"bcrypt": "^5.1.1",
|
||||||
|
"cookie-parser": "^1.4.6",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
|
"express": "^4.19.2",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"multer": "^1.4.5-lts.1",
|
||||||
|
"mysql2": "^3.11.0",
|
||||||
|
"nodemailer": "^6.9.14",
|
||||||
|
"nodemon": "^3.1.4",
|
||||||
|
"sequelize": "^6.37.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
public/uploads/0025932b3fb82d7e19648e1ff9bbf7bc.png
Normal file
BIN
public/uploads/0025932b3fb82d7e19648e1ff9bbf7bc.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
public/uploads/d65fdab37a0cf11d22c7a8b08f727812.jpg
Normal file
BIN
public/uploads/d65fdab37a0cf11d22c7a8b08f727812.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
13
response.js
Normal file
13
response.js
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
const response = (statusCode, data, message, res) => {
|
||||||
|
res.status(statusCode).json({
|
||||||
|
payload: data,
|
||||||
|
message: message,
|
||||||
|
pagination: {
|
||||||
|
prev: "",
|
||||||
|
next: "",
|
||||||
|
max: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default response;
|
||||||
16
routes/auth.js
Normal file
16
routes/auth.js
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
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;
|
||||||
13
routes/index.js
Normal file
13
routes/index.js
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import express from "express";
|
||||||
|
import user_routes from "./user.js";
|
||||||
|
import auth_routes from "./auth.js";
|
||||||
|
import subject_routes from "./subject.js";
|
||||||
|
import topic_routes from "./topic.js";
|
||||||
|
|
||||||
|
const route = express();
|
||||||
|
route.use(user_routes);
|
||||||
|
route.use(auth_routes);
|
||||||
|
route.use(subject_routes);
|
||||||
|
route.use(topic_routes);
|
||||||
|
|
||||||
|
export default route;
|
||||||
19
routes/subject.js
Normal file
19
routes/subject.js
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import express from "express";
|
||||||
|
import handleUpload from '../middlewares/upload.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
|
||||||
18
routes/topic.js
Normal file
18
routes/topic.js
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
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
|
||||||
16
routes/user.js
Normal file
16
routes/user.js
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
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, teacherOnly, getUsers);
|
||||||
|
|
||||||
|
router.get("/user/:id", getUserById);
|
||||||
|
|
||||||
|
router.post("/user/update/:id", updateUserById);
|
||||||
|
|
||||||
|
router.delete("/user/delete/:id", deleteUserById);
|
||||||
|
|
||||||
|
export default router
|
||||||
Loading…
Reference in New Issue
Block a user