GWM : forgot password
This commit is contained in:
parent
0df0d647dd
commit
efa18d4f78
@ -2,7 +2,9 @@ const db = require("../models");
|
|||||||
const bcrypt = require("bcryptjs");
|
const bcrypt = require("bcryptjs");
|
||||||
const Establishment = db.Establishment;
|
const Establishment = db.Establishment;
|
||||||
const EstablishmentUser = db.EstablishmentUser;
|
const EstablishmentUser = db.EstablishmentUser;
|
||||||
|
const EstablishmentPasswordResetRequest = db.EstablishmentPasswordResetRequest;
|
||||||
|
const { sendEmail } = require("../services/emailHelper"); // custom helper
|
||||||
|
const logger = require("../services/logger");
|
||||||
// Create Establishment + linked user
|
// Create Establishment + linked user
|
||||||
exports.createEstablishment = async (req, res) => {
|
exports.createEstablishment = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@ -116,3 +118,238 @@ exports.deleteEstablishment = async (req, res) => {
|
|||||||
return res.status(500).send({'status':"failed",'message':err.message });
|
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 });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
30
app/models/establishmentPasswordResetRequest.model.js
Normal file
30
app/models/establishmentPasswordResetRequest.model.js
Normal 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;
|
||||||
|
};
|
||||||
|
|
||||||
@ -48,6 +48,14 @@ module.exports = (sequelize, DataTypes) => {
|
|||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
},
|
},
|
||||||
|
reset_otp: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
reset_otp_expires_at: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
timestamps: false,
|
timestamps: false,
|
||||||
|
|||||||
@ -25,7 +25,7 @@ db.UnitMaster = require("./UnitMaster.model")(sequelize, DataTypes);
|
|||||||
db.SubmissionDeadline = require("./submissionDeadline.model")(sequelize, DataTypes);
|
db.SubmissionDeadline = require("./submissionDeadline.model")(sequelize, DataTypes);
|
||||||
db.NotificationTemplate = require("./notificationTemplate.model")(sequelize, DataTypes);
|
db.NotificationTemplate = require("./notificationTemplate.model")(sequelize, DataTypes);
|
||||||
db.QuarterlyWindowsConfiguration = require("./quarterlyWindowsConfiguration.model")(sequelize, DataTypes);
|
db.QuarterlyWindowsConfiguration = require("./quarterlyWindowsConfiguration.model")(sequelize, DataTypes);
|
||||||
|
db.EstablishmentPasswordResetRequest = require("./establishmentPasswordResetRequest.model")(sequelize, DataTypes);
|
||||||
|
|
||||||
// Associations
|
// Associations
|
||||||
db.Establishment.hasMany(db.EstablishmentUser, {
|
db.Establishment.hasMany(db.EstablishmentUser, {
|
||||||
|
|||||||
@ -51,6 +51,8 @@ const quarterlyWindowsController = require("../controllers/quarterlyWindowsConfi
|
|||||||
* description: Manage notification templates
|
* description: Manage notification templates
|
||||||
* - name: Quarterly Windows Configuration
|
* - name: Quarterly Windows Configuration
|
||||||
* description: Manage quarterly windows configuration master data
|
* 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);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
20
app/services/emailHelper.js
Normal file
20
app/services/emailHelper.js
Normal 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,
|
||||||
|
});
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user