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 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 });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
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,
|
||||
allowNull: true,
|
||||
},
|
||||
reset_otp: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
reset_otp_expires_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: false,
|
||||
|
||||
@ -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, {
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
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