diff --git a/app/controllers/auth.controller.js b/app/controllers/auth.controller.js index da10770..318604f 100644 --- a/app/controllers/auth.controller.js +++ b/app/controllers/auth.controller.js @@ -12,12 +12,20 @@ const { checkPassword } = require("../utils/hashPassword.util"); const { sendMail } = require("../utils/mail.util"); const { generateOTP, validateOTP } = require("../utils/otp.util"); -const {getCachedUser,clearUserCache} = require("../utils/cache.util"); +const { getCachedUser, clearUserCache } = require("../utils/cache.util"); const { generateToken } = require("../utils/jwt.util"); +const { + createRefreshSession, + validateRefreshSession, + deleteRefreshSession, +} = require("../utils/refreshSession.util"); +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 { @@ -47,7 +55,7 @@ exports.loginReq = async (req, res) => { firstName: user.firstName, otp: otp, }, - text: `Hello ${user.firstName}, your otp is ${otp}`, + text: `Hello ${user.firstName}, your otp is ${otp}`, }); log(`OTP for ${email}: ${otp}`); @@ -65,6 +73,12 @@ 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) { @@ -73,7 +87,14 @@ exports.login = async (req, res) => { .send({ success: false, message: "User Not Found" }); } - const isValidOTP = validateOTP(email, otp); + 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 @@ -87,16 +108,24 @@ exports.login = async (req, res) => { firstName: user.firstName, lastName: user.lastName, email: user.email, - role: user.role, 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: 24 * 60 * 60 * 1000, // 1 day + 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}`); @@ -112,6 +141,7 @@ exports.login = async (req, res) => { lastName: user.lastName, role: user.role, accountType: user.accountType, + accessToken: token, }, }); } catch (error) { @@ -120,6 +150,83 @@ exports.login = async (req, res) => { } }; +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.", + }); + } +}; + // Logout: Clear the JWT cookie exports.logout = (req, res) => { res.clearCookie("access_token", { diff --git a/app/controllers/user.controller.js b/app/controllers/user.controller.js index caeeda1..d938f9b 100644 --- a/app/controllers/user.controller.js +++ b/app/controllers/user.controller.js @@ -9,19 +9,29 @@ // app/controllers/user.controller.js -// const { Op } = require("sequelize"); const db = require("../models"); const { hashPassword } = require("../utils/hashPassword.util"); -const { validatePassword } = require("../utils/validation/validatePassword.util"); +const { + validatePassword, +} = require("../utils/validation/validatePassword.util"); const { validateEmail } = require("../utils/validation/validateEmail.util"); const { generateUserId, generateId } = require("../utils/idGen.util"); +const { + createCustomerDetails, +} = require("../utils/users/createCustomerDetails.util"); +const { + createBusinessCustomerDetails, +} = require("../utils/users/createBusinessCustomer.util"); const { logActivity } = require("../services/activity.service"); const { sendMail } = require("../utils/mail.util"); const { - createEmailVerification, verifyEmailVerificationToken, deleteEmailVerification} = require("../utils/emailVerification.util");; + createEmailVerification, + verifyEmailVerificationToken, + sendVerificationEmail, + deleteEmailVerification, +} = require("../utils/emailVerification.util"); const User = db.User; const Profile = db.Profile; -// const EmailVerification = db.EmailVerification; // Create a new user exports.createNewUser = async (req, res) => { @@ -33,28 +43,48 @@ exports.createNewUser = async (req, res) => { lastName, email, password, + accountType, + address, + phoneNumber, + businessName, + businessRegistrationNumber, + businessType, + contactName, + businessEmail, + expectedMonthlyVolume, + note, } = req.body; - if (!firstName || !lastName || !email || !password) { - await transaction.rollback(); + if (!firstName || !lastName || !email || !password || !accountType) { + await transaction.rollback(); - return res.status(400).send({ - success: false, - message: - "First name, last name, email and password are required", - }); - } + return res.status(400).send({ + success: false, + message: + "First name, last name, email, password and account type are required", + }); + } - const emailValid = validateEmail(email); + if (accountType !== "customer" && accountType !== "business_customer") { + await transaction.rollback(); -if (!emailValid) { - await transaction.rollback(); + return res.status(400).send({ + success: false, + message: + "Invalid account type. Must be either 'customer' or 'business_customer'", + }); + } - return res.status(400).send({ - success: false, - message: "Invalid email address", - }); -} + const emailValid = validateEmail(email); + + if (!emailValid) { + await transaction.rollback(); + + return res.status(400).send({ + success: false, + message: "Invalid email address", + }); + } const userExists = await User.findOne({ where: { email } }); if (userExists) { @@ -66,14 +96,6 @@ if (!emailValid) { }); } - // let pass; - - // if(accountType === "admin" || accountType === "superadmin" || accountType === "manager" || accountType === "support_agent") { - // pass = process.env.DEFAULT_PASSWORD; - // }else{ - // pass = password; - // } - const validatePasswordResult = validatePassword(password); if (!validatePasswordResult) { @@ -95,13 +117,12 @@ if (!emailValid) { lastName, email, password: hashedPassword, - // accountType, + accountType, accountStatus: "PENDING_VERIFICATION", - emailVerifiedAt: null, + emailVerifiedAt: null, }, { transaction }, ); - const newProfile = await Profile.create( { @@ -117,78 +138,52 @@ if (!emailValid) { { transaction }, ); + //create customer and business customer + if (accountType === "customer") { + const customerData = { + address,phoneNumber, + }; + + await createCustomerDetails( + newUser.id, + customerData, + transaction, + ); + } else if (accountType === "business_customer") { + const businessData = { + businessName, + businessRegistrationNumber, + businessType, + contactName, + phoneNumber, + businessEmail, + expectedMonthlyVolume, + note, + }; + + await createBusinessCustomerDetails( + newUser.id, + businessData, + transaction, + ); + } + await transaction.commit(); const verificationToken = await createEmailVerification(newUser.id); - const confirmationLink = - `${process.env.FRONTEND_URL}/verify-email?token=${verificationToken}`; - - try { - - await sendMail({ - to: - email, - - subject: - "Confirm Your ZUMRI Account", - - templateName: - "emailVerification", - - templateVars: { - - customer_name: - firstName, - - confirmation_link: - confirmationLink, - }, - - text: - `Hello ${firstName}, please verify your ZUMRI account using this link: ${confirmationLink}`, - }); - - - } catch (mailError) { - - console.error( - "VERIFICATION EMAIL ERROR:", - mailError + await sendVerificationEmail( + newUser.email, + newUser.firstName, + verificationToken, ); - - - return res.status(201).send({ - success: true, - - message: - "Account created, but verification email could not be sent. Please request a new verification email.", - - data: { - id: - newUser.id, - - firstName: - newUser.firstName, - - lastName: - newUser.lastName, - - email: - newUser.email, - - accountType: - newUser.accountType, - - accountStatus: - newUser.accountStatus, - }, - }); + } catch (error) { + console.error("Error sending verification email:", error); } await logActivity({ - user: req.user, + user: newUser, description: `Created New User with ID: ${newUser.id}`, type: "CREATE_USER", module: "User Management", @@ -202,20 +197,16 @@ if (!emailValid) { firstName: newUser.firstName, lastName: newUser.lastName, email: newUser.email, - // accountType: newUser.accountType, - // role: newUser.role, - // department: newUser.department, + accountType: newUser.accountType, + }, }); } catch (error) { - if (!transaction.finished) { - await transaction.rollback(); - } + if (!transaction.finished) { + await transaction.rollback(); + } - console.error( - "CREATE USER ERROR:", - error - ); + console.error("CREATE USER ERROR:", error); res.status(500).send({ success: false, @@ -226,168 +217,100 @@ if (!emailValid) { }; // Verify customer email -exports.verifyEmail = - async (req, res) => { +exports.verifyEmail = async (req, res) => { + const transaction = await db.sequelize.transaction(); - const transaction = - await db.sequelize.transaction(); + try { + const { token } = req.body; + if (!token) { + await transaction.rollback(); - try { - const { - token, - } = req.body; + return res.status(400).send({ + success: false, - - if (!token) { - - await transaction.rollback(); - - - return res.status(400).send({ - success: - false, - - message: - "Verification token is required", - }); - } - - const verification = - await verifyEmailVerificationToken( - token - ); - - if (!verification) { - - await transaction.rollback(); - - - return res.status(400).send({ - success: - false, - - message: - "Verification token is invalid or expired", - }); - } - - - - const { - userId, - redisKey, - } = - verification; - - const user = - await User.findOne({ - where: { - id: - userId, - }, - - transaction, - }); - - - - if (!user) { - - await transaction.rollback(); - - await deleteEmailVerification( - redisKey - ); - - - return res.status(404).send({ - success: - false, - - message: - "User not found", - }); - } - - if ( - user.accountStatus === - "ACTIVE" && - user.emailVerifiedAt - ) { - - await transaction.rollback(); - - - // Token is no longer needed - await deleteEmailVerification( - redisKey - ); - - - return res.status(400).send({ - success: - false, - - message: - "Email is already verified", - }); - } - - - user.accountStatus = - "ACTIVE"; - - - user.emailVerifiedAt = - new Date(); - - - await user.save({ - transaction, - }); - - await transaction.commit(); - - await deleteEmailVerification( - redisKey - ); - - - - return res.status(200).send({ - success: - true, - - message: - "Email verified successfully. Your account is now active.", - }); - - - } catch (error) { - - if (!transaction.finished) { - - await transaction.rollback(); - - } - - - console.error( - "VERIFY EMAIL ERROR:", - error - ); - - - return res.status(500).send({ - success: - false, - - message: - "Failed to verify email", + message: "Verification token is required", }); } - }; + + const verification = await verifyEmailVerificationToken(token); + + if (!verification) { + await transaction.rollback(); + + return res.status(400).send({ + success: false, + + message: "Verification token is invalid or expired", + }); + } + + const { userId, redisKey } = verification; + + const user = await User.findOne({ + where: { + id: userId, + }, + + transaction, + }); + + if (!user) { + await transaction.rollback(); + + await deleteEmailVerification(redisKey); + + return res.status(404).send({ + success: false, + + message: "User not found", + }); + } + + if (user.accountStatus === "ACTIVE" && user.emailVerifiedAt) { + await transaction.rollback(); + + // Token is no longer needed + await deleteEmailVerification(redisKey); + + return res.status(400).send({ + success: false, + + message: "Email is already verified", + }); + } + + user.accountStatus = "ACTIVE"; + + user.emailVerifiedAt = new Date(); + + await user.save({ + transaction, + }); + + await transaction.commit(); + + await deleteEmailVerification(redisKey); + + return res.status(200).send({ + success: true, + + message: "Email verified successfully. Your account is now active.", + }); + } catch (error) { + if (!transaction.finished) { + await transaction.rollback(); + } + + console.error("VERIFY EMAIL ERROR:", error); + + return res.status(500).send({ + success: false, + + message: "Failed to verify email", + }); + } +}; // Get users with pagination (20 per page) exports.getAllUsers = async (req, res) => { diff --git a/app/models/index.js b/app/models/index.js index d695284..18b07a6 100644 --- a/app/models/index.js +++ b/app/models/index.js @@ -41,6 +41,8 @@ db.sequelize = sequelize; // User and Authentication db.User = require("./user/user.model")(sequelize, DataTypes); +db.Customer = require("./user/customer.model")(sequelize, DataTypes); +db.BusinessCustomer = require("./user/businessCustomer.model")(sequelize, DataTypes); db.UserActivity = require("./activities/userActivities.model")(sequelize, DataTypes); db.Profile = require("./user/profile.model")(sequelize, DataTypes); diff --git a/app/models/user/businessCustomer.model.js b/app/models/user/businessCustomer.model.js new file mode 100644 index 0000000..10e5282 --- /dev/null +++ b/app/models/user/businessCustomer.model.js @@ -0,0 +1,65 @@ +//app/models/user/businessCustomer.model.js + +module.exports = (sequelize, DataTypes) => { + const BusinessCustomer = sequelize.define( + "BusinessCustomer", + { + business_customer_id: { + type: DataTypes.STRING, + primaryKey: true, + }, + + user_id: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + }, + + businessName: { + type: DataTypes.STRING, + allowNull: false, + }, + businessRegistrationNumber: { + type: DataTypes.STRING, + allowNull: false, + }, + businessType: { + type: DataTypes.STRING, + allowNull: false, + }, + contactName: { + type: DataTypes.STRING, + allowNull: false, + }, + phoneNumber: { + type: DataTypes.STRING, + allowNull: false, + }, + businessEmail: { + type: DataTypes.STRING, + allowNull: false, + }, + expectedMonthlyVolume: { + type: DataTypes.STRING, + allowNull: false, + }, + note: { + type: DataTypes.STRING, + allowNull: true, + }, + }, + { + tableName: "business_customers", + timestamps: true, + }, + ); + + BusinessCustomer.associate = (db) => { + BusinessCustomer.belongsTo(db.User, { + foreignKey: "user_id", + as: "user", + }); + }; + + return BusinessCustomer; +}; diff --git a/app/models/user/customer.model.js b/app/models/user/customer.model.js new file mode 100644 index 0000000..fc76a5f --- /dev/null +++ b/app/models/user/customer.model.js @@ -0,0 +1,49 @@ +/** + * 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/models/customer.model.js + +module.exports = (sequelize, DataTypes) => { + const Customer = sequelize.define( + "Customer", + { + customer_id: { + type: DataTypes.STRING, + primaryKey: true, + collate: "utf8mb4_general_ci", + }, + user_id: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + }, + address: { + type: DataTypes.STRING, + allowNull: false, + }, + phoneNumber: { + type: DataTypes.STRING, + allowNull: false, + }, + }, + { + tableName: "customers", + timestamps: true, + }, + ); + + Customer.associate = (db) => { + Customer.belongsTo(db.User, { + foreignKey: "user_id", + as: "user", + }); + }; + + return Customer; +}; diff --git a/app/models/user/user.model.js b/app/models/user/user.model.js index e73e011..2dc99c2 100644 --- a/app/models/user/user.model.js +++ b/app/models/user/user.model.js @@ -36,7 +36,7 @@ module.exports = (sequelize, DataTypes) => { allowNull: false }, accountType: { - type: DataTypes.ENUM("admin", "superadmin", "manager", "business_customer", "rider", "customer","support_agent"), + type: DataTypes.ENUM("business_customer", "customer"), defaultValue: "customer" }, accountStatus: { @@ -71,6 +71,14 @@ module.exports = (sequelize, DataTypes) => { foreignKey: "user_id", as: "profile", }); + User.hasOne(db.Customer, { + foreignKey: "user_id", + as: "customer", + }); + User.hasOne(db.BusinessCustomer, { + foreignKey: "user_id", + as: "businessCustomer", + }); }; diff --git a/app/routes/auth.routes.js b/app/routes/auth.routes.js index 4d334ad..8e22a46 100644 --- a/app/routes/auth.routes.js +++ b/app/routes/auth.routes.js @@ -24,6 +24,7 @@ router.get("/me", authenticate, (req, res) => { router.post('/req-otp', authController.loginReq); router.post('/login', authController.login); +router.post('/refresh', authController.refreshToken); router.post('/logout', authController.logout); module.exports = router; diff --git a/app/templates/emails/otp.html b/app/templates/emails/otp.html index f2672dd..b820af8 100644 --- a/app/templates/emails/otp.html +++ b/app/templates/emails/otp.html @@ -2,14 +2,14 @@ - GLAURA 2FA Code + ZUMRI CEYLON

Dear {{firstName}},

-

Greetings from Oceanic Titan!

+

Greetings from ZUMRI CEYLON!

-

Your One Time Password (OTP) to log in to your Oceanic Titan account is mentioned below.

+

Your One Time Password (OTP) to log in to your ZUMRI CEYLON account is mentioned below.

Please enter this OTP in the required field to proceed further:

@@ -24,14 +24,14 @@ -

The above-mentioned OTP is valid for 5 minutes.

+

The above-mentioned OTP is valid for 15 minutes.


Regards,
- Oceanic Titan Team + ZUMRI CEYLON Team

-

{{currentYear}} Oceanic Titan. This is an auto-generated email, please do not reply to this email.

+

{{currentYear}} ZUMRI CEYLON. This is an auto-generated email, please do not reply to this email.

\ No newline at end of file diff --git a/app/utils/emailVerification.util.js b/app/utils/emailVerification.util.js index 2ab997d..d7e26af 100644 --- a/app/utils/emailVerification.util.js +++ b/app/utils/emailVerification.util.js @@ -1,103 +1,91 @@ // app/utils/emailVerification.util.js const crypto = require("crypto"); - +const { sendMail } = require("../utils/mail.util"); const redis = require("../config/redisClient"); const EMAIL_VERIFICATION_TTL = - Number( - process.env.EMAIL_VERIFICATION_TTL_SECONDS - ) || 1800; + Number(process.env.EMAIL_VERIFICATION_TTL_SECONDS) || 1800; const generateEmailVerificationToken = () => { - return crypto - .randomBytes(32) - .toString("hex"); + return crypto.randomBytes(32).toString("hex"); }; const hashEmailVerificationToken = (token) => { - return crypto - .createHash("sha256") - .update(token) - .digest("hex"); + return crypto.createHash("sha256").update(token).digest("hex"); }; const createEmailVerification = async (userId) => { - // 1. Generate raw token - const token = - generateEmailVerificationToken(); - + const token = generateEmailVerificationToken(); // 2. Hash token - const tokenHash = - hashEmailVerificationToken(token); - + const tokenHash = hashEmailVerificationToken(token); // 3. Create Redis key - const redisKey = - `email-verification:${tokenHash}`; + const redisKey = `email-verification:${tokenHash}`; - await redis.set( - redisKey, - userId, - "EX", - EMAIL_VERIFICATION_TTL - ); + await redis.set(redisKey, userId, "EX", EMAIL_VERIFICATION_TTL); return token; }; - /** * Check a verification token. */ -const verifyEmailVerificationToken = - async (token) => { +const verifyEmailVerificationToken = async (token) => { + if (!token || typeof token !== "string") { + return null; + } - if ( - !token || - typeof token !== "string" - ) { - return null; - } + // 1. Hash token received from user + const tokenHash = hashEmailVerificationToken(token); + // 2. Build same Redis key + const redisKey = `email-verification:${tokenHash}`; - // 1. Hash token received from user - const tokenHash = - hashEmailVerificationToken(token); + // 3. Search Redis + const userId = await redis.get(redisKey); + if (!userId) { + return null; + } - // 2. Build same Redis key - const redisKey = - `email-verification:${tokenHash}`; - - - // 3. Search Redis - const userId = - await redis.get(redisKey); - - if (!userId) { - return null; - } - - - return { - userId, - redisKey, - }; + return { + userId, + redisKey, }; +}; -const deleteEmailVerification = - async (redisKey) => { +//send verification email to user +const sendVerificationEmail = async (email, firstName, verificationToken) => { + const confirmationLink = `${process.env.FRONTEND_URL}/verify-email?token=${verificationToken}`; - await redis.del(redisKey); - }; + // Send email + await sendMail({ + to: email, + subject: "Confirm Your ZUMRI Account", + + templateName: "emailVerification", + + templateVars: { + customer_name: firstName, + confirmation_link: confirmationLink, + }, + + text: `Hello ${firstName}, please verify your ZUMRI account using this link: ${confirmationLink}`, + }); +}; + +const deleteEmailVerification = async (redisKey) => { + await redis.del(redisKey); +}; module.exports = { createEmailVerification, verifyEmailVerificationToken, + sendVerificationEmail, deleteEmailVerification, hashEmailVerificationToken, -}; \ No newline at end of file +}; diff --git a/app/utils/idGen.util.js b/app/utils/idGen.util.js index 5d9d7f2..0fc0abc 100644 --- a/app/utils/idGen.util.js +++ b/app/utils/idGen.util.js @@ -23,6 +23,14 @@ const generateUserId = () => { return "usr_" + Math.random().toString(36).slice(2, 10); }; +const generateCustomerId = () => { + return "cust_" + Math.random().toString(36).slice(2, 10); +} + +const generateBusinessCustomerId = () => { + return "b_cust_" + Math.random().toString(36).slice(2, 10); +} + const generateClientId = () => { const prefix = "cli_"; return prefix + uuidv4(); @@ -141,6 +149,8 @@ const generateDocumentReferenceNo = async (type) => { module.exports = { generateUserId, + generateCustomerId, + generateBusinessCustomerId, generateClientId, generateInquId, generateInquRefNo, diff --git a/app/utils/jwt.util.js b/app/utils/jwt.util.js index f2bb54a..7d91279 100644 --- a/app/utils/jwt.util.js +++ b/app/utils/jwt.util.js @@ -15,6 +15,9 @@ require("dotenv").config(); const JWT_SECRET = process.env.JWT_SECRET || "your_jwt_secret_key"; const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || "1d"; // token validity +const REFRESH_TOKEN_SECRET = process.env.REFRESH_TOKEN_SECRET || "your_refresh_token_secret_key"; +const REFRESH_TOKEN_DAYS = process.env.REFRESH_TOKEN_DAYS || "7d"; // refresh token validity + /** * Generate JWT token * @param {Object} payload - usually { id, email, role } @@ -33,4 +36,12 @@ const verifyToken = (token) => { return jwt.verify(token, JWT_SECRET); }; -module.exports = { generateToken, verifyToken }; +const generateRefreshToken = (payload) => { + return jwt.sign(payload, REFRESH_TOKEN_SECRET, { expiresIn: REFRESH_TOKEN_DAYS }); +} + +const verifyRefreshToken = (token) => { + return jwt.verify(token, REFRESH_TOKEN_SECRET); +} + +module.exports = { generateToken, verifyToken, generateRefreshToken, verifyRefreshToken }; diff --git a/app/utils/otp.util.js b/app/utils/otp.util.js index 7c391d8..8259d2b 100644 --- a/app/utils/otp.util.js +++ b/app/utils/otp.util.js @@ -17,8 +17,9 @@ function generateOTP(key){ return otp; } -function saveOTP(key, otp, ttl = 5 * 60 * 1000) { // default TTL: 5 mins - const expiresAt = Date.now() + ttl; +function saveOTP(key, otp) { + ttl = parseInt(process.env.LOGIN_OTP_TTL_SECONDS || 300); + const expiresAt = Date.now() + ttl * 1000; // Convert seconds to milliseconds otpCache.set(key, { otp, expiresAt }); } diff --git a/app/utils/refreshSession.util.js b/app/utils/refreshSession.util.js new file mode 100644 index 0000000..bef329a --- /dev/null +++ b/app/utils/refreshSession.util.js @@ -0,0 +1,201 @@ +// app/utils/refreshSession.util.js + +const crypto = require("crypto"); + +const { + generateRefreshToken, + verifyRefreshToken, +} = require("./jwt.util"); + +const refreshSessions = new Map(); + + +// Default = 7 days +const REFRESH_SESSION_TTL = + 7 * 24 * 60 * 60 * 1000; + + +const hashRefreshToken = (token) => { + return crypto + .createHash("sha256") + .update(token) + .digest("hex"); +}; + +const createRefreshSession = (userId) => { + + // Unique session ID + const sessionId = + crypto.randomUUID(); + + + // Create raw Refresh JWT + const refreshToken = + generateRefreshToken({ + sub: userId, + sid: sessionId, + }); + + + // Hash raw refresh token + const tokenHash = + hashRefreshToken( + refreshToken + ); + + + // Expiration time + const expiresAt = + Date.now() + + REFRESH_SESSION_TTL; + + + // Save only HASH in RAM + refreshSessions.set( + sessionId, + { + userId, + tokenHash, + expiresAt, + } + ); + + + return { + refreshToken, + sessionId, + }; +}; + +const validateRefreshSession = ( + refreshToken +) => { + + if (!refreshToken) { + return null; + } + + + let decoded; + + try { + + decoded = + verifyRefreshToken( + refreshToken + ); + + } catch (error) { + + return null; + + } + + + const sessionId = + decoded.sid; + + const userId = + decoded.sub; + + + if (!sessionId || !userId) { + return null; + } + + + // Get session from RAM + const session = + refreshSessions.get( + sessionId + ); + + + if (!session) { + return null; + } + + + // Check session expiration + if ( + Date.now() > + session.expiresAt + ) { + + refreshSessions.delete( + sessionId + ); + + return null; + } + + + // Hash received refresh token + const receivedHash = + hashRefreshToken( + refreshToken + ); + + + // Compare stored hash + if ( + receivedHash !== + session.tokenHash + ) { + return null; + } + + + // Extra user check + if ( + session.userId !== + userId + ) { + return null; + } + + + return { + userId, + sessionId, + }; +}; + +const deleteRefreshSession = ( + sessionId +) => { + + refreshSessions.delete( + sessionId + ); +}; + +const deleteAllUserSessions = ( + userId +) => { + + for ( + const [sessionId, session] + of refreshSessions.entries() + ) { + + if ( + session.userId === userId + ) { + + refreshSessions.delete( + sessionId + ); + + } + + } +}; + + +module.exports = { + createRefreshSession, + validateRefreshSession, + deleteRefreshSession, + deleteAllUserSessions, +}; \ No newline at end of file diff --git a/app/utils/users/createBusinessCustomer.util.js b/app/utils/users/createBusinessCustomer.util.js new file mode 100644 index 0000000..1f55a2a --- /dev/null +++ b/app/utils/users/createBusinessCustomer.util.js @@ -0,0 +1,86 @@ +// app/utils/users/createBusinessCustomer.util.js + +const db = require("../../models"); + +const { + generateBusinessCustomerId, +} = require("../idGen.util"); + +const BusinessCustomer = db.BusinessCustomer; + + +/** + * Create business-customer-specific details + */ +const createBusinessCustomerDetails = async ( + userId, + businessData, + transaction +) => { + + const { + businessName, + businessRegistrationNumber, + businessType, + contactName, + phoneNumber, + businessEmail, + expectedMonthlyVolume, + note, + } = businessData; + + + if ( + !businessName || + !businessRegistrationNumber || + !businessType || + !contactName || + !phoneNumber || + !businessEmail || + !expectedMonthlyVolume + ) { + throw new Error( + "Required business customer details are missing" + ); + } + + + const businessCustomer = + await BusinessCustomer.create( + { + business_customer_id: + generateBusinessCustomerId(), + + user_id: + userId, + + businessName, + + businessRegistrationNumber, + + businessType, + + contactName, + + phoneNumber, + + businessEmail, + + expectedMonthlyVolume, + + note: + note || null, + }, + { + transaction, + } + ); + + + return businessCustomer; +}; + + +module.exports = { + createBusinessCustomerDetails, +}; \ No newline at end of file diff --git a/app/utils/users/createCustomerDetails.util.js b/app/utils/users/createCustomerDetails.util.js new file mode 100644 index 0000000..81b4110 --- /dev/null +++ b/app/utils/users/createCustomerDetails.util.js @@ -0,0 +1,54 @@ +//app/utils/users/createCustomerDetails.util.js + +const db = require("../../models"); + +const { generateCustomerId } = require("../idGen.util"); + +const Customer = db.Customer; + +const createCustomerDetails = async ( + userId, + customerData, + transaction +) => { + + const { + address, + phoneNumber, + } = customerData; + + + if (!address || !phoneNumber) { + throw new Error( + "Address and phone number are required for customer" + ); + } + + + const customer = await Customer.create( + { + customer_id: + generateCustomerId(), + + user_id: + userId, + + address: + address, + + phoneNumber: + phoneNumber, + }, + { + transaction, + } + ); + + + return customer; +}; + + +module.exports = { + createCustomerDetails, +};