/** * 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", }); } }; // Logout: Clear the JWT cookie exports.logout = (req, res) => { res.clearCookie("access_token", { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax", }); res.json({ success: true, message: "Logged out successfully" }); };