GWM : forgot password

This commit is contained in:
Gowtham M 2025-10-28 16:53:04 +05:30
parent 0df0d647dd
commit efa18d4f78
6 changed files with 441 additions and 2 deletions

View File

@ -2,7 +2,9 @@ const db = require("../models");
const bcrypt = require("bcryptjs");
const Establishment = db.Establishment;
const EstablishmentUser = db.EstablishmentUser;
const EstablishmentPasswordResetRequest = db.EstablishmentPasswordResetRequest;
const { sendEmail } = require("../services/emailHelper"); // custom helper
const logger = require("../services/logger");
// Create Establishment + linked user
exports.createEstablishment = async (req, res) => {
try {
@ -116,3 +118,238 @@ exports.deleteEstablishment = async (req, res) => {
return res.status(500).send({'status':"failed",'message':err.message });
}
};
// GET ALL RESET REQUESTS
exports.getAllRequests = async (req, res) => {
try {
const requests = await EstablishmentPasswordResetRequest.findAll({
order: [["created_at", "DESC"]],
});
res.status(200).json({
status: "success",
data: requests,
});
} catch (err) {
res.status(500).json({ status: "failed", message: err.message });
}
};
// CREATE RESET REQUEST
exports.createRequest = async (req, res) => {
try {
const {
establishment_name,
establishment_code,
registered_email,
contact_person_name,
contact_phone,
additional_notes,
} = req.body;
// Validate input
if (!establishment_name || !establishment_code || !registered_email) {
return res.status(400).json({
status: "failed",
message: "establishment_name, establishment_code, and registered_email are required",
});
}
// Find user by email
const user = await EstablishmentUser.findOne({
where: { email: registered_email },
});
if (!user)
return res.status(404).json({ status: "failed", message: "Registered email not found" });
// Find establishment by code
const establishment = await Establishment.findOne({
where: { code: establishment_code },
});
if (!establishment)
return res.status(404).json({ status: "failed", message: "Invalid establishment code" });
// Verify both match
if (user.establishment_id !== establishment.id)
return res.status(400).json({
status: "failed",
message: "Establishment mismatch between code and email",
});
// Create reset request
const newRequest = await EstablishmentPasswordResetRequest.create({
establishment_name,
establishment_code,
registered_email,
contact_person_name,
contact_phone,
additional_notes,
establishment_user_id: user.id,
establishment_id: establishment.id,
});
// Trigger Email
const subject = "Password Reset Request Received";
const body = `
Dear ${contact_person_name || establishment_name},
We have received your password reset request for establishment "${establishment_name}".
Our team will verify and get back to you shortly.
Regards,
Support Team
`;
await sendEmail(registered_email, subject, body);
res.status(201).json({
status: "success",
message: "Password reset request created and email sent successfully",
data: newRequest,
});
} catch (err) {
console.error(err);
res.status(500).json({ status: "failed", message: err.message });
}
};
exports.forgotPasswordRequestOTP = async (req, res) => {
try {
const { establishment_name, establishment_code, registered_email } = req.body;
// Validate inputs
if (!establishment_name || !establishment_code || !registered_email)
return res.status(400).json({ status: "failed", message: "All fields are required" });
// Find establishment and user
const establishment = await Establishment.findOne({ where: { establishment_code } });
if (!establishment)
return res.status(404).json({ status: "failed", message: "Establishment not found" });
const user = await EstablishmentUser.findOne({
where: { email: registered_email, establishment_id: establishment.id },
});
if (!user)
return res.status(404).json({ status: "failed", message: "User not found for this establishment" });
// Generate 6-digit OTP
const otp = Math.floor(100000 + Math.random() * 900000).toString();
// Hash OTP before saving (security)
const hashedOtp = await bcrypt.hash(otp, 10);
// Store OTP in user record
await user.update({
reset_otp: hashedOtp,
reset_otp_expires_at: new Date(Date.now() + 10 * 60 * 1000), // valid for 10 mins
});
// Send OTP email
await sendEmail(
registered_email,
"Password Reset OTP",
`<p>Dear ${user.name},</p><p>Your OTP for password reset is <b>${otp}</b>. It is valid for 10 minutes.</p>`
);
logger.info(`OTP sent to ${registered_email} for establishment_id=${establishment.id}`);
return res.status(200).json({
status: "success",
message: "OTP sent successfully to registered email",
});
} catch (err) {
logger.error(`Error in forgotPasswordRequestOTP: ${err.message}`);
return res.status(500).json({ status: "failed", message: err.message });
}
};
exports.forgotPasswordVerifyOTP = async (req, res) => {
try {
const { registered_email, otp, password, confirm_password } = req.body;
if (!registered_email || !otp || !password || !confirm_password)
return res.status(400).json({ status: "failed", message: "All fields are required" });
if (password !== confirm_password)
return res.status(400).json({ status: "failed", message: "Passwords do not match" });
const user = await EstablishmentUser.findOne({ where: { email: registered_email } });
if (!user || !user.reset_otp)
return res.status(404).json({ status: "failed", message: "OTP not found or invalid user" });
// Check OTP expiry
if (new Date() > new Date(user.reset_otp_expires_at))
return res.status(400).json({ status: "failed", message: "OTP expired" });
// Compare OTP
const isOtpValid = await bcrypt.compare(otp, user.reset_otp);
if (!isOtpValid)
return res.status(400).json({ status: "failed", message: "Invalid OTP" });
// Update password
const hashedPassword = await bcrypt.hash(password, 10);
await user.update({
password: hashedPassword,
reset_otp: null,
reset_otp_expires_at: null,
});
logger.info(`Password reset successful for user=${registered_email}`);
return res.status(200).json({
status: "success",
message: "Password reset successfully",
});
} catch (err) {
logger.error(`Error in forgotPasswordVerifyOTP: ${err.message}`);
return res.status(500).json({ status: "failed", message: err.message });
}
};

View File

@ -0,0 +1,30 @@
module.exports = (sequelize, DataTypes) => {
const EstablishmentPasswordResetRequest = sequelize.define(
"EstablishmentPasswordResetRequest",
{
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
establishment_name: { type: DataTypes.STRING, allowNull: false },
establishment_code: { type: DataTypes.STRING, allowNull: false },
registered_email: { type: DataTypes.STRING, allowNull: false },
contact_person_name: { type: DataTypes.STRING },
contact_phone: { type: DataTypes.STRING },
additional_notes: { type: DataTypes.TEXT },
status: {
type: DataTypes.ENUM("pending", "completed", "rejected"),
defaultValue: "pending",
},
created_at: { type: DataTypes.DATE, defaultValue: DataTypes.NOW },
updated_at: { type: DataTypes.DATE, defaultValue: DataTypes.NOW },
updated_by: { type: DataTypes.INTEGER },
establishment_user_id: { type: DataTypes.INTEGER },
establishment_id: { type: DataTypes.INTEGER },
},
{
tableName: "establishment_password_reset_request",
timestamps: false,
}
);
return EstablishmentPasswordResetRequest;
};

View File

@ -48,6 +48,14 @@ module.exports = (sequelize, DataTypes) => {
type: DataTypes.INTEGER,
allowNull: true,
},
reset_otp: {
type: DataTypes.STRING,
allowNull: true,
},
reset_otp_expires_at: {
type: DataTypes.DATE,
allowNull: true,
},
},
{
timestamps: false,

View File

@ -25,7 +25,7 @@ db.UnitMaster = require("./UnitMaster.model")(sequelize, DataTypes);
db.SubmissionDeadline = require("./submissionDeadline.model")(sequelize, DataTypes);
db.NotificationTemplate = require("./notificationTemplate.model")(sequelize, DataTypes);
db.QuarterlyWindowsConfiguration = require("./quarterlyWindowsConfiguration.model")(sequelize, DataTypes);
db.EstablishmentPasswordResetRequest = require("./establishmentPasswordResetRequest.model")(sequelize, DataTypes);
// Associations
db.Establishment.hasMany(db.EstablishmentUser, {

View File

@ -51,6 +51,8 @@ const quarterlyWindowsController = require("../controllers/quarterlyWindowsConfi
* description: Manage notification templates
* - name: Quarterly Windows Configuration
* description: Manage quarterly windows configuration master data
* - name: Establishment Password Reset
* description: Manage Establishment Password Reset Requests
*/
@ -2111,6 +2113,148 @@ router.put("/quarterly_windows/:id",[verifySignature, verifyToken], quarterlyWin
/**
* @swagger
* /api/password-reset-requests:
* get:
* summary: Get all establishment password reset requests
* tags: [Establishment Password Reset Requests]
* security:
* - appSignature: []
* - bearerAuth: []
* responses:
* 200:
* description: List of all password reset requests
* 500:
* description: Internal server error
*/
router.get("/password-reset-requests", establishmentController.getAllRequests);
/**
* @swagger
* /api/password-reset-requests:
* post:
* summary: Create a new establishment password reset request
* tags: [Establishment Password Reset Requests]
* security:
* - appSignature: []
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - establishment_name
* - establishment_code
* - registered_email
* properties:
* establishment_name:
* type: string
* example: "ABC Industries"
* establishment_code:
* type: string
* example: "EST1234"
* registered_email:
* type: string
* example: "contact@abcindustries.com"
* contact_person_name:
* type: string
* example: "John Doe"
* contact_phone:
* type: string
* example: "+971501234567"
* additional_notes:
* type: string
* example: "Forgot credentials and need password reset."
* responses:
* 201:
* description: Password reset request created successfully
* 400:
* description: Validation failed or mismatch between establishment and email
* 404:
* description: Establishment or user not found
* 500:
* description: Internal server error
*/
router.post("/password-reset-requests", establishmentController.createRequest);
/**
* @swagger
* /api/forgot-password/request-otp:
* post:
* summary: Request OTP for establishment user password reset
* tags: [Establishment Password Reset Requests]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - establishment_name
* - establishment_code
* - registered_email
* properties:
* establishment_name: { type: string, example: "ABC Industries" }
* establishment_code: { type: string, example: "EST1234" }
* registered_email: { type: string, example: "contact@abcindustries.com" }
* responses:
* 200:
* description: OTP sent successfully
* 400:
* description: Missing or invalid data
* 404:
* description: Establishment or user not found
* 500:
* description: Server error
*/
router.post("/forgot-password/request-otp", establishmentController.forgotPasswordRequestOTP);
/**
* @swagger
* /api/forgot-password/verify-otp:
* post:
* summary: Verify OTP and reset establishment user password
* tags: [Establishment Password Reset Requests]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - registered_email
* - otp
* - password
* - confirm_password
* properties:
* registered_email: { type: string, example: "contact@abcindustries.com" }
* otp: { type: string, example: "123456" }
* password: { type: string, example: "NewPassword@123" }
* confirm_password: { type: string, example: "NewPassword@123" }
* responses:
* 200:
* description: Password reset successfully
* 400:
* description: Invalid OTP or password mismatch
* 404:
* description: User or OTP not found
* 500:
* description: Server error
*/
router.post("/forgot-password/verify-otp", establishmentController.forgotPasswordVerifyOTP);

View File

@ -0,0 +1,20 @@
const nodemailer = require("nodemailer");
exports.sendEmail = async (to, subject, body) => {
const transporter = nodemailer.createTransport({
host: process.env.MAIL_HOST,
port: process.env.MAIL_PORT,
secure: false,
auth: {
user: process.env.MAIL_USER,
pass: process.env.MAIL_PASS,
},
});
await transporter.sendMail({
from: process.env.MAIL_FROM || "noreply@example.com",
to,
subject,
html: body,
});
};