Auth #1

Merged
Sathira merged 17 commits from auth into main 2026-09-07 06:53:05 +00:00
11 changed files with 781 additions and 53 deletions
Showing only changes of commit c3d9f899a1 - Show all commits
+87 -16
View File
@@ -1,10 +1,20 @@
# User API
#### Create New User
#### Create New Customer Account
Creates a new customer account and sends an email verification link.
The account is initially created with:
```text
accountType = customer
accountStatus = PENDING_VERIFICATION
emailVerifiedAt = null
```
**Endpoint**
```
```text
POST: http://localhost:3070/api/user
```
@@ -12,33 +22,94 @@ POST: http://localhost:3070/api/user
```json
{
"firstName": "Jhon",
"lastName": "Doe",
"email": "kalanajayasekara@niolla.lk",
"role": "System Developer",
"accountType": "admin",
"department": "IT Department"
"firstName": "Isuru",
"lastName": "Bimsara",
"email": "ibimsara00@gmail.com",
"password": "Hello@12346"
}
```
**Respond**
**Response**
```json
{
"success": true,
"message": "User Created Successfully",
"data": {
"id": "usr_763107d9-b56f-4b81-9d86-1889f98a6e6c",
"firstName": "Jhon",
"lastName": "Doe",
"email": "kalanajayasekara@niolla.lk",
"accountType": "admin",
"role": "System Developer",
"department": "IT Department"
"id": "usr_ei6i4n49",
"firstName": "Isuru",
"lastName": "Bimsara",
"email": "ibimsara00@gmail.com",
"accountType": "customer"
}
}
```
After registration, the user receives an email containing a verification link.
```
#### Verify Email
Verifies the customer's email using the raw verification token received by email.
The backend hashes the received token and compares the resulting hash with the `tokenHash` stored in the `email_verifications` table.
The token must:
```text
Exist in the database
Not have been used
Not have expired
```
**Endpoint**
```text
POST: http://localhost:3070/api/user/verify-email
```
**Request Body**
```json
{
"token": "RAW_VERIFICATION_TOKEN_FROM_EMAIL"
}
```
**Successful Response**
```json
{
"success": true,
"message": "Email verified successfully. Your account is now active."
}
```
After successful verification, the user record changes from:
```text
accountStatus = PENDING_VERIFICATION
emailVerifiedAt = NULL
```
to:
```text
accountStatus = ACTIVE
emailVerifiedAt = <current date and time>
```
The verification record is also updated:
```text
usedAt = <current date and time>
```
This prevents the same verification token from being successfully used again.
---
#### Get All Users
**Endpoint**
+9 -9
View File
@@ -9,14 +9,14 @@
// app/config/s3.config.js
const { S3Client } = require("@aws-sdk/client-s3");
// const { S3Client } = require("@aws-sdk/client-s3");
const s3 = new S3Client({
region: process.env.AWS_REGION,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
},
});
// const s3 = new S3Client({
// region: process.env.AWS_REGION,
// credentials: {
// accessKeyId: process.env.AWS_ACCESS_KEY_ID,
// secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
// },
// });
module.exports = s3;
// module.exports = s3;
+318 -23
View File
@@ -9,14 +9,18 @@
// app/controllers/user.controller.js
const { Op } = require("sequelize");
const db = require("../models");
const { hashPassword } = require("../utils/hashPassword.util");
const { validatePassword } = require("../utils/validation/validatePassword.util");
const { validateEmail } = require("../utils/validation/validateEmail.util");
const { generateUserId, generateId } = require("../utils/idGen.util");
const { logActivity } = require("../services/activity.service");
const { sendMail } = require("../utils/mail.util");
const {generateEmailVerificationToken, hashEmailVerificationToken} = require("../utils/emailVerification.util");
const User = db.User;
const Profile = db.Profile;
const EmailVerification = db.EmailVerification;
// Create a new user
exports.createNewUser = async (req, res) => {
@@ -28,9 +32,29 @@ exports.createNewUser = async (req, res) => {
lastName,
email,
password,
accountType,
} = req.body;
if (!firstName || !lastName || !email || !password) {
await transaction.rollback();
return res.status(400).send({
success: false,
message:
"First name, last name, email and password are required",
});
}
const emailValid = validateEmail(email);
if (!emailValid) {
await transaction.rollback();
return res.status(400).send({
success: false,
message: "Invalid email address",
});
}
const userExists = await User.findOne({ where: { email } });
if (userExists) {
await transaction.rollback();
@@ -41,15 +65,15 @@ exports.createNewUser = async (req, res) => {
});
}
let pass;
// let pass;
if(accountType === "admin" || accountType === "superadmin" || accountType === "manager" || accountType === "support_agent") {
pass = process.env.DEFAULT_PASSWORD;
}else{
pass = password;
}
// if(accountType === "admin" || accountType === "superadmin" || accountType === "manager" || accountType === "support_agent") {
// pass = process.env.DEFAULT_PASSWORD;
// }else{
// pass = password;
// }
const validatePasswordResult = validatePassword(pass);
const validatePasswordResult = validatePassword(password);
if (!validatePasswordResult) {
await transaction.rollback();
@@ -59,10 +83,21 @@ exports.createNewUser = async (req, res) => {
});
}
const hashedPassword = await hashPassword(pass);
const hashedPassword = await hashPassword(password);
const userID = generateUserId();
const verificationToken = generateEmailVerificationToken();
const verificationTokenHash =
hashEmailVerificationToken(
verificationToken
);
const verificationExpiresAt =
new Date(
Date.now() +
30 * 60 * 1000
);
const newUser = await User.create(
{
@@ -71,7 +106,9 @@ exports.createNewUser = async (req, res) => {
lastName,
email,
password: hashedPassword,
accountType,
// accountType,
accountStatus: "PENDING_VERIFICATION",
emailVerifiedAt: null,
},
{ transaction },
);
@@ -91,20 +128,109 @@ exports.createNewUser = async (req, res) => {
{ transaction },
);
await sendMail({
to: email,
subject: "Welcome - Ocenic Titan",
templateName: "welcome",
templateVars: {
firstName: firstName,
email: email,
password: process.env.DEFAULT_PASSWORD,
await EmailVerification.create(
{
id:
generateId(),
user_id:
newUser.id,
tokenHash:
verificationTokenHash,
expiresAt:
verificationExpiresAt,
usedAt:
null,
},
text: `Hello ${firstName}, your account has been created successfully.`,
});
{
transaction,
}
);
await transaction.commit();
const confirmationLink =
`${process.env.FRONTEND_URL}/verify-email?token=${verificationToken}`;
// ==================================================
// 18. Send verification email
// ==================================================
try {
await sendMail({
to:
email,
subject:
"Confirm Your ZUMRI Account",
templateName:
"emailVerification",
templateVars: {
customer_name:
firstName,
confirmation_link:
confirmationLink,
},
text:
`Hello ${firstName}, please verify your ZUMRI account using this link: ${confirmationLink}`,
});
} catch (mailError) {
// The database transaction is already committed.
//
// Do NOT delete the user here.
//
// User remains:
// PENDING_VERIFICATION
//
// Later resend-verification can send another email.
console.error(
"VERIFICATION EMAIL ERROR:",
mailError
);
return res.status(201).send({
success: true,
message:
"Account created, but verification email could not be sent. Please request a new verification email.",
data: {
id:
newUser.id,
firstName:
newUser.firstName,
lastName:
newUser.lastName,
email:
newUser.email,
accountType:
newUser.accountType,
accountStatus:
newUser.accountStatus,
},
});
}
await logActivity({
user: req.user,
description: `Created New User with ID: ${newUser.id}`,
@@ -121,8 +247,8 @@ exports.createNewUser = async (req, res) => {
lastName: newUser.lastName,
email: newUser.email,
accountType: newUser.accountType,
role: newUser.role,
department: newUser.department,
// role: newUser.role,
// department: newUser.department,
},
});
} catch (error) {
@@ -136,6 +262,175 @@ exports.createNewUser = async (req, res) => {
}
};
// Verify customer email
exports.verifyEmail = async (req, res) => {
const transaction =
await db.sequelize.transaction();
try {
// 1. Get token from request body
const {
token
} = req.body;
// 2. Token is required
if (!token) {
await transaction.rollback();
return res.status(400).send({
success: false,
message:
"Verification token is required",
});
}
// 3. Hash the received raw token
const tokenHash =
hashEmailVerificationToken(
token
);
// 4. Find matching valid token
const verification =
await EmailVerification.findOne({
where: {
tokenHash:
tokenHash,
usedAt:
null,
expiresAt: {
[Op.gt]:
new Date(),
},
},
transaction,
});
// 5. Token invalid / expired / already used
if (!verification) {
await transaction.rollback();
return res.status(400).send({
success: false,
message:
"Verification token is invalid or expired",
});
}
// 6. Find user
const user =
await User.findOne({
where: {
id:
verification.user_id,
},
transaction,
});
// 7. User not found
if (!user) {
await transaction.rollback();
return res.status(404).send({
success: false,
message:
"User not found",
});
}
// 8. Check already verified
if (
user.accountStatus === "ACTIVE" &&
user.emailVerifiedAt
) {
await transaction.rollback();
return res.status(400).send({
success: false,
message:
"Email is already verified",
});
}
// 9. Activate account
user.accountStatus =
"ACTIVE";
user.emailVerifiedAt =
new Date();
await user.save({
transaction,
});
// 10. Mark token as used
verification.usedAt =
new Date();
await verification.save({
transaction,
});
// 11. Commit
await transaction.commit();
// 12. Success
return res.status(200).send({
success: true,
message:
"Email verified successfully. Your account is now active.",
});
} catch (error) {
if (!transaction.finished) {
await transaction.rollback();
}
console.error(
"VERIFY EMAIL ERROR:",
error
);
return res.status(500).send({
success: false,
message:
"Failed to verify email",
});
}
};
// Get users with pagination (20 per page)
exports.getAllUsers = async (req, res) => {
try {
+1
View File
@@ -41,6 +41,7 @@ db.sequelize = sequelize;
// User and Authentication
db.User = require("./user/user.model")(sequelize, DataTypes);
db.EmailVerification = require("./user/emailVerification.model")(sequelize,DataTypes);
db.UserActivity = require("./activities/userActivities.model")(sequelize, DataTypes);
db.Profile = require("./user/profile.model")(sequelize, DataTypes);
@@ -0,0 +1,46 @@
//app/models/user/emailVerification.model.js
module.exports = (sequelize, DataTypes) => {
const EmailVerification = sequelize.define(
"EmailVerification",
{
id: {
type: DataTypes.STRING,
primaryKey: true,
},
user_id: {
type: DataTypes.STRING,
allowNull: false,
},
tokenHash: {
type: DataTypes.STRING,
allowNull: false,
},
expiresAt: {
type: DataTypes.DATE,
allowNull: false,
},
usedAt: {
type: DataTypes.DATE,
allowNull: true,
defaultValue: null,
},
},
{
tableName: "email_verifications",
timestamps: true,
}
);
EmailVerification.associate = (db) => {
EmailVerification.belongsTo(db.User, {
foreignKey: "user_id",
as: "user",
});
};
return EmailVerification;
};
+24 -3
View File
@@ -36,9 +36,25 @@ module.exports = (sequelize, DataTypes) => {
allowNull: false
},
accountType: {
type: DataTypes.ENUM("user", "admin", "superadmin", "manager", "business_customer", "rider", "customer","support_agent"),
defaultValue: "user"
}
type: DataTypes.ENUM("admin", "superadmin", "manager", "business_customer", "rider", "customer","support_agent"),
defaultValue: "customer"
},
accountStatus: {
type: DataTypes.ENUM(
"PENDING_VERIFICATION",
"ACTIVE",
"SUSPENDED",
"DEACTIVATED"
),
allowNull: false,
defaultValue: "PENDING_VERIFICATION",
},
emailVerifiedAt: {
type: DataTypes.DATE,
allowNull: true,
defaultValue: null,
},
},
{
tableName: "users",
@@ -55,6 +71,11 @@ module.exports = (sequelize, DataTypes) => {
foreignKey: "user_id",
as: "profile",
});
User.hasMany(db.EmailVerification, {
foreignKey: "user_id",
as: "emailVerifications",
});
};
return User;
+7 -2
View File
@@ -22,11 +22,16 @@ const PERMISSIONS = require("../constants/permissions");
router.post(
"/",
authenticate,
authorizedAccountType(["admin"]),
// authenticate,
// authorizedAccountType(["admin"]),
userController.createNewUser
);
router.post(
"/verify-email",
userController.verifyEmail
);
router.get(
"/",
authenticate,
+239
View File
@@ -0,0 +1,239 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
>
<title>Confirm Your ZUMRI Account</title>
</head>
<body
style="
margin: 0;
padding: 0;
background-color: #f4f4f4;
font-family: Arial, Helvetica, sans-serif;
"
>
<table
width="100%"
cellpadding="0"
cellspacing="0"
role="presentation"
style="background-color: #f4f4f4; padding: 40px 0;"
>
<tr>
<td align="center">
<table
width="600"
cellpadding="0"
cellspacing="0"
role="presentation"
style="
max-width: 600px;
width: 100%;
background-color: #ffffff;
padding: 40px;
border-radius: 8px;
"
>
<!-- Greeting -->
<tr>
<td>
<p
style="
font-size: 18px;
color: #333333;
margin-bottom: 30px;
"
>
Hi {{customer_name}},
</p>
</td>
</tr>
<!-- Welcome -->
<tr>
<td>
<h2
style="
color: #222222;
margin-bottom: 25px;
"
>
Welcome to ZUMRI!
</h2>
</td>
</tr>
<!-- Description -->
<tr>
<td>
<p
style="
font-size: 16px;
line-height: 1.6;
color: #555555;
"
>
Your account has been successfully registered.
To complete your registration and activate your
account, please confirm your email address by
clicking the button below.
</p>
</td>
</tr>
<!-- Verification Button -->
<tr>
<td
align="center"
style="padding: 30px 0;"
>
<a
href="{{confirmation_link}}"
style="
display: inline-block;
padding: 14px 28px;
background-color: #222222;
color: #ffffff;
text-decoration: none;
font-size: 16px;
font-weight: bold;
border-radius: 6px;
"
>
Confirm Your Email
</a>
</td>
</tr>
<!-- Alternative 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="{{confirmation_link}}"
style="color: #0066cc;"
>
{{confirmation_link}}
</a>
</p>
</td>
</tr>
<!-- Security Information -->
<tr>
<td>
<p
style="
font-size: 14px;
line-height: 1.6;
color: #777777;
margin-top: 30px;
"
>
For security purposes, this confirmation link
will expire after a limited period. If you did
not create a ZUMRI account, you can safely
ignore this email.
</p>
</td>
</tr>
<!-- Thank You -->
<tr>
<td>
<p
style="
font-size: 16px;
line-height: 1.6;
color: #555555;
margin-top: 30px;
"
>
Thank you for joining ZUMRI. We look forward
to having you with us!
</p>
</td>
</tr>
<!-- Footer -->
<tr>
<td>
<p
style="
font-size: 16px;
color: #333333;
margin-top: 30px;
"
>
Best regards,<br>
<strong>ZUMRI Team</strong>
</p>
</td>
</tr>
<!-- Copyright -->
<tr>
<td
align="center"
style="
border-top: 1px solid #eeeeee;
padding-top: 20px;
"
>
<p
style="
font-size: 12px;
color: #999999;
"
>
&copy; {{currentYear}} ZUMRI.
All rights reserved.
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
+28
View File
@@ -0,0 +1,28 @@
//app/utils/emailVerification.util.js
const crypto = require("crypto");
/**
* Generate a cryptographically secure
* random email-verification token.
*/
const generateEmailVerificationToken = () => {
return crypto.randomBytes(32).toString("hex");
};
/**
* Hash verification token before
* storing it in database.
*/
const hashEmailVerificationToken = (token) => {
return crypto
.createHash("sha256")
.update(token)
.digest("hex");
};
module.exports = {
generateEmailVerificationToken,
hashEmailVerificationToken,
};
@@ -0,0 +1,21 @@
const validateEmail = (email) => {
// Must be a string
if (typeof email !== "string") {
return false;
}
// Remove spaces
const trimmedEmail = email.trim();
// Basic email format validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(trimmedEmail);
};
const normalizeEmail = (email) => {
return email.trim().toLowerCase();
};
module.exports = {
validateEmail, normalizeEmail
};
@@ -1,3 +1,4 @@
//app/utils/validation/validatePassword.util.js
const validatePassword = (password) => {
// Check if password is a string
if (typeof password !== "string") {