Auth #1

Merged
Sathira merged 17 commits from auth into main 2026-09-07 06:53:05 +00:00
6 changed files with 244 additions and 262 deletions
Showing only changes of commit 621d348eb1 - Show all commits
+1 -1
View File
@@ -20,5 +20,5 @@ module.exports = {
user: process.env.MAIL_USER, user: process.env.MAIL_USER,
pass: process.env.MAIL_PASS, pass: process.env.MAIL_PASS,
}, },
from: `Oceanic Maritime Solutions <${process.env.MAIL_FROM}>`, from: `ZUMRI <${process.env.MAIL_FROM}>`,
}; };
+156 -198
View File
@@ -9,7 +9,7 @@
// app/controllers/user.controller.js // app/controllers/user.controller.js
const { Op } = require("sequelize"); // 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");
@@ -17,10 +17,11 @@ const { validateEmail } = require("../utils/validation/validateEmail.util");
const { generateUserId, generateId } = require("../utils/idGen.util"); const { generateUserId, generateId } = require("../utils/idGen.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 {generateEmailVerificationToken, hashEmailVerificationToken} = require("../utils/emailVerification.util"); const {
createEmailVerification, verifyEmailVerificationToken, deleteEmailVerification} = require("../utils/emailVerification.util");;
const User = db.User; const User = db.User;
const Profile = db.Profile; const Profile = db.Profile;
const EmailVerification = db.EmailVerification; // const EmailVerification = db.EmailVerification;
// Create a new user // Create a new user
exports.createNewUser = async (req, res) => { exports.createNewUser = async (req, res) => {
@@ -84,20 +85,8 @@ if (!emailValid) {
} }
const hashedPassword = await hashPassword(password); const hashedPassword = await hashPassword(password);
const userID = generateUserId(); const userID = generateUserId();
const verificationToken = generateEmailVerificationToken();
const verificationTokenHash =
hashEmailVerificationToken(
verificationToken
);
const verificationExpiresAt =
new Date(
Date.now() +
30 * 60 * 1000
);
const newUser = await User.create( const newUser = await User.create(
{ {
@@ -128,37 +117,13 @@ if (!emailValid) {
{ transaction }, { transaction },
); );
await EmailVerification.create(
{
id:
generateId(),
user_id:
newUser.id,
tokenHash:
verificationTokenHash,
expiresAt:
verificationExpiresAt,
usedAt:
null,
},
{
transaction,
}
);
await transaction.commit(); await transaction.commit();
const verificationToken = await createEmailVerification(newUser.id);
const confirmationLink = const confirmationLink =
`${process.env.FRONTEND_URL}/verify-email?token=${verificationToken}`; `${process.env.FRONTEND_URL}/verify-email?token=${verificationToken}`;
// ==================================================
// 18. Send verification email
// ==================================================
try { try {
@@ -188,15 +153,6 @@ if (!emailValid) {
} catch (mailError) { } 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( console.error(
"VERIFICATION EMAIL ERROR:", "VERIFICATION EMAIL ERROR:",
mailError mailError
@@ -246,13 +202,20 @@ 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, // role: newUser.role,
// department: newUser.department, // department: newUser.department,
}, },
}); });
} catch (error) { } catch (error) {
await transaction.rollback(); if (!transaction.finished) {
await transaction.rollback();
}
console.error(
"CREATE USER ERROR:",
error
);
res.status(500).send({ res.status(500).send({
success: false, success: false,
@@ -263,173 +226,168 @@ if (!emailValid) {
}; };
// Verify customer email // Verify customer email
exports.verifyEmail = async (req, res) => { exports.verifyEmail =
async (req, res) => {
const transaction = const transaction =
await db.sequelize.transaction(); await db.sequelize.transaction();
try {
// 1. Get token from request body
const {
token
} = req.body;
// 2. Token is required try {
if (!token) { const {
token,
} = req.body;
await transaction.rollback();
return res.status(400).send({ if (!token) {
success: false,
message: await transaction.rollback();
"Verification token is required",
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();
// 3. Hash the received raw token await deleteEmailVerification(
const tokenHash = redisKey
hashEmailVerificationToken(
token
); );
// 4. Find matching valid token
const verification =
await EmailVerification.findOne({
where: { return res.status(200).send({
success:
true,
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: message:
"Verification token is invalid or expired", "Email verified successfully. Your account is now active.",
});
}
// 6. Find user
const user =
await User.findOne({
where: {
id:
verification.user_id,
},
transaction,
}); });
// 7. User not found } catch (error) {
if (!user) {
await transaction.rollback(); if (!transaction.finished) {
await transaction.rollback();
}
console.error(
"VERIFY EMAIL ERROR:",
error
);
return res.status(500).send({
success:
false,
return res.status(404).send({
success: false,
message: message:
"User not found", "Failed to verify email",
}); });
} }
};
// 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) // Get users with pagination (20 per page)
exports.getAllUsers = async (req, res) => { exports.getAllUsers = async (req, res) => {
-1
View File
@@ -41,7 +41,6 @@ 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.EmailVerification = require("./user/emailVerification.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);
@@ -1,46 +0,0 @@
//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;
};
-4
View File
@@ -72,10 +72,6 @@ module.exports = (sequelize, DataTypes) => {
as: "profile", as: "profile",
}); });
User.hasMany(db.EmailVerification, {
foreignKey: "user_id",
as: "emailVerifications",
});
}; };
return User; return User;
+87 -12
View File
@@ -1,19 +1,20 @@
//app/utils/emailVerification.util.js // app/utils/emailVerification.util.js
const crypto = require("crypto"); const crypto = require("crypto");
/** const redis = require("../config/redisClient");
* Generate a cryptographically secure
* random email-verification token. const EMAIL_VERIFICATION_TTL =
*/ Number(
process.env.EMAIL_VERIFICATION_TTL_SECONDS
) || 1800;
const generateEmailVerificationToken = () => { const generateEmailVerificationToken = () => {
return crypto.randomBytes(32).toString("hex"); return crypto
.randomBytes(32)
.toString("hex");
}; };
/**
* Hash verification token before
* storing it in database.
*/
const hashEmailVerificationToken = (token) => { const hashEmailVerificationToken = (token) => {
return crypto return crypto
.createHash("sha256") .createHash("sha256")
@@ -21,8 +22,82 @@ const hashEmailVerificationToken = (token) => {
.digest("hex"); .digest("hex");
}; };
const createEmailVerification = async (userId) => {
// 1. Generate raw token
const token =
generateEmailVerificationToken();
// 2. Hash token
const tokenHash =
hashEmailVerificationToken(token);
// 3. Create Redis key
const redisKey =
`email-verification:${tokenHash}`;
await redis.set(
redisKey,
userId,
"EX",
EMAIL_VERIFICATION_TTL
);
return token;
};
/**
* Check a verification token.
*/
const verifyEmailVerificationToken =
async (token) => {
if (
!token ||
typeof token !== "string"
) {
return null;
}
// 1. Hash token received from user
const tokenHash =
hashEmailVerificationToken(token);
// 2. Build same Redis key
const redisKey =
`email-verification:${tokenHash}`;
// 3. Search Redis
const userId =
await redis.get(redisKey);
if (!userId) {
return null;
}
return {
userId,
redisKey,
};
};
const deleteEmailVerification =
async (redisKey) => {
await redis.del(redisKey);
};
module.exports = { module.exports = {
generateEmailVerificationToken, createEmailVerification,
verifyEmailVerificationToken,
deleteEmailVerification,
hashEmailVerificationToken, hashEmailVerificationToken,
}; };