1278 lines
36 KiB
JavaScript
1278 lines
36 KiB
JavaScript
import response from "../../response.js";
|
|
import bcrypt from "bcryptjs";
|
|
import csvParser from "csv-parser";
|
|
import streamifier from "streamifier";
|
|
import ExcelJS from "exceljs";
|
|
import jwt from "jsonwebtoken";
|
|
import nodemailer from "nodemailer";
|
|
import moment from "moment-timezone";
|
|
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_USERS, EMAIL, PASSWORD, CONFIRM_PASSWORD } = req.body;
|
|
|
|
if (!NAME_USERS) {
|
|
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 (!CONFIRM_PASSWORD) {
|
|
return response(400, null, "Confirm Password is required!", res);
|
|
}
|
|
|
|
if (PASSWORD !== CONFIRM_PASSWORD) {
|
|
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_USERS,
|
|
EMAIL: EMAIL,
|
|
PASSWORD: hashedPassword,
|
|
ROLE: "admin",
|
|
IS_VALIDATED: 1,
|
|
});
|
|
|
|
const adminResponse = {
|
|
ID: newUser.ID,
|
|
NAME_USERS: newUser.NAME_USERS,
|
|
EMAIL: newUser.EMAIL,
|
|
ROLE: newUser.ROLE,
|
|
IS_VALIDATED: newUser.IS_VALIDATED,
|
|
};
|
|
|
|
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_USERS, EMAIL, NIP, PASSWORD, CONFIRM_PASSWORD } = req.body;
|
|
|
|
if (!NAME_USERS) {
|
|
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 (!CONFIRM_PASSWORD) {
|
|
return response(400, null, "Confirm Password is required!", res);
|
|
}
|
|
|
|
if (PASSWORD !== CONFIRM_PASSWORD) {
|
|
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_USERS,
|
|
EMAIL: EMAIL,
|
|
PASSWORD: hashedPassword,
|
|
ROLE: "teacher",
|
|
IS_VALIDATED: 0,
|
|
},
|
|
{ transaction }
|
|
);
|
|
|
|
await models.Teacher.create(
|
|
{
|
|
ID: newUser.ID,
|
|
NIP: NIP,
|
|
},
|
|
{ transaction }
|
|
);
|
|
|
|
await transaction.commit();
|
|
|
|
const now = moment().tz("Asia/Jakarta");
|
|
const midnight = now.clone().endOf("day");
|
|
const secondsUntilMidnight = midnight.diff(now, "seconds");
|
|
|
|
const token = jwt.sign(
|
|
{ userId: newUser.ID },
|
|
process.env.VERIFY_TOKEN_SECRET,
|
|
{ expiresIn: secondsUntilMidnight }
|
|
);
|
|
|
|
const validationLink = `${process.env.CLIENT_URL}/validate-email?token=${token}`;
|
|
await transporter.sendMail({
|
|
from: process.env.EMAIL_USER,
|
|
to: EMAIL,
|
|
subject: "Email Verification",
|
|
html: `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Email Verification</title>
|
|
<style>
|
|
body {
|
|
margin: 0;
|
|
padding: 0;
|
|
font-family: 'Segoe UI', Arial, sans-serif;
|
|
line-height: 1.6;
|
|
background-color: #f5f5f5;
|
|
}
|
|
.container {
|
|
max-width: 600px;
|
|
margin: 20px auto;
|
|
padding: 0;
|
|
background-color: #ffffff;
|
|
border-radius: 10px;
|
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
}
|
|
.header {
|
|
background-color: #0090FF;
|
|
padding: 25px 20px;
|
|
text-align: center;
|
|
border-radius: 10px 10px 0 0;
|
|
}
|
|
.header h1 {
|
|
color: #ffffff;
|
|
margin: 0;
|
|
font-size: 32px;
|
|
letter-spacing: 2px;
|
|
font-weight: bold;
|
|
}
|
|
.content {
|
|
padding: 40px 30px;
|
|
background-color: #ffffff;
|
|
color: #333333;
|
|
}
|
|
.content h2 {
|
|
color: #0090FF;
|
|
margin-top: 0;
|
|
font-size: 24px;
|
|
margin-bottom: 20px;
|
|
}
|
|
.content p {
|
|
margin-bottom: 15px;
|
|
font-size: 16px;
|
|
line-height: 1.7;
|
|
}
|
|
.button {
|
|
display: inline-block;
|
|
padding: 12px 35px;
|
|
background-color: #0090FF;
|
|
color: #ffffff !important;
|
|
text-decoration: none;
|
|
border-radius: 5px;
|
|
margin: 25px 0;
|
|
font-size: 16px;
|
|
font-weight: bold;
|
|
text-align: center;
|
|
transition: background-color 0.3s ease;
|
|
box-shadow: 0 2px 5px rgba(0,144,255,0.3);
|
|
}
|
|
.button:hover {
|
|
background-color: #007acc;
|
|
}
|
|
.footer {
|
|
background-color: #0090FF;
|
|
color: #ffffff;
|
|
padding: 25px 20px;
|
|
text-align: center;
|
|
border-radius: 0 0 10px 10px;
|
|
font-size: 14px;
|
|
}
|
|
.signature {
|
|
margin-top: 30px;
|
|
padding-top: 20px;
|
|
border-top: 1px solid #eee;
|
|
font-weight: bold;
|
|
color: #0090FF;
|
|
}
|
|
.important-note {
|
|
background-color: #f8f9fa;
|
|
padding: 15px;
|
|
border-left: 4px solid #0090FF;
|
|
margin: 20px 0;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>SEALS</h1>
|
|
</div>
|
|
|
|
<div class="content">
|
|
<h2>Hello, ${NAME_USERS}! 👋</h2>
|
|
|
|
<p>Welcome to SEALS! We're excited to have you on board.</p>
|
|
|
|
<p>To get started, please verify your email address by clicking the button below:</p>
|
|
|
|
<div style="text-align: center;">
|
|
<a href="${validationLink}" class="button">Verify Email</a>
|
|
</div>
|
|
|
|
<div class="important-note">
|
|
<p style="margin: 0;"><strong>Important:</strong> This verification link will expire at 12:00 AM WIB. If you don't complete the verification by this time, you'll need to register again.</p>
|
|
</div>
|
|
|
|
<p>If you didn't create an account with SEALS, please ignore this email.</p>
|
|
|
|
<div class="signature">
|
|
<p>Thank you for choosing SEALS!</p>
|
|
<p style="margin: 5px 0 0 0;">The SEALS Team</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="footer">
|
|
<p style="margin: 0;">© 2024 SEALS. All rights reserved.</p>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
`,
|
|
});
|
|
|
|
response(200, null, "Teacher registered! Please verify your email.", 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_USERS, EMAIL, NISN, PASSWORD, CONFIRM_PASSWORD } = req.body;
|
|
|
|
if (!NAME_USERS) {
|
|
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 (!CONFIRM_PASSWORD) {
|
|
return response(400, null, "Confirm Password is required!", res);
|
|
}
|
|
|
|
if (PASSWORD !== CONFIRM_PASSWORD) {
|
|
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_USERS,
|
|
EMAIL: EMAIL,
|
|
PASSWORD: hashedPassword,
|
|
ROLE: "student",
|
|
IS_VALIDATED: 0,
|
|
},
|
|
{ transaction }
|
|
);
|
|
|
|
await models.Student.create(
|
|
{
|
|
ID: newUser.ID,
|
|
NISN: NISN,
|
|
},
|
|
{ transaction }
|
|
);
|
|
|
|
await transaction.commit();
|
|
|
|
const now = moment().tz("Asia/Jakarta");
|
|
const midnight = now.clone().endOf("day");
|
|
const secondsUntilMidnight = midnight.diff(now, "seconds");
|
|
|
|
const token = jwt.sign(
|
|
{ userId: newUser.ID },
|
|
process.env.VERIFY_TOKEN_SECRET,
|
|
{ expiresIn: secondsUntilMidnight }
|
|
);
|
|
|
|
const validationLink = `${process.env.CLIENT_URL}/validate-email?token=${token}`;
|
|
await transporter.sendMail({
|
|
from: process.env.EMAIL_USER,
|
|
to: EMAIL,
|
|
subject: "Email Verification",
|
|
html: `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Email Verification</title>
|
|
<style>
|
|
body {
|
|
margin: 0;
|
|
padding: 0;
|
|
font-family: 'Segoe UI', Arial, sans-serif;
|
|
line-height: 1.6;
|
|
background-color: #f5f5f5;
|
|
}
|
|
.container {
|
|
max-width: 600px;
|
|
margin: 20px auto;
|
|
padding: 0;
|
|
background-color: #ffffff;
|
|
border-radius: 10px;
|
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
}
|
|
.header {
|
|
background-color: #0090FF;
|
|
padding: 25px 20px;
|
|
text-align: center;
|
|
border-radius: 10px 10px 0 0;
|
|
}
|
|
.header h1 {
|
|
color: #ffffff;
|
|
margin: 0;
|
|
font-size: 32px;
|
|
letter-spacing: 2px;
|
|
font-weight: bold;
|
|
}
|
|
.content {
|
|
padding: 40px 30px;
|
|
background-color: #ffffff;
|
|
color: #333333;
|
|
}
|
|
.content h2 {
|
|
color: #0090FF;
|
|
margin-top: 0;
|
|
font-size: 24px;
|
|
margin-bottom: 20px;
|
|
}
|
|
.content p {
|
|
margin-bottom: 15px;
|
|
font-size: 16px;
|
|
line-height: 1.7;
|
|
}
|
|
.button {
|
|
display: inline-block;
|
|
padding: 12px 35px;
|
|
background-color: #0090FF;
|
|
color: #ffffff !important;
|
|
text-decoration: none;
|
|
border-radius: 5px;
|
|
margin: 25px 0;
|
|
font-size: 16px;
|
|
font-weight: bold;
|
|
text-align: center;
|
|
transition: background-color 0.3s ease;
|
|
box-shadow: 0 2px 5px rgba(0,144,255,0.3);
|
|
}
|
|
.button:hover {
|
|
background-color: #007acc;
|
|
}
|
|
.footer {
|
|
background-color: #0090FF;
|
|
color: #ffffff;
|
|
padding: 25px 20px;
|
|
text-align: center;
|
|
border-radius: 0 0 10px 10px;
|
|
font-size: 14px;
|
|
}
|
|
.signature {
|
|
margin-top: 30px;
|
|
padding-top: 20px;
|
|
border-top: 1px solid #eee;
|
|
font-weight: bold;
|
|
color: #0090FF;
|
|
}
|
|
.important-note {
|
|
background-color: #f8f9fa;
|
|
padding: 15px;
|
|
border-left: 4px solid #0090FF;
|
|
margin: 20px 0;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>SEALS</h1>
|
|
</div>
|
|
|
|
<div class="content">
|
|
<h2>Hello, ${NAME_USERS}! 👋</h2>
|
|
|
|
<p>Welcome to SEALS! We're excited to have you on board.</p>
|
|
|
|
<p>To get started, please verify your email address by clicking the button below:</p>
|
|
|
|
<div style="text-align: center;">
|
|
<a href="${validationLink}" class="button">Verify Email</a>
|
|
</div>
|
|
|
|
<div class="important-note">
|
|
<p style="margin: 0;"><strong>Important:</strong> This verification link will expire at 12:00 AM WIB. If you don't complete the verification by this time, you'll need to register again.</p>
|
|
</div>
|
|
|
|
<p>If you didn't create an account with SEALS, please ignore this email.</p>
|
|
|
|
<div class="signature">
|
|
<p>Thank you for choosing SEALS!</p>
|
|
<p style="margin: 5px 0 0 0;">The SEALS Team</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="footer">
|
|
<p style="margin: 0;">© 2024 SEALS. All rights reserved.</p>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
`,
|
|
});
|
|
|
|
response(200, null, "Student registered! Please verify your email.", 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 validateEmail = async (req, res) => {
|
|
const { TOKEN } = req.body;
|
|
|
|
try {
|
|
const decoded = jwt.verify(TOKEN, process.env.VERIFY_TOKEN_SECRET);
|
|
const userId = decoded.userId;
|
|
|
|
await models.User.update({ IS_VALIDATED: 1 }, { where: { ID: userId } });
|
|
|
|
response(200, null, "Email successfully validated!", res);
|
|
} catch (error) {
|
|
response(400, null, "Invalid or expired token", res);
|
|
}
|
|
};
|
|
|
|
export const registerTeacherForAdmin = async (req, res) => {
|
|
const { NAME_USERS, EMAIL, NIP, PASSWORD, CONFIRM_PASSWORD } = req.body;
|
|
|
|
if (!NAME_USERS) {
|
|
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 (!CONFIRM_PASSWORD) {
|
|
return response(400, null, "Confirm Password is required!", res);
|
|
}
|
|
|
|
if (PASSWORD !== CONFIRM_PASSWORD) {
|
|
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_USERS,
|
|
EMAIL: EMAIL,
|
|
PASSWORD: hashedPassword,
|
|
ROLE: "teacher",
|
|
IS_VALIDATED: 1,
|
|
},
|
|
{ transaction }
|
|
);
|
|
|
|
await models.Teacher.create(
|
|
{
|
|
ID: newUser.ID,
|
|
NIP: NIP,
|
|
},
|
|
{ transaction }
|
|
);
|
|
|
|
await transaction.commit();
|
|
|
|
const teacherResponse = {
|
|
ID: newUser.ID,
|
|
NAME_USERS: newUser.NAME_USERS,
|
|
EMAIL: newUser.EMAIL,
|
|
NIP: NIP,
|
|
ROLE: newUser.ROLE,
|
|
IS_VALIDATED: newUser.IS_VALIDATED,
|
|
};
|
|
|
|
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 registerStudentForAdminAndTeacher = async (req, res) => {
|
|
const { NAME_USERS, NISN } = req.body;
|
|
|
|
if (!NAME_USERS) {
|
|
return response(400, null, "Name is required!", res);
|
|
}
|
|
|
|
if (!NISN) {
|
|
return response(400, null, "NISN is required for students!", res);
|
|
}
|
|
|
|
const EMAIL = `${NISN}@student.seals.com`;
|
|
const PASSWORD = "12345678";
|
|
|
|
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_USERS,
|
|
EMAIL: EMAIL,
|
|
PASSWORD: hashedPassword,
|
|
ROLE: "student",
|
|
IS_VALIDATED: 1,
|
|
},
|
|
{ transaction }
|
|
);
|
|
|
|
await models.Student.create(
|
|
{
|
|
ID: newUser.ID,
|
|
NISN: NISN,
|
|
},
|
|
{ transaction }
|
|
);
|
|
|
|
await transaction.commit();
|
|
|
|
const studentResponse = {
|
|
ID: newUser.ID,
|
|
NAME_USERS: newUser.NAME_USERS,
|
|
EMAIL: newUser.EMAIL,
|
|
NISN: NISN,
|
|
ROLE: newUser.ROLE,
|
|
IS_VALIDATED: newUser.IS_VALIDATED,
|
|
PASSWORD: "12345678",
|
|
};
|
|
|
|
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 registerStudentCSV = async (req, res) => {
|
|
const { buffer, extension } = req.uploadedFile;
|
|
|
|
if (!buffer) {
|
|
return response(400, null, "File is required!", res);
|
|
}
|
|
|
|
const students = [];
|
|
|
|
try {
|
|
if (extension === ".csv") {
|
|
await new Promise((resolve, reject) => {
|
|
const csvStream = streamifier.createReadStream(buffer);
|
|
let headers = null;
|
|
let headerFound = false;
|
|
|
|
csvStream
|
|
.pipe(csvParser())
|
|
.on("data", (row) => {
|
|
if (!headerFound) {
|
|
const keys = Object.keys(row).filter((key) => row[key]?.trim());
|
|
if (keys.length > 0) {
|
|
headers = normalizeHeaders(keys);
|
|
headerFound = true;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (headers) {
|
|
const NISN = row[headers.NISN]?.trim();
|
|
const NAME_USERS = row[headers.NAME_USERS]?.trim();
|
|
|
|
if (NISN && NAME_USERS) {
|
|
students.push({ NISN, NAME_USERS });
|
|
}
|
|
}
|
|
})
|
|
.on("end", resolve)
|
|
.on("error", reject);
|
|
});
|
|
} else if (extension === ".xlsx") {
|
|
const workbook = new ExcelJS.Workbook();
|
|
await workbook.xlsx.load(buffer);
|
|
const worksheet = workbook.worksheets[0];
|
|
|
|
let headers = null;
|
|
|
|
worksheet.eachRow((row, rowNumber) => {
|
|
if (rowNumber === 1 || !headers) {
|
|
const headersRow = worksheet.getRow(rowNumber);
|
|
headers = normalizeHeadersXLSX(
|
|
headersRow.values.filter((value) => value)
|
|
);
|
|
if (Object.keys(headers).length === 0) {
|
|
return;
|
|
}
|
|
} else {
|
|
const isEmptyRow = row.values.every(
|
|
(cell) => !cell || !String(cell).trim()
|
|
);
|
|
if (isEmptyRow) {
|
|
return;
|
|
}
|
|
|
|
const NISN = row.getCell(headers.NISN)?.text?.trim();
|
|
const NAME_USERS = row.getCell(headers.NAME_USERS)?.text?.trim();
|
|
|
|
if (NISN && NAME_USERS) {
|
|
students.push({ NISN, NAME_USERS });
|
|
}
|
|
}
|
|
});
|
|
} else {
|
|
return response(400, null, "Unsupported file format!", res);
|
|
}
|
|
|
|
if (students.length === 0) {
|
|
return response(400, null, "File is empty or invalid format!", res);
|
|
}
|
|
} catch (error) {
|
|
console.error(error);
|
|
return response(500, null, "Error processing file!", res);
|
|
}
|
|
|
|
const transaction = await models.db.transaction();
|
|
const processed = [];
|
|
const duplicates = [];
|
|
|
|
try {
|
|
for (const student of students) {
|
|
const { NISN, NAME_USERS } = student;
|
|
|
|
const EMAIL = `${NISN}@student.seals.com`;
|
|
const PASSWORD = "12345678";
|
|
|
|
const salt = await bcrypt.genSalt(10);
|
|
const hashedPassword = await bcrypt.hash(PASSWORD, salt);
|
|
|
|
const existingUser = await models.User.findOne({ where: { EMAIL } });
|
|
|
|
if (existingUser) {
|
|
duplicates.push(student);
|
|
continue;
|
|
}
|
|
|
|
const existingStudent = await models.Student.findOne({
|
|
where: { NISN },
|
|
});
|
|
|
|
if (existingStudent) {
|
|
duplicates.push(student);
|
|
continue;
|
|
}
|
|
|
|
const newUser = await models.User.create(
|
|
{
|
|
NAME_USERS,
|
|
EMAIL,
|
|
PASSWORD: hashedPassword,
|
|
ROLE: "student",
|
|
IS_VALIDATED: 1,
|
|
},
|
|
{ transaction }
|
|
);
|
|
|
|
await models.Student.create(
|
|
{
|
|
ID: newUser.ID,
|
|
NISN,
|
|
},
|
|
{ transaction }
|
|
);
|
|
|
|
processed.push(student);
|
|
}
|
|
|
|
await transaction.commit();
|
|
|
|
response(
|
|
200,
|
|
{
|
|
success: true,
|
|
message: "Students registered successfully!",
|
|
processed,
|
|
duplicates,
|
|
},
|
|
"Operation complete with some duplicates skipped.",
|
|
res
|
|
);
|
|
} catch (error) {
|
|
console.error(error);
|
|
await transaction.rollback();
|
|
response(500, null, "Internal Server Error", res);
|
|
}
|
|
};
|
|
|
|
export const loginUser = async (req, res) => {
|
|
const { IDENTIFIER, PASSWORD } = req.body;
|
|
|
|
if (!IDENTIFIER) {
|
|
return response(400, null, "Identifier (Email or NISN) is required!", res);
|
|
}
|
|
|
|
if (!PASSWORD) {
|
|
return response(400, null, "Password is required!", res);
|
|
}
|
|
|
|
try {
|
|
let user;
|
|
|
|
if (IDENTIFIER.includes("@")) {
|
|
user = await models.User.findOne({ where: { EMAIL: IDENTIFIER } });
|
|
} else {
|
|
const student = await models.Student.findOne({
|
|
where: { NISN: IDENTIFIER },
|
|
include: [
|
|
{
|
|
model: models.User,
|
|
as: "studentUser",
|
|
},
|
|
],
|
|
});
|
|
user = student?.studentUser;
|
|
}
|
|
|
|
if (!user) {
|
|
return response(404, null, "User data not found!", res);
|
|
}
|
|
|
|
if (user.IS_VALIDATED !== 1) {
|
|
return response(
|
|
403,
|
|
null,
|
|
"User is not validated! Please verify via email first.",
|
|
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, ROLE: user.ROLE },
|
|
process.env.ACCESS_TOKEN_SECRET,
|
|
{ expiresIn: "3h" }
|
|
);
|
|
|
|
const refreshToken = jwt.sign(
|
|
{ ID: user.ID, ROLE: user.ROLE },
|
|
process.env.REFRESH_TOKEN_SECRET,
|
|
{ expiresIn: "7d" }
|
|
);
|
|
|
|
await models.User.update(
|
|
{ REFRESH_TOKEN: refreshToken },
|
|
{ where: { ID: user.ID } }
|
|
);
|
|
|
|
res.cookie("refreshToken", refreshToken, {
|
|
httpOnly: true,
|
|
secure: process.env.NODE_ENV === "production",
|
|
maxAge: 7 * 24 * 60 * 60 * 1000,
|
|
});
|
|
|
|
const userResponse = {
|
|
ID: user.ID,
|
|
NAME_USERS: user.NAME_USERS,
|
|
EMAIL: user.EMAIL,
|
|
ROLE: user.ROLE,
|
|
TOKEN: `Bearer ${accessToken}`,
|
|
REFRESH_TOKEN: refreshToken,
|
|
};
|
|
|
|
response(200, userResponse, "Login successful", res);
|
|
} catch (error) {
|
|
console.log(error);
|
|
response(500, null, "Internal Server Error", res);
|
|
}
|
|
};
|
|
|
|
export const refreshToken = async (req, res) => {
|
|
const refreshToken = req.cookies?.refreshToken || req.body?.REFRESH_TOKEN;
|
|
|
|
if (!refreshToken) {
|
|
return response(400, null, "Refresh token is required!", res);
|
|
}
|
|
|
|
try {
|
|
const user = await models.User.findOne({
|
|
where: { REFRESH_TOKEN: refreshToken },
|
|
});
|
|
|
|
if (!user) {
|
|
return response(403, null, "Invalid refresh token!", res);
|
|
}
|
|
|
|
let decoded;
|
|
try {
|
|
decoded = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET);
|
|
} catch (err) {
|
|
if (err.name === "TokenExpiredError") {
|
|
return response(
|
|
401,
|
|
null,
|
|
"Refresh token expired. Please login again.",
|
|
res
|
|
);
|
|
}
|
|
return response(403, null, "Invalid refresh token!", res);
|
|
}
|
|
|
|
if (decoded.ID !== user.ID) {
|
|
return response(403, null, "Invalid refresh token data!", res);
|
|
}
|
|
|
|
const newAccessToken = jwt.sign(
|
|
{ ID: user.ID, ROLE: user.ROLE },
|
|
process.env.ACCESS_TOKEN_SECRET,
|
|
{ expiresIn: "3h" }
|
|
);
|
|
|
|
const newRefreshToken = jwt.sign(
|
|
{ ID: user.ID, ROLE: user.ROLE },
|
|
process.env.REFRESH_TOKEN_SECRET,
|
|
{ expiresIn: "7d" }
|
|
);
|
|
|
|
await models.User.update(
|
|
{ REFRESH_TOKEN: newRefreshToken },
|
|
{ where: { ID: user.ID } }
|
|
);
|
|
|
|
res.cookie("refreshToken", newRefreshToken, {
|
|
httpOnly: true,
|
|
secure: process.env.NODE_ENV === "production",
|
|
|
|
maxAge: 7 * 24 * 60 * 60 * 1000,
|
|
});
|
|
|
|
response(
|
|
200,
|
|
{ TOKEN: `Bearer ${newAccessToken}`, REFRESH_TOKEN: newRefreshToken },
|
|
"Token refreshed successfully",
|
|
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 = `${process.env.CLIENT_URL}/resetPassword/${resetToken}`;
|
|
|
|
const mailOptions = {
|
|
from: process.env.EMAIL_USER,
|
|
to: user.EMAIL,
|
|
subject: "Password Reset",
|
|
html: `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Reset Password</title>
|
|
<style>
|
|
body {
|
|
margin: 0;
|
|
padding: 0;
|
|
font-family: 'Segoe UI', Arial, sans-serif;
|
|
line-height: 1.6;
|
|
background-color: #f5f5f5;
|
|
}
|
|
.container {
|
|
max-width: 600px;
|
|
margin: 20px auto;
|
|
padding: 0;
|
|
background-color: #ffffff;
|
|
border-radius: 10px;
|
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
}
|
|
.header {
|
|
background-color: #0090FF;
|
|
padding: 25px 20px;
|
|
text-align: center;
|
|
border-radius: 10px 10px 0 0;
|
|
}
|
|
.header h1 {
|
|
color: #ffffff;
|
|
margin: 0;
|
|
font-size: 32px;
|
|
letter-spacing: 2px;
|
|
font-weight: bold;
|
|
}
|
|
.content {
|
|
padding: 40px 30px;
|
|
background-color: #ffffff;
|
|
color: #333333;
|
|
}
|
|
.content h2 {
|
|
color: #0090FF;
|
|
margin-top: 0;
|
|
font-size: 24px;
|
|
margin-bottom: 20px;
|
|
}
|
|
.content p {
|
|
margin-bottom: 15px;
|
|
font-size: 16px;
|
|
line-height: 1.7;
|
|
}
|
|
.button {
|
|
display: inline-block;
|
|
padding: 12px 35px;
|
|
background-color: #0090FF;
|
|
color: #ffffff !important;
|
|
text-decoration: none;
|
|
border-radius: 5px;
|
|
margin: 25px 0;
|
|
font-size: 16px;
|
|
font-weight: bold;
|
|
text-align: center;
|
|
transition: background-color 0.3s ease;
|
|
box-shadow: 0 2px 5px rgba(0,144,255,0.3);
|
|
}
|
|
.button:hover {
|
|
background-color: #007acc;
|
|
}
|
|
.footer {
|
|
background-color: #0090FF;
|
|
color: #ffffff;
|
|
padding: 25px 20px;
|
|
text-align: center;
|
|
border-radius: 0 0 10px 10px;
|
|
font-size: 14px;
|
|
}
|
|
.signature {
|
|
margin-top: 30px;
|
|
padding-top: 20px;
|
|
border-top: 1px solid #eee;
|
|
font-weight: bold;
|
|
color: #0090FF;
|
|
}
|
|
.important-note {
|
|
background-color: #f8f9fa;
|
|
padding: 15px;
|
|
border-left: 4px solid #0090FF;
|
|
margin: 20px 0;
|
|
}
|
|
.security-warning {
|
|
background-color: #fff3cd;
|
|
padding: 15px;
|
|
border-left: 4px solid #ffc107;
|
|
margin: 20px 0;
|
|
font-size: 14px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>SEALS</h1>
|
|
</div>
|
|
|
|
<div class="content">
|
|
<h2>Password Reset Request</h2>
|
|
|
|
<p>Hello!</p>
|
|
|
|
<p>We received a request to reset the password for your SEALS account. To proceed with the password reset, please click the button below:</p>
|
|
|
|
<div style="text-align: center;">
|
|
<a href="${resetLink}" class="button">Reset Password</a>
|
|
</div>
|
|
|
|
<div class="important-note">
|
|
<p style="margin: 0;"><strong>Important:</strong> This reset password link will expire in 1 hour. Please reset your password as soon as possible to maintain access to your account.</p>
|
|
</div>
|
|
|
|
<div class="security-warning">
|
|
<p style="margin: 0;">⚠️ If you didn't request a password reset, please ignore this email or contact our support team if you have concerns about your account's security.</p>
|
|
</div>
|
|
|
|
<div class="signature">
|
|
<p>Thank you for using SEALS!</p>
|
|
<p style="margin: 5px 0 0 0;">The SEALS Team</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="footer">
|
|
<p style="margin: 0;">© 2024 SEALS. All rights reserved.</p>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
`,
|
|
};
|
|
|
|
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, NEW_PASSWORD, CONFIRM_NEW_PASSWORD } = req.body;
|
|
|
|
if (!TOKEN) {
|
|
return response(400, null, "Token is required!", res);
|
|
}
|
|
|
|
if (!NEW_PASSWORD) {
|
|
return response(400, null, "New password is required!", res);
|
|
}
|
|
|
|
if (!CONFIRM_NEW_PASSWORD) {
|
|
return response(400, null, "Confirm new password is required!", res);
|
|
}
|
|
|
|
if (NEW_PASSWORD !== CONFIRM_NEW_PASSWORD) {
|
|
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(NEW_PASSWORD, 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);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Normalize headers from file (case-insensitive) and map to field names.
|
|
* @param {Array} headers Array of headers from file
|
|
* @returns {Object} Mapping of normalized headers
|
|
*/
|
|
function normalizeHeaders(headers) {
|
|
const synonyms = {
|
|
NISN: ["nisn", "no nisn", "nomor nisn"],
|
|
NAME_USERS: ["nama", "nama lengkap", "full name", "nama siswa"],
|
|
};
|
|
|
|
const normalized = {};
|
|
|
|
headers.forEach((header, index) => {
|
|
const match = findClosestMatch(header, synonyms);
|
|
if (match) {
|
|
normalized[match] = header;
|
|
}
|
|
});
|
|
|
|
return normalized;
|
|
}
|
|
|
|
function findClosestMatch(header, synonyms) {
|
|
header = header.toLowerCase();
|
|
for (const [key, values] of Object.entries(synonyms)) {
|
|
if (values.some((value) => header.includes(value))) {
|
|
return key;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function normalizeHeadersXLSX(headers) {
|
|
const mapping = {
|
|
nisn: "NISN",
|
|
"no nisn": "NISN",
|
|
"nomor nisn": "NISN",
|
|
nama: "NAME_USERS",
|
|
"nama lengkap": "NAME_USERS",
|
|
"full name": "NAME_USERS",
|
|
"nama siswa": "NAME_USERS",
|
|
};
|
|
|
|
const normalized = {};
|
|
|
|
headers.forEach((header, index) => {
|
|
const normalizedHeader = header.trim().toLowerCase();
|
|
if (mapping[normalizedHeader]) {
|
|
normalized[mapping[normalizedHeader]] = index + 1;
|
|
}
|
|
});
|
|
|
|
return normalized;
|
|
}
|