/** * Copyright (c) 2026 Niolla * All rights reserved. * * This source code is proprietary and confidential. * Unauthorized copying, modification, distribution, or use * of this file, via any medium, is strictly prohibited. */ // app/controllers/auth.controller.js const { checkPassword, hashPassword } = require("../utils/hashPassword.util"); const { sendMail } = require("../utils/mail.util"); const { validatePassword, } = require("../utils/validation/validatePassword.util"); const { validateEmail } = require("../utils/validation/validateEmail.util"); const { generateOTP, validateOTP } = require("../utils/otp.util"); const { getCachedUser, clearUserCache } = require("../utils/cache.util"); const { generateToken } = require("../utils/jwt.util"); const { createRefreshSession, validateRefreshSession, deleteRefreshSession, deleteAllUserSessions, } = require("../utils/refreshSession.util"); const { createPasswordReset, verifyPasswordResetToken, deletePasswordReset, sendPasswordResetEmail, sendPasswordChangedEmail, } = require("../utils/passwordReset.utill"); const db = require("../models"); const { log } = require("../utils/consoleLog.utill"); const appName = process.env.APP_NAME || "Niolla"; const User = db.User; // Login Step 1: Request OTP exports.loginReq = async (req, res) => { try { const { email, password } = req.body; const user = await getCachedUser(email); if (!user) { return res .status(404) .send({ success: false, message: "User Not Found" }); } const passwordIsValid = await checkPassword(password, user.password); if (!passwordIsValid) { return res .status(401) .send({ success: false, message: "Invalid Password" }); } const otp = generateOTP(email); await sendMail({ to: email, subject: `OTP for Your ${appName} Account`, templateName: "otp", templateVars: { firstName: user.firstName, otp: otp, }, text: `Hello ${user.firstName}, your otp is ${otp}`, }); log(`OTP for ${email}: ${otp}`); log(`OTP sent to ${email} successfully.`); res.status(201).send({ success: true, message: "OTP Sent Successfully" }); } catch (error) { log("Error occurred while sending OTP:", error); res.status(500).send({ success: false, message: error.message }); } }; // Login Step 2: Verify OTP and issue JWT exports.login = async (req, res) => { try { const { email, otp } = req.body; if (!email || !otp) { return res .status(400) .send({ success: false, message: "Email and OTP are required" }); } const user = await getCachedUser(email); if (!user) { return res .status(404) .send({ success: false, message: "User Not Found" }); } if (user.accountStatus !== "ACTIVE") { return res.status(403).send({ success: false, message: "Account is not active", }); } const isValidOTP = validateOTP(email, String(otp)); if (!isValidOTP) { return res .status(401) .send({ success: false, message: "Invalid or Expired OTP" }); } // Generate JWT token const token = generateToken({ id: user.id, firstName: user.firstName, lastName: user.lastName, email: user.email, accountType: user.accountType, }); const { refreshToken } = createRefreshSession(user.id); // 3. Set JWT as HttpOnly cookie res.cookie("access_token", token, { httpOnly: true, // JS cannot access secure: process.env.NODE_ENV === "production", // HTTPS only in prod sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax", maxAge: 15 * 60 * 1000, // 1 day }); res.cookie("refresh_token", refreshToken, { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax", maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days }); log(`JWT issued for ${email}`); clearUserCache(email); res.status(200).send({ success: true, message: "Login Successful", data: { id: user.id, email: user.email, firstName: user.firstName, lastName: user.lastName, role: user.role, accountType: user.accountType, accessToken: token, }, }); } catch (error) { log("Error occurred during login:", error); res.status(500).send({ success: false, message: error.message }); } }; exports.refreshToken = async (req, res) => { try { const refreshToken = req.cookies?.refresh_token; if (!refreshToken) { return res.status(401).send({ success: false, message: "Refresh token is required", }); } const session = validateRefreshSession(refreshToken); if (!session) { res.clearCookie("refresh_token"); return res.status(401).send({ success: false, message: "Invalid or expired session. Please login again.", }); } const user = await User.findByPk(session.userId); if (!user || user.accountStatus !== "ACTIVE") { deleteRefreshSession(session.sessionId); return res.status(401).send({ success: false, message: "Session is no longer valid", }); } // Generate new Access Token const token = generateToken({ id: user.id, firstName: user.firstName, lastName: user.lastName, email: user.email, accountType: user.accountType, }); // Generate new Refresh Token const { refreshToken: newRefreshToken } = createRefreshSession(user.id); deleteRefreshSession(session.sessionId); // Replace access cookie res.cookie("access_token", token, { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax", maxAge: 15 * 60 * 1000, }); // Replace refresh cookie res.cookie("refresh_token", newRefreshToken, { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax", maxAge: 7 * 24 * 60 * 60 * 1000, }); return res.status(200).send({ success: true, message: "Session refreshed successfully", }); } catch (error) { console.error("REFRESH ERROR:", error); return res.status(401).send({ success: false, message: "Invalid or expired session. Please login again.", }); } }; exports.forgotPassword = async (req, res) => { try { let { email } = req.body; if (!email) { return res.status(400).send({ success: false, message: "Email is required", }); } email = email.trim().toLowerCase(); if (!validateEmail(email)) { return res.status(400).send({ success: false, message: "Invalid email address", }); } const user = await User.findOne({ where: { email }, }); if (!user) { return res.status(200).send({ success: true, message: "If an account exists for this email, a password reset link has been sent.", }); } const resetToken = await createPasswordReset(user.id); await sendPasswordResetEmail(user.email, user.firstName, resetToken); return res.status(200).send({ success: true, message: "If an account exists for this email, a password reset link has been sent.", }); } catch (error) { console.error("FORGOT PASSWORD ERROR:", error); return res.status(500).send({ success: false, message: "Unable to process password reset request", }); } }; exports.resetPassword = async (req, res) => { try { const { token, newPassword, confirmPassword } = req.body; if (!token || !newPassword || !confirmPassword) { return res.status(400).send({ success: false, message: "Token, new password and confirm password are required", }); } if (newPassword !== confirmPassword) { return res.status(400).send({ success: false, message: "Passwords do not match", }); } if (!validatePassword(newPassword)) { return res.status(400).send({ success: false, message: "Password does not meet the required criteria", }); } const verification = await verifyPasswordResetToken(token); if (!verification) { return res.status(400).send({ success: false, message: "Reset token is invalid or expired", }); } const { userId, redisKey } = verification; const user = await User.findByPk(userId); if (!user) { await deletePasswordReset(redisKey); return res.status(400).send({ success: false, message: "Reset token is invalid or expired", }); } const samePassword = await checkPassword(newPassword, user.password); if (samePassword) { return res.status(400).send({ success: false, message: "New password must be different from the current password", }); } const hashedPassword = await hashPassword(newPassword); user.password = hashedPassword; user.passwordChangedAt = new Date(); await user.save(); await sendPasswordChangedEmail(user.email, user.firstName); await deletePasswordReset(redisKey); deleteAllUserSessions(user.id); return res.status(200).send({ success: true, message: "Password reset successfully. Please login again.", }); } catch (error) { console.error("RESET PASSWORD ERROR:", error); return res.status(500).send({ success: false, message: "Failed to reset password", }); } }; exports.changePassword = async (req, res) => { try { // User ID comes from authenticate middleware const userId = req.user.id; const { currentPassword, newPassword, confirmPassword } = req.body; // 1. Check required fields if (!currentPassword || !newPassword || !confirmPassword) { return res.status(400).send({ success: false, message: "Current password, new password and confirm password are required", }); } // 2. Check new password and confirmation if (newPassword !== confirmPassword) { return res.status(400).send({ success: false, message: "New password and confirm password do not match", }); } // 3. Validate password policy const passwordValid = validatePassword(newPassword); if (!passwordValid) { return res.status(400).send({ success: false, message: "New password does not meet the required criteria", }); } // 4. Get logged-in user from database const user = await User.findByPk(userId); if (!user) { return res.status(404).send({ success: false, message: "User not found", }); } // 5. Check current password const currentPasswordValid = await checkPassword( currentPassword, user.password, ); if (!currentPasswordValid) { return res.status(401).send({ success: false, message: "Current password is incorrect", }); } // 6. Make sure new password is different const sameAsOldPassword = await checkPassword(newPassword, user.password); if (sameAsOldPassword) { return res.status(400).send({ success: false, message: "New password must be different from current password", }); } // 7. Hash new password const hashedPassword = await hashPassword(newPassword); // 8. Update user user.password = hashedPassword; user.passwordChangedAt = new Date(); await user.save(); // 9. Revoke all refresh sessions deleteAllUserSessions(user.id); // 10. Clear auth cookies res.clearCookie("access_token", { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax", }); res.clearCookie("refresh_token", { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax", }); // 11. Send confirmation email try { await sendPasswordChangedEmail(user.email, user.firstName); } catch (mailError) { console.error("PASSWORD CHANGED EMAIL ERROR:", mailError); } // 12. Response return res.status(200).send({ success: true, message: "Password changed successfully. Please login again.", }); } catch (error) { console.error("CHANGE PASSWORD ERROR:", error); return res.status(500).send({ success: false, message: "Failed to change password", }); } }; // Logout: Clear the JWT cookie exports.logout = async (req, res) => { try { // 1. Get refresh token from cookie const refreshToken = req.cookies?.refresh_token; // 2. If refresh token exists, find its session if (refreshToken) { const session = validateRefreshSession(refreshToken); // 3. Delete refresh session from server RAM if (session) { deleteRefreshSession(session.sessionId); console.log(`Refresh session deleted: ${session.sessionId}`); } } // 4. Clear access token cookie res.clearCookie("access_token", { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax", }); // 5. Clear refresh token cookie res.clearCookie("refresh_token", { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax", }); // 6. Send response return res.status(200).json({ success: true, message: "Logged out successfully", }); } catch (error) { console.error("LOGOUT ERROR:", error); return res.status(500).json({ success: false, message: "Failed to logout", }); } };