import response from "../../response.js"; import bcrypt from "bcryptjs"; 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: ` Email Verification

SEALS

Hello, ${NAME_USERS}! 👋

Welcome to SEALS! We're excited to have you on board.

To get started, please verify your email address by clicking the button below:

Verify Email

Important: 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.

If you didn't create an account with SEALS, please ignore this email.

Thank you for choosing SEALS!

The SEALS Team

`, }); 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: ` Email Verification

SEALS

Hello, ${NAME_USERS}! 👋

Welcome to SEALS! We're excited to have you on board.

To get started, please verify your email address by clicking the button below:

Verify Email

Important: 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.

If you didn't create an account with SEALS, please ignore this email.

Thank you for choosing SEALS!

The SEALS Team

`, }); 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 registerStudentForAdmin = 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: 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, }; 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 { 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: ` Reset Password

SEALS

Password Reset Request

Hello!

We received a request to reset the password for your SEALS account. To proceed with the password reset, please click the button below:

Reset Password

Important: This reset password link will expire in 1 hour. Please reset your password as soon as possible to maintain access to your account.

⚠️ 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.

Thank you for using SEALS!

The SEALS Team

`, }; 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); } } };