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
``` ```text
POST: http://localhost:3070/api/auth/req-otp 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 ```json
{ {
@@ -17,7 +42,7 @@ POST: http://localhost:3070/api/auth/req-otp
} }
``` ```
**Respond** ### Success response — `201 Created`
```json ```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.
``` **Endpoint:** `POST` [http://localhost:3070/api/auth/login](http://localhost:3070/api/auth/login)
POST: http://localhost:3070/api/auth/login
```
**Request Body** ### Request body
```json ```json
{ {
@@ -45,7 +74,7 @@ POST: http://localhost:3070/api/auth/login
} }
``` ```
**Respond** ### Success response — `200 OK`
```json ```json
{ {
@@ -54,33 +83,40 @@ POST: http://localhost:3070/api/auth/login
"data": { "data": {
"id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4", "id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
"email": "sathira@niolla.lk", "email": "sathira@niolla.lk",
"firstName": "Jhon", "firstName": "Sathira",
"lastName": "Doe", "lastName": "Sri Sathsara",
"role": "System Developer", "role": null,
"accountType": "admin" "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.
``` **Endpoint:** `GET` [http://localhost:3070/api/auth/me](http://localhost:3070/api/auth/me)
GET: http://localhost:3070/api/auth/me
```
**Authorization**: Required (Bearer token) **Authentication:** Required
**Request Body** ### Request body
``` No request body.
No Body
```
**Response (200)** ### Success response — `200 OK`
```json ```json
{ {
@@ -90,7 +126,6 @@ No Body
"firstName": "Sathira", "firstName": "Sathira",
"lastName": "Sri Sathsara", "lastName": "Sri Sathsara",
"email": "sathira@niolla.lk", "email": "sathira@niolla.lk",
"role": "System Developer",
"accountType": "admin", "accountType": "admin",
"iat": 1778865265, "iat": 1778865265,
"exp": 1778868865, "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.
``` **Endpoint:** `POST` [http://localhost:3070/api/auth/refresh](http://localhost:3070/api/auth/refresh)
POST: http://localhost:3070/api/auth/logout
### 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`
``` Returned when the refresh cookie is missing or the session is invalid, expired, or no longer active.
No Body
---
## 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 ```json
{ {
@@ -123,3 +337,12 @@ No Body
"message": "Logged out successfully" "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 POST: http://localhost:3070/api/user
``` ```
**Request Body** **Request Body customer**
accontType = customer and bussiness_customer
```json ```json
{ {
@@ -26,6 +28,10 @@ POST: http://localhost:3070/api/user
"lastName": "Bimsara", "lastName": "Bimsara",
"email": "ibimsara00@gmail.com", "email": "ibimsara00@gmail.com",
"password": "Hello@12346" "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. After registration, the user receives an email containing a verification link.
```
#### Verify Email #### Verify Email
Verifies the customer's email using the raw verification token received by 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: The token must:
```text ```text
Exist in the database store in redis and expire token
Not have been used
Not have expired
``` ```
**Endpoint** **Endpoint**
@@ -107,7 +112,44 @@ usedAt = <current date and time>
This prevents the same verification token from being successfully used again. 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 #### Get All Users
+417 -13
View File
@@ -9,15 +9,35 @@
// app/controllers/auth.controller.js // 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 { 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 { 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 { 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 { log } = require("../utils/consoleLog.utill");
const appName = process.env.APP_NAME || "Niolla"; const appName = process.env.APP_NAME || "Niolla";
const User = db.User;
// Login Step 1: Request OTP // Login Step 1: Request OTP
exports.loginReq = async (req, res) => { exports.loginReq = async (req, res) => {
try { try {
@@ -65,6 +85,12 @@ exports.login = async (req, res) => {
try { try {
const { email, otp } = req.body; 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); const user = await getCachedUser(email);
if (!user) { if (!user) {
@@ -73,7 +99,14 @@ exports.login = async (req, res) => {
.send({ success: false, message: "User Not Found" }); .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) { if (!isValidOTP) {
return res return res
@@ -87,16 +120,24 @@ exports.login = async (req, res) => {
firstName: user.firstName, firstName: user.firstName,
lastName: user.lastName, lastName: user.lastName,
email: user.email, email: user.email,
role: user.role,
accountType: user.accountType, accountType: user.accountType,
}); });
const { refreshToken } = createRefreshSession(user.id);
// 3. Set JWT as HttpOnly cookie // 3. Set JWT as HttpOnly cookie
res.cookie("access_token", token, { res.cookie("access_token", token, {
httpOnly: true, // JS cannot access httpOnly: true, // JS cannot access
secure: process.env.NODE_ENV === "production", // HTTPS only in prod secure: process.env.NODE_ENV === "production", // HTTPS only in prod
sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax", 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}`); log(`JWT issued for ${email}`);
@@ -112,6 +153,7 @@ exports.login = async (req, res) => {
lastName: user.lastName, lastName: user.lastName,
role: user.role, role: user.role,
accountType: user.accountType, accountType: user.accountType,
accessToken: token,
}, },
}); });
} catch (error) { } catch (error) {
@@ -120,13 +162,375 @@ exports.login = async (req, res) => {
} }
}; };
// Logout: Clear the JWT cookie exports.refreshToken = async (req, res) => {
exports.logout = (req, res) => { try {
res.clearCookie("access_token", { const refreshToken = req.cookies?.refresh_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" }); if (!refreshToken) {
return res.status(401).send({
success: false,
message: "Refresh token is required",
});
}
const session = validateRefreshSession(refreshToken);
if (!session) {
res.clearCookie("refresh_token");
return res.status(401).send({
success: false,
message: "Invalid or expired session. Please login again.",
});
}
const user = await User.findByPk(session.userId);
if (!user || user.accountStatus !== "ACTIVE") {
deleteRefreshSession(session.sessionId);
return res.status(401).send({
success: false,
message: "Session is no longer valid",
});
}
// Generate new Access Token
const token = generateToken({
id: user.id,
firstName: user.firstName,
lastName: user.lastName,
email: user.email,
accountType: user.accountType,
});
// Generate new Refresh Token
const { refreshToken: newRefreshToken } = createRefreshSession(user.id);
deleteRefreshSession(session.sessionId);
// Replace access cookie
res.cookie("access_token", token, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax",
maxAge: 15 * 60 * 1000,
});
// Replace refresh cookie
res.cookie("refresh_token", newRefreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax",
maxAge: 7 * 24 * 60 * 60 * 1000,
});
return res.status(200).send({
success: true,
message: "Session refreshed successfully",
});
} catch (error) {
console.error("REFRESH ERROR:", error);
return res.status(401).send({
success: false,
message: "Invalid or expired session. Please login again.",
});
}
};
exports.forgotPassword = async (req, res) => {
try {
let { email } = req.body;
if (!email) {
return res.status(400).send({
success: false,
message: "Email is required",
});
}
email = email.trim().toLowerCase();
if (!validateEmail(email)) {
return res.status(400).send({
success: false,
message: "Invalid email address",
});
}
const user = await User.findOne({
where: { email },
});
if (!user) {
return res.status(200).send({
success: true,
message:
"If an account exists for this email, a password reset link has been sent.",
});
}
const resetToken = await createPasswordReset(user.id);
await sendPasswordResetEmail(user.email, user.firstName, resetToken);
return res.status(200).send({
success: true,
message:
"If an account exists for this email, a password reset link has been sent.",
});
} catch (error) {
console.error("FORGOT PASSWORD ERROR:", error);
return res.status(500).send({
success: false,
message: "Unable to process password reset request",
});
}
};
exports.resetPassword = async (req, res) => {
try {
const { token, newPassword, confirmPassword } = req.body;
if (!token || !newPassword || !confirmPassword) {
return res.status(400).send({
success: false,
message: "Token, new password and confirm password are required",
});
}
if (newPassword !== confirmPassword) {
return res.status(400).send({
success: false,
message: "Passwords do not match",
});
}
if (!validatePassword(newPassword)) {
return res.status(400).send({
success: false,
message: "Password does not meet the required criteria",
});
}
const verification = await verifyPasswordResetToken(token);
if (!verification) {
return res.status(400).send({
success: false,
message: "Reset token is invalid or expired",
});
}
const { userId, redisKey } = verification;
const user = await User.findByPk(userId);
if (!user) {
await deletePasswordReset(redisKey);
return res.status(400).send({
success: false,
message: "Reset token is invalid or expired",
});
}
const samePassword = await checkPassword(newPassword, user.password);
if (samePassword) {
return res.status(400).send({
success: false,
message: "New password must be different from the current password",
});
}
const hashedPassword = await hashPassword(newPassword);
user.password = hashedPassword;
user.passwordChangedAt = new Date();
await user.save();
await sendPasswordChangedEmail(user.email, user.firstName);
await deletePasswordReset(redisKey);
deleteAllUserSessions(user.id);
return res.status(200).send({
success: true,
message: "Password reset successfully. Please login again.",
});
} catch (error) {
console.error("RESET PASSWORD ERROR:", error);
return res.status(500).send({
success: false,
message: "Failed to reset password",
});
}
};
exports.changePassword = async (req, res) => {
try {
// User ID comes from authenticate middleware
const userId = req.user.id;
const { currentPassword, newPassword, confirmPassword } = req.body;
// 1. Check required fields
if (!currentPassword || !newPassword || !confirmPassword) {
return res.status(400).send({
success: false,
message:
"Current password, new password and confirm password are required",
});
}
// 2. Check new password and confirmation
if (newPassword !== confirmPassword) {
return res.status(400).send({
success: false,
message: "New password and confirm password do not match",
});
}
// 3. Validate password policy
const passwordValid = validatePassword(newPassword);
if (!passwordValid) {
return res.status(400).send({
success: false,
message: "New password does not meet the required criteria",
});
}
// 4. Get logged-in user from database
const user = await User.findByPk(userId);
if (!user) {
return res.status(404).send({
success: false,
message: "User not found",
});
}
// 5. Check current password
const currentPasswordValid = await checkPassword(
currentPassword,
user.password,
);
if (!currentPasswordValid) {
return res.status(401).send({
success: false,
message: "Current password is incorrect",
});
}
// 6. Make sure new password is different
const sameAsOldPassword = await checkPassword(newPassword, user.password);
if (sameAsOldPassword) {
return res.status(400).send({
success: false,
message: "New password must be different from current password",
});
}
// 7. Hash new password
const hashedPassword = await hashPassword(newPassword);
// 8. Update user
user.password = hashedPassword;
user.passwordChangedAt = new Date();
await user.save();
// 9. Revoke all refresh sessions
deleteAllUserSessions(user.id);
// 10. Clear auth cookies
res.clearCookie("access_token", {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax",
});
res.clearCookie("refresh_token", {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax",
});
// 11. Send confirmation email
try {
await sendPasswordChangedEmail(user.email, user.firstName);
} catch (mailError) {
console.error("PASSWORD CHANGED EMAIL ERROR:", mailError);
}
// 12. Response
return res.status(200).send({
success: true,
message: "Password changed successfully. Please login again.",
});
} catch (error) {
console.error("CHANGE PASSWORD ERROR:", error);
return res.status(500).send({
success: false,
message: "Failed to change password",
});
}
};
// Logout: Clear the JWT cookie
exports.logout = async (req, res) => {
try {
// 1. Get refresh token from cookie
const refreshToken = req.cookies?.refresh_token;
// 2. If refresh token exists, find its session
if (refreshToken) {
const session = validateRefreshSession(refreshToken);
// 3. Delete refresh session from server RAM
if (session) {
deleteRefreshSession(session.sessionId);
console.log(`Refresh session deleted: ${session.sessionId}`);
}
}
// 4. Clear access token cookie
res.clearCookie("access_token", {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax",
});
// 5. Clear refresh token cookie
res.clearCookie("refresh_token", {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax",
});
// 6. Send response
return res.status(200).json({
success: true,
message: "Logged out successfully",
});
} catch (error) {
console.error("LOGOUT ERROR:", error);
return res.status(500).json({
success: false,
message: "Failed to logout",
});
}
}; };
+244 -275
View File
@@ -9,19 +9,30 @@
// app/controllers/user.controller.js // app/controllers/user.controller.js
// const { Op } = require("sequelize");
const db = require("../models"); const db = require("../models");
const { hashPassword } = require("../utils/hashPassword.util"); 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 { validateEmail } = require("../utils/validation/validateEmail.util");
const { generateUserId, generateId } = require("../utils/idGen.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 { logActivity } = require("../services/activity.service");
const { sendMail } = require("../utils/mail.util"); const { sendMail } = require("../utils/mail.util");
const { 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 User = db.User;
const Profile = db.Profile; const Profile = db.Profile;
// const EmailVerification = db.EmailVerification;
// Create a new user // Create a new user
exports.createNewUser = async (req, res) => { exports.createNewUser = async (req, res) => {
@@ -33,28 +44,48 @@ exports.createNewUser = async (req, res) => {
lastName, lastName,
email, email,
password, password,
accountType,
address,
phoneNumber,
businessName,
businessRegistrationNumber,
businessType,
contactName,
businessEmail,
expectedMonthlyVolume,
note,
} = req.body; } = req.body;
if (!firstName || !lastName || !email || !password) { if (!firstName || !lastName || !email || !password || !accountType) {
await transaction.rollback(); await transaction.rollback();
return res.status(400).send({ return res.status(400).send({
success: false, success: false,
message: message:
"First name, last name, email and password are required", "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) { return res.status(400).send({
await transaction.rollback(); success: false,
message:
"Invalid account type. Must be either 'customer' or 'business_customer'",
});
}
return res.status(400).send({ const emailValid = validateEmail(email);
success: false,
message: "Invalid email address", if (!emailValid) {
}); await transaction.rollback();
}
return res.status(400).send({
success: false,
message: "Invalid email address",
});
}
const userExists = await User.findOne({ where: { email } }); const userExists = await User.findOne({ where: { email } });
if (userExists) { 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); const validatePasswordResult = validatePassword(password);
if (!validatePasswordResult) { if (!validatePasswordResult) {
@@ -95,14 +118,13 @@ if (!emailValid) {
lastName, lastName,
email, email,
password: hashedPassword, password: hashedPassword,
// accountType, accountType,
accountStatus: "PENDING_VERIFICATION", accountStatus: "PENDING_VERIFICATION",
emailVerifiedAt: null, emailVerifiedAt: null,
}, },
{ transaction }, { transaction },
); );
const newProfile = await Profile.create( const newProfile = await Profile.create(
{ {
profile_id: generateId(), profile_id: generateId(),
@@ -117,78 +139,52 @@ if (!emailValid) {
{ transaction }, { 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(); await transaction.commit();
const verificationToken = await createEmailVerification(newUser.id); const verificationToken = await createEmailVerification(newUser.id);
const confirmationLink =
`${process.env.FRONTEND_URL}/verify-email?token=${verificationToken}`;
try { try {
await sendVerificationEmail(
await sendMail({ newUser.email,
to: newUser.firstName,
email, verificationToken,
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
); );
} catch (error) {
console.error("Error sending verification email:", error);
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,
},
});
} }
await logActivity({ await logActivity({
user: req.user, user: newUser,
description: `Created New User with ID: ${newUser.id}`, description: `Created New User with ID: ${newUser.id}`,
type: "CREATE_USER", type: "CREATE_USER",
module: "User Management", module: "User Management",
@@ -202,20 +198,16 @@ if (!emailValid) {
firstName: newUser.firstName, firstName: newUser.firstName,
lastName: newUser.lastName, lastName: newUser.lastName,
email: newUser.email, email: newUser.email,
// accountType: newUser.accountType, accountType: newUser.accountType,
// role: newUser.role,
// department: newUser.department,
}, },
}); });
} catch (error) { } catch (error) {
if (!transaction.finished) { if (!transaction.finished) {
await transaction.rollback(); await transaction.rollback();
} }
console.error( console.error("CREATE USER ERROR:", error);
"CREATE USER ERROR:",
error
);
res.status(500).send({ res.status(500).send({
success: false, success: false,
@@ -226,168 +218,100 @@ if (!emailValid) {
}; };
// Verify customer email // Verify customer email
exports.verifyEmail = exports.verifyEmail = async (req, res) => {
async (req, res) => { const transaction = await db.sequelize.transaction();
const transaction = try {
await db.sequelize.transaction(); const { token } = req.body;
if (!token) {
await transaction.rollback();
try { return res.status(400).send({
const { success: false,
token,
} = req.body;
message: "Verification token is required",
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",
}); });
} }
};
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) // Get users with pagination (20 per page)
exports.getAllUsers = async (req, res) => { exports.getAllUsers = async (req, res) => {
@@ -422,36 +346,81 @@ exports.getAllUsers = async (req, res) => {
} }
}; };
// Get user details by ID //get user profile details
exports.getUserById = async (req, res) => { exports.userProfile = 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) { try {
const userId = req.user.id;
const profile =
await getUserProfile(userId);
if (!profile) {
return res.status(404).send({ return res.status(404).send({
success: false, success: false,
message: "User not found", message: "User not found",
}); });
} }
res.status(200).send({
return res.status(200).send({
success: true, success: true,
data: user, message:
"User profile retrieved successfully",
data: profile,
}); });
} catch (error) { } catch (error) {
res.status(500).send({
console.error(
"GET USER PROFILE ERROR:",
error
);
return res.status(500).send({
success: false, success: false,
message: "Failed to retrieve user", message:
error: error.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 // Update user details
exports.updateUser = async (req, res) => { exports.updateUser = async (req, res) => {
const transaction = await db.sequelize.transaction(); const transaction = await db.sequelize.transaction();
+2
View File
@@ -41,6 +41,8 @@ db.sequelize = sequelize;
// User and Authentication // User and Authentication
db.User = require("./user/user.model")(sequelize, DataTypes); 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.UserActivity = require("./activities/userActivities.model")(sequelize, DataTypes);
db.Profile = require("./user/profile.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;
};
+25 -14
View File
@@ -16,62 +16,73 @@ module.exports = (sequelize, DataTypes) => {
id: { id: {
type: DataTypes.STRING, type: DataTypes.STRING,
primaryKey: true, primaryKey: true,
collate: 'utf8mb4_general_ci' collate: "utf8mb4_general_ci",
}, },
firstName: { firstName: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false allowNull: false,
}, },
lastName: { lastName: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false allowNull: false,
}, },
email: { email: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: false,
unique: true unique: true,
}, },
password: { password: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false allowNull: false,
}, },
accountType: { accountType: {
type: DataTypes.ENUM("admin", "superadmin", "manager", "business_customer", "rider", "customer","support_agent"), type: DataTypes.ENUM("business_customer", "customer"),
defaultValue: "customer" defaultValue: "customer",
}, },
accountStatus: { accountStatus: {
type: DataTypes.ENUM( type: DataTypes.ENUM(
"PENDING_VERIFICATION", "PENDING_VERIFICATION",
"ACTIVE", "ACTIVE",
"SUSPENDED", "SUSPENDED",
"DEACTIVATED" "DEACTIVATED",
), ),
allowNull: false, allowNull: false,
defaultValue: "PENDING_VERIFICATION", defaultValue: "PENDING_VERIFICATION",
}, },
emailVerifiedAt: { emailVerifiedAt: {
type: DataTypes.DATE,
allowNull: true,
defaultValue: null,
},
passwordChangedAt: {
type: DataTypes.DATE, type: DataTypes.DATE,
allowNull: true, allowNull: true,
defaultValue: null, defaultValue: null,
}, },
}, },
{ {
tableName: "users", tableName: "users",
timestamps: true timestamps: true,
} },
); );
User.associate = (db) => { User.associate = (db) => {
User.hasMany(db.userPermission, { User.hasMany(db.userPermission, {
foreignKey: "user_id", foreignKey: "user_id",
as: "userPermissions" as: "userPermissions",
}); });
User.hasOne(db.Profile, { User.hasOne(db.Profile, {
foreignKey: "user_id", foreignKey: "user_id",
as: "profile", as: "profile",
}); });
User.hasOne(db.Customer, {
foreignKey: "user_id",
as: "customer",
});
User.hasOne(db.BusinessCustomer, {
foreignKey: "user_id",
as: "businessCustomer",
});
}; };
return User; return User;
+4
View File
@@ -24,6 +24,10 @@ router.get("/me", authenticate, (req, res) => {
router.post('/req-otp', authController.loginReq); router.post('/req-otp', authController.loginReq);
router.post('/login', authController.login); 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); router.post('/logout', authController.logout);
module.exports = router; module.exports = router;
+12 -6
View File
@@ -32,6 +32,12 @@ router.post(
userController.verifyEmail userController.verifyEmail
); );
router.get(
"/profile",
authenticate,
userController.userProfile
);
router.get( router.get(
"/", "/",
authenticate, authenticate,
@@ -39,12 +45,12 @@ router.get(
userController.getAllUsers userController.getAllUsers
); );
router.get( // router.get(
"/:id", // "/:id",
authenticate, // authenticate,
authorizedAccountType(["admin", "management", "team_head", "user"]), // authorizedAccountType(["admin", "management", "team_head", "user"]),
userController.getUserById // userController.getUserById
); // );
router.patch( router.patch(
"/:id", "/:id",
+6 -6
View File
@@ -2,14 +2,14 @@
<html> <html>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>GLAURA 2FA Code</title> <title>ZUMRI CEYLON</title>
</head> </head>
<body style="font-family: Arial, Helvetica, sans-serif; font-size: 14px; color: #000; line-height: 1.6; margin: 0; padding: 20px;"> <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>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> <p>Please enter this OTP in the required field to proceed further:</p>
@@ -24,14 +24,14 @@
</tr> </tr>
</table> </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> <br>
<p>Regards,<br> <p>Regards,<br>
<b>Oceanic Titan Team</b> <b>ZUMRI CEYLON Team</b>
</p> </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> </body>
</html> </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>
+229 -40
View File
@@ -1,53 +1,242 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title> <meta
<style> name="viewport"
body { content="width=device-width, initial-scale=1.0"
font-family: Arial, Helvetica, sans-serif; >
font-size: 14px;
color: #000; <title>Reset Your ZUMRI Password</title>
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>
</head> </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> <!-- Title -->
<b>Oceanic Titan Team</b> <tr>
</p> <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: 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>
<p style="font-size: 12px; color: #555;">{{currentYear}} Oceanic Titan. This is an auto-generated email, please do not reply to this email.</p>
</body> </body>
</html> </html>
+48 -60
View File
@@ -1,103 +1,91 @@
// app/utils/emailVerification.util.js // app/utils/emailVerification.util.js
const crypto = require("crypto"); const crypto = require("crypto");
const { sendMail } = require("../utils/mail.util");
const redis = require("../config/redisClient"); const redis = require("../config/redisClient");
const EMAIL_VERIFICATION_TTL = const EMAIL_VERIFICATION_TTL =
Number( Number(process.env.EMAIL_VERIFICATION_TTL_SECONDS) || 1800;
process.env.EMAIL_VERIFICATION_TTL_SECONDS
) || 1800;
const generateEmailVerificationToken = () => { const generateEmailVerificationToken = () => {
return crypto return crypto.randomBytes(32).toString("hex");
.randomBytes(32)
.toString("hex");
}; };
const hashEmailVerificationToken = (token) => { const hashEmailVerificationToken = (token) => {
return crypto return crypto.createHash("sha256").update(token).digest("hex");
.createHash("sha256")
.update(token)
.digest("hex");
}; };
const createEmailVerification = async (userId) => { const createEmailVerification = async (userId) => {
// 1. Generate raw token // 1. Generate raw token
const token = const token = generateEmailVerificationToken();
generateEmailVerificationToken();
// 2. Hash token // 2. Hash token
const tokenHash = const tokenHash = hashEmailVerificationToken(token);
hashEmailVerificationToken(token);
// 3. Create Redis key // 3. Create Redis key
const redisKey = const redisKey = `email-verification:${tokenHash}`;
`email-verification:${tokenHash}`;
await redis.set( await redis.set(redisKey, userId, "EX", EMAIL_VERIFICATION_TTL);
redisKey,
userId,
"EX",
EMAIL_VERIFICATION_TTL
);
return token; return token;
}; };
/** /**
* Check a verification token. * Check a verification token.
*/ */
const verifyEmailVerificationToken = const verifyEmailVerificationToken = async (token) => {
async (token) => { if (!token || typeof token !== "string") {
return null;
}
if ( // 1. Hash token received from user
!token || const tokenHash = hashEmailVerificationToken(token);
typeof token !== "string"
) {
return null;
}
// 2. Build same Redis key
const redisKey = `email-verification:${tokenHash}`;
// 1. Hash token received from user // 3. Search Redis
const tokenHash = const userId = await redis.get(redisKey);
hashEmailVerificationToken(token);
if (!userId) {
return null;
}
// 2. Build same Redis key return {
const redisKey = userId,
`email-verification:${tokenHash}`; redisKey,
// 3. Search Redis
const userId =
await redis.get(redisKey);
if (!userId) {
return null;
}
return {
userId,
redisKey,
};
}; };
};
const deleteEmailVerification = //send verification email to user
async (redisKey) => { 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 = { module.exports = {
createEmailVerification, createEmailVerification,
verifyEmailVerificationToken, verifyEmailVerificationToken,
sendVerificationEmail,
deleteEmailVerification, deleteEmailVerification,
hashEmailVerificationToken, hashEmailVerificationToken,
}; };
+10
View File
@@ -23,6 +23,14 @@ const generateUserId = () => {
return "usr_" + Math.random().toString(36).slice(2, 10); 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 generateClientId = () => {
const prefix = "cli_"; const prefix = "cli_";
return prefix + uuidv4(); return prefix + uuidv4();
@@ -141,6 +149,8 @@ const generateDocumentReferenceNo = async (type) => {
module.exports = { module.exports = {
generateUserId, generateUserId,
generateCustomerId,
generateBusinessCustomerId,
generateClientId, generateClientId,
generateInquId, generateInquId,
generateInquRefNo, generateInquRefNo,
+13 -2
View File
@@ -13,7 +13,10 @@ const jwt = require("jsonwebtoken");
require("dotenv").config(); require("dotenv").config();
const JWT_SECRET = process.env.JWT_SECRET || "your_jwt_secret_key"; 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 * Generate JWT token
@@ -33,4 +36,12 @@ const verifyToken = (token) => {
return jwt.verify(token, JWT_SECRET); 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; return otp;
} }
function saveOTP(key, otp, ttl = 5 * 60 * 1000) { // default TTL: 5 mins function saveOTP(key, otp) {
const expiresAt = Date.now() + ttl; ttl = parseInt(process.env.LOGIN_OTP_TTL_SECONDS || 300);
const expiresAt = Date.now() + ttl * 1000; // Convert seconds to milliseconds
otpCache.set(key, { otp, expiresAt }); otpCache.set(key, { otp, expiresAt });
} }
+107 -84
View File
@@ -10,104 +10,127 @@
// app/utils/passwordReset.util.js // app/utils/passwordReset.util.js
const crypto = require("crypto"); const crypto = require("crypto");
const redis = require("../config/redisClient");
const { sendMail } = require("./mail.util");
const createRedisConnection = require("../config/redis.config"); const PASSWORD_RESET_TTL =
const redis = createRedisConnection(); 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 createPasswordReset = async (userId) => {
const { hashPassword } = require("./hashPassword.util"); // 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);
/** // 3. Create Redis key
* Generate password reset token const redisKey = `password-reset:${tokenHash}`;
*
* @param {string} userId
* @returns {Promise<string>}
*/
async function generateResetToken(userId) {
try {
const token = crypto.randomBytes(32).toString("hex");
const tokenHash = crypto // 4. Save user ID with 15 minute TTL
.createHash("sha256") await redis.set(redisKey, userId, "EX", PASSWORD_RESET_TTL);
.update(token)
.digest("hex");
await redis.set( // Send only RAW token to user
`passwordReset:user:${userId}`, return token;
tokenHash, };
"EX",
RESET_TOKEN_TTL const verifyPasswordResetToken = async (token) => {
); if (!token || typeof token !== "string") {
log(`Generated password reset token for user ${userId} with TTL of ${RESET_TOKEN_TTL} seconds, Generated token:`, token); return null;
return token;
} catch (error) {
log("Password reset token generation failed", error);
throw new Error("Failed to generate password reset token");
} }
}
/** // Hash token received from user
* Reset password using token const tokenHash = hashPasswordResetToken(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) { // Create same Redis key
throw new Error("Invalid or expired reset token"); const redisKey = `password-reset:${tokenHash}`;
}
const tokenHash = crypto // Search Redis
.createHash("sha256") const userId = await redis.get(redisKey);
.update(token)
.digest("hex");
const isValid = if (!userId) {
crypto.timingSafeEqual( return null;
Buffer.from(storedHash),
Buffer.from(tokenHash)
);
if (!isValid) {
throw new Error("Invalid or expired reset token");
}
const user = await User.findByPk(userId);
if (!user) {
throw new Error("User not found");
}
user.password = await hashPassword(newPassword);
await user.save();
await redis.del(`passwordReset:user:${userId}`);
log(
`Password reset completed successfully for user ${userId}`
);
return true;
} catch (error) {
log("Password reset failed", error);
throw error;
} }
}
return {
userId,
redisKey,
};
};
const deletePasswordReset = async (redisKey) => {
await redis.del(redisKey);
};
const sendPasswordResetEmail =
async (email, firstName, resetToken) => {
const resetLink =
`${process.env.FRONTEND_URL}/reset-password?token=${resetToken}`;
await sendMail({
to: email,
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 = { module.exports = {
generateResetToken, createPasswordReset,
resetPassword 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,
};