This commit is contained in:
Sathira Sri Sathara
2026-09-03 13:02:31 +05:30
22 changed files with 1980 additions and 554 deletions
+261 -38
View File
@@ -1,14 +1,39 @@
# Auth API
# Authentication API
#### Request OTP
The Authentication API provides OTP-based login, access-token renewal, password recovery, current-user lookup, and logout.
**Endpoint**
## Base URL
```
POST: http://localhost:3070/api/auth/req-otp
```text
http://localhost:3070/api/auth
```
**Request Body**
Requests and responses use JSON unless otherwise stated.
## Authentication
After a successful login, the API returns an access token in the response and sets two HTTP-only cookies:
- `access_token` — valid for 15 minutes
- `refresh_token` — valid for 7 days
Protected endpoints accept the access token through the `access_token` cookie or this header:
```http
Authorization: Bearer <access-token>
```
When using cookie authentication from a browser, send requests with credentials enabled.
---
## Request OTP
Validates the user's email and password, then sends a one-time password to the registered email address.
**Endpoint:** `POST` [http://localhost:3070/api/auth/req-otp](http://localhost:3070/api/auth/req-otp)
### Request body
```json
{
@@ -17,7 +42,7 @@ POST: http://localhost:3070/api/auth/req-otp
}
```
**Respond**
### Success response — `201 Created`
```json
{
@@ -26,17 +51,21 @@ POST: http://localhost:3070/api/auth/req-otp
}
```
### Error responses
- `401 Unauthorized` — invalid password
- `404 Not Found` — user not found
- `500 Internal Server Error` — OTP generation or email delivery failed
---
#### Login
## Login
**Endpoint**
Verifies the emailed OTP and creates an authenticated session. The user account must be active.
```
POST: http://localhost:3070/api/auth/login
```
**Endpoint:** `POST` [http://localhost:3070/api/auth/login](http://localhost:3070/api/auth/login)
**Request Body**
### Request body
```json
{
@@ -45,7 +74,7 @@ POST: http://localhost:3070/api/auth/login
}
```
**Respond**
### Success response — `200 OK`
```json
{
@@ -54,33 +83,40 @@ POST: http://localhost:3070/api/auth/login
"data": {
"id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
"email": "sathira@niolla.lk",
"firstName": "Jhon",
"lastName": "Doe",
"role": "System Developer",
"accountType": "admin"
"firstName": "Sathira",
"lastName": "Sri Sathsara",
"role": null,
"accountType": "admin",
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
}
```
The response also sets the `access_token` and `refresh_token` HTTP-only cookies.
### Error responses
- `400 Bad Request` — email or OTP is missing
- `401 Unauthorized` — OTP is invalid or expired
- `403 Forbidden` — account is not active
- `404 Not Found` — user not found
- `500 Internal Server Error` — login failed unexpectedly
---
#### Get Current User
## Get Current User
**Endpoint**
Returns the authenticated user's token claims and effective permissions.
```
GET: http://localhost:3070/api/auth/me
```
**Endpoint:** `GET` [http://localhost:3070/api/auth/me](http://localhost:3070/api/auth/me)
**Authorization**: Required (Bearer token)
**Authentication:** Required
**Request Body**
### Request body
```
No Body
```
No request body.
**Response (200)**
### Success response — `200 OK`
```json
{
@@ -90,7 +126,6 @@ No Body
"firstName": "Sathira",
"lastName": "Sri Sathsara",
"email": "sathira@niolla.lk",
"role": "System Developer",
"accountType": "admin",
"iat": 1778865265,
"exp": 1778868865,
@@ -99,23 +134,202 @@ No Body
}
```
### Error responses
- `401 Unauthorized` — token is missing, invalid, or expired
---
### Logout
## Refresh Session
**Endpoint**
Uses the HTTP-only refresh-token cookie to rotate the session and issue new access and refresh cookies.
```
POST: http://localhost:3070/api/auth/logout
**Endpoint:** `POST` [http://localhost:3070/api/auth/refresh](http://localhost:3070/api/auth/refresh)
### Request body
No request body. The `refresh_token` cookie is required.
### Success response — `200 OK`
```json
{
"success": true,
"message": "Session refreshed successfully"
}
```
**Request Body**
### Error response — `401 Unauthorized`
```
No Body
Returned when the refresh cookie is missing or the session is invalid, expired, or no longer active.
---
## Forgot Password
Sends a password-reset link when an account exists for the supplied email. The same success response is returned for unknown email addresses to prevent account discovery.
**Endpoint:** `POST` [http://localhost:3070/api/auth/forgot-password](http://localhost:3070/api/auth/forgot-password)
**Authentication:** Not required
### Request body
```json
{
"email": "sathira@niolla.lk"
}
```
**Respond**
### Success response — `200 OK`
```json
{
"success": true,
"message": "If an account exists for this email, a password reset link has been sent."
}
```
### Error responses
- `400 Bad Request` — email is missing or invalid
- `500 Internal Server Error` — the reset request could not be processed
---
## Reset Password
Sets a new password using the token from the password-reset email. A successful reset invalidates all existing refresh sessions for the user.
**Endpoint:** `POST` [http://localhost:3070/api/auth/reset-password](http://localhost:3070/api/auth/reset-password)
**Authentication:** Not required
### Request body
```json
{
"token": "password-reset-token",
"newPassword": "NewPassword@1234",
"confirmPassword": "NewPassword@1234"
}
```
The new password must contain at least one uppercase letter, one lowercase letter, one symbol, and four digits. It must differ from the current password.
### Success response — `200 OK`
```json
{
"success": true,
"message": "Password reset successfully. Please login again."
}
```
### Error responses
- `400 Bad Request` — fields are missing, passwords do not match, password rules are not met, the new password matches the current password, or the token is invalid or expired
- `500 Internal Server Error` — password reset failed unexpectedly
---
## Change Password
Changes the authenticated user's password. After a successful change, all refresh sessions are revoked, authentication cookies are cleared, and the user must log in again.
**Endpoint:** `POST` [http://localhost:3070/api/auth/change-password](http://localhost:3070/api/auth/change-password)
**Authentication:** Required
The access token may be supplied through the `access_token` cookie or as a Bearer token:
```http
Authorization: Bearer <access-token>
```
### Request body
```json
{
"currentPassword": "CurrentPassword@1234",
"newPassword": "NewPassword@5678",
"confirmPassword": "NewPassword@5678"
}
```
The new password:
- Must match `confirmPassword`
- Must differ from the current password
- Must contain at least one uppercase letter
- Must contain at least one lowercase letter
- Must contain at least one symbol
- Must contain at least four digits
### Success response — `200 OK`
```json
{
"success": true,
"message": "Password changed successfully. Please login again."
}
```
### Error responses
#### `400 Bad Request`
Returned when required fields are missing, the passwords do not match, the new password does not satisfy the password policy, or it matches the current password.
```json
{
"success": false,
"message": "New password and confirm password do not match"
}
```
#### `401 Unauthorized`
Returned when authentication fails or the current password is incorrect.
```json
{
"success": false,
"message": "Current password is incorrect"
}
```
#### `404 Not Found`
```json
{
"success": false,
"message": "User not found"
}
```
#### `500 Internal Server Error`
```json
{
"success": false,
"message": "Failed to change password"
}
```
---
## Logout
Deletes the current refresh session when available and clears both authentication cookies.
**Endpoint:** `POST` [http://localhost:3070/api/auth/logout](http://localhost:3070/api/auth/logout)
### Request body
No request body.
### Success response — `200 OK`
```json
{
@@ -123,3 +337,12 @@ No Body
"message": "Logged out successfully"
}
```
### Error response — `500 Internal Server Error`
```json
{
"success": false,
"message": "Failed to logout"
}
```
+49 -7
View File
@@ -18,7 +18,9 @@ emailVerifiedAt = null
POST: http://localhost:3070/api/user
```
**Request Body**
**Request Body customer**
accontType = customer and bussiness_customer
```json
{
@@ -26,6 +28,10 @@ POST: http://localhost:3070/api/user
"lastName": "Bimsara",
"email": "ibimsara00@gmail.com",
"password": "Hello@12346"
"accountType": "customer",
"address": "Colombo, Sri Lanka",
"phoneNumber": "0771234567"
}
```
@@ -45,21 +51,20 @@ POST: http://localhost:3070/api/user
}
```
After registration, the user receives an email containing a verification link.
```
#### Verify Email
Verifies the customer's email using the raw verification token received by email.
The backend hashes the received token and compares the resulting hash with the `tokenHash` stored in the `email_verifications` table.
The token must:
```text
Exist in the database
Not have been used
Not have expired
store in redis and expire token
```
**Endpoint**
@@ -107,7 +112,44 @@ usedAt = <current date and time>
This prevents the same verification token from being successfully used again.
---
#### Get user's details
**Endpoint**
```
GET: http://localhost:3070/api/profile
```
**Respond**
```json
{
"success": true,
"message": "User profile retrieved successfully",
"data": {
"user": {
"id": "usr_572gtlpi",
"firstName": "Isuru",
"lastName": "Bimsara",
"email": "ibimsara00@gmail.com",
"accountType": "customer",
"accountStatus": "ACTIVE",
"emailVerifiedAt": "2026-08-20T16:26:04.000Z",
"passwordChangedAt": "2026-08-21T05:29:16.000Z",
"createdAt": "2026-08-20T16:25:30.000Z",
"updatedAt": "2026-08-21T05:29:16.000Z"
},
"accountDetails": {
"customer_id": "cust_c8avqwbc",
"user_id": "usr_572gtlpi",
"address": "Colombo, Sri Lanka",
"phoneNumber": "0771234567",
"createdAt": "2026-08-20T16:25:31.000Z",
"updatedAt": "2026-08-20T16:25:31.000Z"
}
}
}
```
#### Get All Users
+412 -8
View File
@@ -9,15 +9,35 @@
// app/controllers/auth.controller.js
const { checkPassword } = require("../utils/hashPassword.util");
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 { 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 {
@@ -65,6 +85,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 +99,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 +120,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 +153,7 @@ exports.login = async (req, res) => {
lastName: user.lastName,
role: user.role,
accountType: user.accountType,
accessToken: token,
},
});
} catch (error) {
@@ -120,13 +162,375 @@ exports.login = async (req, res) => {
}
};
// Logout: Clear the JWT cookie
exports.logout = (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.",
});
}
};
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.json({ success: true, message: "Logged out successfully" });
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",
});
}
};
+167 -198
View File
@@ -9,19 +9,30 @@
// 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 { getUserProfile } = require("../utils/users/userProfileDetails.util");
const User = db.User;
const Profile = db.Profile;
// const EmailVerification = db.EmailVerification;
// Create a new user
exports.createNewUser = async (req, res) => {
@@ -33,28 +44,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) {
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",
"First name, last name, email, password and account type are required",
});
}
if (accountType !== "customer" && accountType !== "business_customer") {
await transaction.rollback();
return res.status(400).send({
success: false,
message:
"Invalid account type. Must be either 'customer' or 'business_customer'",
});
}
const emailValid = validateEmail(email);
if (!emailValid) {
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 +97,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,14 +118,13 @@ if (!emailValid) {
lastName,
email,
password: hashedPassword,
// accountType,
accountType,
accountStatus: "PENDING_VERIFICATION",
emailVerifiedAt: null,
},
{ transaction },
);
const newProfile = await Profile.create(
{
profile_id: generateId(),
@@ -117,78 +139,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
);
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:
await sendVerificationEmail(
newUser.email,
accountType:
newUser.accountType,
accountStatus:
newUser.accountStatus,
},
});
newUser.firstName,
verificationToken,
);
} 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,9 +198,8 @@ 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) {
@@ -212,10 +207,7 @@ if (!emailValid) {
await transaction.rollback();
}
console.error(
"CREATE USER ERROR:",
error
);
console.error("CREATE USER ERROR:", error);
res.status(500).send({
success: false,
@@ -226,122 +218,72 @@ if (!emailValid) {
};
// Verify customer email
exports.verifyEmail =
async (req, res) => {
const transaction =
await db.sequelize.transaction();
exports.verifyEmail = async (req, res) => {
const transaction = await db.sequelize.transaction();
try {
const {
token,
} = req.body;
const { token } = req.body;
if (!token) {
await transaction.rollback();
return res.status(400).send({
success:
false,
success: false,
message:
"Verification token is required",
message: "Verification token is required",
});
}
const verification =
await verifyEmailVerificationToken(
token
);
const verification = await verifyEmailVerificationToken(token);
if (!verification) {
await transaction.rollback();
return res.status(400).send({
success:
false,
success: false,
message:
"Verification token is invalid or expired",
message: "Verification token is invalid or expired",
});
}
const { userId, redisKey } = verification;
const {
userId,
redisKey,
} =
verification;
const user =
await User.findOne({
const user = await User.findOne({
where: {
id:
userId,
id: userId,
},
transaction,
});
if (!user) {
await transaction.rollback();
await deleteEmailVerification(
redisKey
);
await deleteEmailVerification(redisKey);
return res.status(404).send({
success:
false,
success: false,
message:
"User not found",
message: "User not found",
});
}
if (
user.accountStatus ===
"ACTIVE" &&
user.emailVerifiedAt
) {
if (user.accountStatus === "ACTIVE" && user.emailVerifiedAt) {
await transaction.rollback();
// Token is no longer needed
await deleteEmailVerification(
redisKey
);
await deleteEmailVerification(redisKey);
return res.status(400).send({
success:
false,
success: false,
message:
"Email is already verified",
message: "Email is already verified",
});
}
user.accountStatus = "ACTIVE";
user.accountStatus =
"ACTIVE";
user.emailVerifiedAt =
new Date();
user.emailVerifiedAt = new Date();
await user.save({
transaction,
@@ -349,45 +291,27 @@ exports.verifyEmail =
await transaction.commit();
await deleteEmailVerification(
redisKey
);
await deleteEmailVerification(redisKey);
return res.status(200).send({
success:
true,
success: true,
message:
"Email verified successfully. Your account is now active.",
message: "Email verified successfully. Your account is now active.",
});
} catch (error) {
if (!transaction.finished) {
await transaction.rollback();
}
console.error(
"VERIFY EMAIL ERROR:",
error
);
console.error("VERIFY EMAIL ERROR:", error);
return res.status(500).send({
success:
false,
success: false,
message:
"Failed to verify email",
message: "Failed to verify email",
});
}
};
};
// Get users with pagination (20 per page)
exports.getAllUsers = async (req, res) => {
@@ -422,36 +346,81 @@ exports.getAllUsers = async (req, res) => {
}
};
// Get user details by ID
exports.getUserById = async (req, res) => {
try {
const { id } = req.params;
const user = await User.findOne({
where: { id },
attributes: { exclude: ["password"] },
include: [{ model: Profile, as: "profile" }],
});
//get user profile details
exports.userProfile = async (req, res) => {
if (!user) {
try {
const userId = req.user.id;
const profile =
await getUserProfile(userId);
if (!profile) {
return res.status(404).send({
success: false,
message: "User not found",
});
}
res.status(200).send({
return res.status(200).send({
success: true,
data: user,
message:
"User profile retrieved successfully",
data: profile,
});
} catch (error) {
res.status(500).send({
console.error(
"GET USER PROFILE ERROR:",
error
);
return res.status(500).send({
success: false,
message: "Failed to retrieve user",
error: error.message,
message:
"Failed to retrieve user profile",
});
}
};
// Get user details by ID
// exports.getUserById = async (req, res) => {
// try {
// const { id } = req.params;
// const user = await User.findOne({
// where: { id },
// attributes: { exclude: ["password"] },
// include: [{ model: Profile, as: "profile" }],
// });
// if (!user) {
// return res.status(404).send({
// success: false,
// message: "User not found",
// });
// }
// res.status(200).send({
// success: true,
// data: user,
// });
// } catch (error) {
// res.status(500).send({
// success: false,
// message: "Failed to retrieve user",
// error: error.message,
// });
// }
// };
// Update user details
exports.updateUser = async (req, res) => {
const transaction = await db.sequelize.transaction();
+2
View File
@@ -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);
+65
View File
@@ -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;
};
+49
View File
@@ -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;
};
+24 -13
View File
@@ -16,35 +16,35 @@ module.exports = (sequelize, DataTypes) => {
id: {
type: DataTypes.STRING,
primaryKey: true,
collate: 'utf8mb4_general_ci'
collate: "utf8mb4_general_ci",
},
firstName: {
type: DataTypes.STRING,
allowNull: false
allowNull: false,
},
lastName: {
type: DataTypes.STRING,
allowNull: false
allowNull: false,
},
email: {
type: DataTypes.STRING,
allowNull: false,
unique: true
unique: true,
},
password: {
type: DataTypes.STRING,
allowNull: false
allowNull: false,
},
accountType: {
type: DataTypes.ENUM("admin", "superadmin", "manager", "business_customer", "rider", "customer","support_agent"),
defaultValue: "customer"
type: DataTypes.ENUM("business_customer", "customer"),
defaultValue: "customer",
},
accountStatus: {
type: DataTypes.ENUM(
"PENDING_VERIFICATION",
"ACTIVE",
"SUSPENDED",
"DEACTIVATED"
"DEACTIVATED",
),
allowNull: false,
defaultValue: "PENDING_VERIFICATION",
@@ -54,24 +54,35 @@ module.exports = (sequelize, DataTypes) => {
allowNull: true,
defaultValue: null,
},
passwordChangedAt: {
type: DataTypes.DATE,
allowNull: true,
defaultValue: null,
},
},
{
tableName: "users",
timestamps: true
}
timestamps: true,
},
);
User.associate = (db) => {
User.hasMany(db.userPermission, {
foreignKey: "user_id",
as: "userPermissions"
as: "userPermissions",
});
User.hasOne(db.Profile, {
foreignKey: "user_id",
as: "profile",
});
User.hasOne(db.Customer, {
foreignKey: "user_id",
as: "customer",
});
User.hasOne(db.BusinessCustomer, {
foreignKey: "user_id",
as: "businessCustomer",
});
};
return User;
+4
View File
@@ -24,6 +24,10 @@ router.get("/me", authenticate, (req, res) => {
router.post('/req-otp', authController.loginReq);
router.post('/login', authController.login);
router.post('/refresh', authController.refreshToken);
router.post('/forgot-password', authController.forgotPassword);
router.post('/reset-password', authController.resetPassword);
router.post('/change-password', authenticate, authController.changePassword);
router.post('/logout', authController.logout);
module.exports = router;
+12 -6
View File
@@ -32,6 +32,12 @@ router.post(
userController.verifyEmail
);
router.get(
"/profile",
authenticate,
userController.userProfile
);
router.get(
"/",
authenticate,
@@ -39,12 +45,12 @@ router.get(
userController.getAllUsers
);
router.get(
"/:id",
authenticate,
authorizedAccountType(["admin", "management", "team_head", "user"]),
userController.getUserById
);
// router.get(
// "/:id",
// authenticate,
// authorizedAccountType(["admin", "management", "team_head", "user"]),
// userController.getUserById
// );
router.patch(
"/:id",
+6 -6
View File
@@ -2,14 +2,14 @@
<html>
<head>
<meta charset="UTF-8">
<title>GLAURA 2FA Code</title>
<title>ZUMRI CEYLON</title>
</head>
<body style="font-family: Arial, Helvetica, sans-serif; font-size: 14px; color: #000; line-height: 1.6; margin: 0; padding: 20px;">
<p>Dear {{firstName}},</p>
<p>Greetings from <b>Oceanic Titan</b>!</p>
<p>Greetings from <b>ZUMRI CEYLON</b>!</p>
<p>Your One Time Password (OTP) to log in to your Oceanic Titan account is mentioned below.</p>
<p>Your One Time Password (OTP) to log in to your ZUMRI CEYLON account is mentioned below.</p>
<p>Please enter this OTP in the required field to proceed further:</p>
@@ -24,14 +24,14 @@
</tr>
</table>
<p>The above-mentioned OTP is valid for <b>5 minutes</b>.</p>
<p>The above-mentioned OTP is valid for <b>15 minutes</b>.</p>
<br>
<p>Regards,<br>
<b>Oceanic Titan Team</b>
<b>ZUMRI CEYLON Team</b>
</p>
<p style="font-size: 12px; color: #555;">{{currentYear}} Oceanic Titan. This is an auto-generated email, please do not reply to this email.</p>
<p style="font-size: 12px; color: #555;">{{currentYear}} ZUMRI CEYLON. This is an auto-generated email, please do not reply to this email.</p>
</body>
</html>
+32
View File
@@ -0,0 +1,32 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Your ZUMRI password was changed</title>
</head>
<body style="font-family: Arial, Helvetica, sans-serif; font-size: 14px; color: #000; line-height: 1.6; margin: 0; padding: 20px;">
<p>Hi {{customer_name}},</p>
<p>
Your password was changed at {{changed_at}}.
</p>
<p>
If this was not you, contact support immediately.
</p>
<br>
<p>
Thank you,<br>
<b>ZUMRI Team</b>
</p>
<p style="font-size: 12px; color: #555;">
Document Classification: Internal Use Only, Version: 1.0
</p>
</body>
</html>
+227 -38
View File
@@ -2,52 +2,241 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
body {
font-family: Arial, Helvetica, sans-serif;
font-size: 14px;
color: #000;
line-height: 1.6;
margin: 0;
padding: 20px;
}
table {
border-collapse: collapse;
width: 400px;
border: 1px solid #000;
}
th {
background-color: #053534;
color: #ffffff;
text-align: left;
}
td {
border: 1px solid #000;
}
a {
color: #1a73e8;
}
</style>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
>
<title>Reset Your ZUMRI Password</title>
</head>
<body>
<p>Dear {{firstName}},</p>
<p>Greetings from <b>Oceanic Titan</b>!</p>
<body
style="
margin: 0;
padding: 0;
background-color: #f4f4f4;
font-family: Arial, Helvetica, sans-serif;
"
>
<p>You have requested to reset your password for your Oceanic Titan account. Please click the link below to reset your password:</p>
<table
width="100%"
cellpadding="0"
cellspacing="0"
role="presentation"
style="
background-color: #f4f4f4;
padding: 40px 0;
"
>
<tr>
<td align="center">
<p><a href="{{resetLink}}" target="_blank">Reset Password</a></p>
<table
width="600"
cellpadding="0"
cellspacing="0"
role="presentation"
style="
max-width: 600px;
width: 100%;
background-color: #ffffff;
padding: 40px;
border-radius: 8px;
"
>
<p>If you did not request a password reset, please ignore this email. The above link will expire in <b>{{expiryTime}}</b>.</p>
<!-- Greeting -->
<tr>
<td>
<p
style="
font-size: 18px;
color: #333333;
margin-bottom: 30px;
"
>
Hi {{firstName}},
</p>
</td>
</tr>
<br>
<p>Regards,<br>
<b>Oceanic Titan Team</b>
<!-- Title -->
<tr>
<td>
<h2
style="
color: #222222;
margin-bottom: 25px;
"
>
Reset Your ZUMRI Password
</h2>
</td>
</tr>
<!-- Description -->
<tr>
<td>
<p
style="
font-size: 16px;
line-height: 1.6;
color: #555555;
"
>
We received a request to reset the password
for your ZUMRI account.
Click the button below to create a new password.
</p>
</td>
</tr>
<!-- Reset Button -->
<tr>
<td
align="center"
style="padding: 30px 0;"
>
<a
href="{{resetLink}}"
target="_blank"
style="
display: inline-block;
padding: 14px 28px;
background-color: #222222;
color: #ffffff;
text-decoration: none;
font-size: 16px;
font-weight: bold;
border-radius: 6px;
"
>
Reset Password
</a>
</td>
</tr>
<!-- Direct Link -->
<tr>
<td>
<p
style="
font-size: 14px;
line-height: 1.6;
color: #777777;
"
>
If the button doesn't work, copy and paste
the following link into your browser:
</p>
<p style="font-size: 12px; color: #555;">{{currentYear}} Oceanic Titan. This is an auto-generated email, please do not reply to this email.</p>
<p
style="
font-size: 13px;
line-height: 1.5;
word-break: break-all;
"
>
<a
href="{{resetLink}}"
target="_blank"
style="color: #0066cc;"
>
{{resetLink}}
</a>
</p>
</td>
</tr>
<!-- Expiry Information -->
<tr>
<td>
<p
style="
font-size: 14px;
line-height: 1.6;
color: #777777;
margin-top: 30px;
"
>
For security purposes, this password reset
link will expire in
<strong>{{expiryTime}}</strong>.
</p>
</td>
</tr>
<!-- Security Message -->
<tr>
<td>
<p
style="
font-size: 14px;
line-height: 1.6;
color: #777777;
"
>
If you did not request a password reset,
you can safely ignore this email.
Your password will remain unchanged.
</p>
</td>
</tr>
<!-- Footer -->
<tr>
<td>
<p
style="
font-size: 16px;
color: #333333;
margin-top: 30px;
"
>
Best regards,<br>
<strong>ZUMRI Team</strong>
</p>
</td>
</tr>
<!-- Copyright -->
<tr>
<td
align="center"
style="
border-top: 1px solid #eeeeee;
padding-top: 20px;
"
>
<p
style="
font-size: 12px;
color: #999999;
"
>
&copy; {{currentYear}} ZUMRI.
All rights reserved.
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
+37 -49
View File
@@ -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) => {
if (
!token ||
typeof token !== "string"
) {
const verifyEmailVerificationToken = async (token) => {
if (!token || typeof token !== "string") {
return null;
}
// 1. Hash token received from user
const tokenHash =
hashEmailVerificationToken(token);
const tokenHash = hashEmailVerificationToken(token);
// 2. Build same Redis key
const redisKey =
`email-verification:${tokenHash}`;
const redisKey = `email-verification:${tokenHash}`;
// 3. Search Redis
const userId =
await redis.get(redisKey);
const userId = await redis.get(redisKey);
if (!userId) {
return null;
}
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}`;
// 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,
};
+10
View File
@@ -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,
+13 -2
View File
@@ -13,7 +13,10 @@ const jwt = require("jsonwebtoken");
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 JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || "15m"; // 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
@@ -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 };
+3 -2
View File
@@ -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 });
}
+99 -76
View File
@@ -10,104 +10,127 @@
// app/utils/passwordReset.util.js
const crypto = require("crypto");
const redis = require("../config/redisClient");
const { sendMail } = require("./mail.util");
const createRedisConnection = require("../config/redis.config");
const redis = createRedisConnection();
const PASSWORD_RESET_TTL =
Number(process.env.PASSWORD_RESET_TTL_SECONDS) || 900;
const db = require("../models");
const generatePasswordResetToken = () => {
return crypto.randomBytes(32).toString("hex");
};
const User = db.User;
const hashPasswordResetToken = (token) => {
return crypto.createHash("sha256").update(token).digest("hex");
};
const { log } = require("./consoleLog.utill");
const { hashPassword } = require("./hashPassword.util");
const createPasswordReset = async (userId) => {
// 1. Generate RAW token
const token = generatePasswordResetToken();
const RESET_TOKEN_TTL = process.env.RESET_TOKEN_TTL || 10 * 60; // 10 minutes
// 2. Hash RAW token
const tokenHash = hashPasswordResetToken(token);
/**
* Generate password reset token
*
* @param {string} userId
* @returns {Promise<string>}
*/
async function generateResetToken(userId) {
try {
const token = crypto.randomBytes(32).toString("hex");
// 3. Create Redis key
const redisKey = `password-reset:${tokenHash}`;
const tokenHash = crypto
.createHash("sha256")
.update(token)
.digest("hex");
// 4. Save user ID with 15 minute TTL
await redis.set(redisKey, userId, "EX", PASSWORD_RESET_TTL);
await redis.set(
`passwordReset:user:${userId}`,
tokenHash,
"EX",
RESET_TOKEN_TTL
);
log(`Generated password reset token for user ${userId} with TTL of ${RESET_TOKEN_TTL} seconds, Generated token:`, token);
// Send only RAW token to user
return token;
} catch (error) {
log("Password reset token generation failed", error);
throw new Error("Failed to generate password reset token");
}
}
};
/**
* Reset password using token
*
* @param {string} userId
* @param {string} token
* @param {string} newPassword
*/
async function resetPassword(userId, token, newPassword) {
try {
const storedHash = await redis.get(
`passwordReset:user:${userId}`
);
if (!storedHash) {
throw new Error("Invalid or expired reset token");
const verifyPasswordResetToken = async (token) => {
if (!token || typeof token !== "string") {
return null;
}
const tokenHash = crypto
.createHash("sha256")
.update(token)
.digest("hex");
// Hash token received from user
const tokenHash = hashPasswordResetToken(token);
const isValid =
crypto.timingSafeEqual(
Buffer.from(storedHash),
Buffer.from(tokenHash)
);
// Create same Redis key
const redisKey = `password-reset:${tokenHash}`;
if (!isValid) {
throw new Error("Invalid or expired reset token");
// Search Redis
const userId = await redis.get(redisKey);
if (!userId) {
return null;
}
const user = await User.findByPk(userId);
return {
userId,
redisKey,
};
};
if (!user) {
throw new Error("User not found");
}
const deletePasswordReset = async (redisKey) => {
await redis.del(redisKey);
};
user.password = await hashPassword(newPassword);
const sendPasswordResetEmail =
async (email, firstName, resetToken) => {
await user.save();
const resetLink =
`${process.env.FRONTEND_URL}/reset-password?token=${resetToken}`;
await redis.del(`passwordReset:user:${userId}`);
log(
`Password reset completed successfully for user ${userId}`
);
await sendMail({
to: email,
return true;
} catch (error) {
log("Password reset failed", error);
throw error;
}
}
subject:
"Reset your ZUMRI password",
templateName:
"passwordReset",
templateVars: {
firstName: firstName,
resetLink: resetLink,
expiryTime: "15 minutes",
},
text:
`Hi ${firstName}, use this link within 15 minutes to reset your password: ${resetLink}`,
});
};
const sendPasswordChangedEmail = async (
email,
firstName
) => {
const changedAt =
new Date().toLocaleString();
await sendMail({
to: email,
subject:
"Your ZUMRI password was changed",
templateName:
"passwordChanged",
templateVars: {
customer_name:
firstName,
changed_at:
changedAt,
},
text:
`Hi ${firstName}, your password was changed at ${changedAt}. If this was not you, contact support immediately.`,
});
};
module.exports = {
generateResetToken,
resetPassword
createPasswordReset,
verifyPasswordResetToken,
deletePasswordReset,
hashPasswordResetToken,
sendPasswordResetEmail,
sendPasswordChangedEmail,
};
+201
View File
@@ -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,
};
@@ -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,
};
@@ -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,
};
@@ -0,0 +1,56 @@
// app/services/userProfile.service.js
const db = require("../../models");
const User = db.User;
const Customer = db.Customer;
const BusinessCustomer = db.BusinessCustomer;
const getUserProfile = async (userId) => {
const user = await User.findByPk(userId, {
attributes: {
exclude: ["password"],
},
});
if (!user) {
return null;
}
let accountDetails = null;
if (user.accountType === "customer") {
accountDetails = await Customer.findOne({
where: {
user_id: user.id,
},
});
}
else if (user.accountType === "business_customer") {
accountDetails =
await BusinessCustomer.findOne({
where: {
user_id: user.id,
},
});
}
return {
user,
accountDetails,
};
};
module.exports = {
getUserProfile,
};