Auth #1
+264
-41
@@ -1,23 +1,48 @@
|
|||||||
# 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
|
||||||
{
|
{
|
||||||
"email": "sathira@niolla.lk",
|
"email": "sathira@niolla.lk",
|
||||||
"password": "Niolla@123"
|
"password": "Niolla@123"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Respond**
|
### Success response — `201 Created`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -26,26 +51,30 @@ 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
|
||||||
{
|
{
|
||||||
"email": "sathira@niolla.lk",
|
"email": "sathira@niolla.lk",
|
||||||
"otp": "922304"
|
"otp": "922304"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**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,27 +134,215 @@ 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
|
||||||
{
|
{
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "Logged out successfully"
|
"message": "Logged out successfully"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Error response — `500 Internal Server Error`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Failed to logout"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|||||||
@@ -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.
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Verify Email
|
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.
|
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
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -47,7 +67,7 @@ exports.loginReq = async (req, res) => {
|
|||||||
firstName: user.firstName,
|
firstName: user.firstName,
|
||||||
otp: otp,
|
otp: otp,
|
||||||
},
|
},
|
||||||
text: `Hello ${user.firstName}, your otp is ${otp}`,
|
text: `Hello ${user.firstName}, your otp is ${otp}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
log(`OTP for ${email}: ${otp}`);
|
log(`OTP for ${email}: ${otp}`);
|
||||||
@@ -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
@@ -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,13 +118,12 @@ 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(
|
||||||
{
|
{
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
© {{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>
|
||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
@@ -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 };
|
||||||
|
|||||||
@@ -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 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user