diff --git a/app/controllers/establishment.controller.js b/app/controllers/establishment.controller.js index 23a785a..7f21b1a 100644 --- a/app/controllers/establishment.controller.js +++ b/app/controllers/establishment.controller.js @@ -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", + `

Dear ${user.name},

Your OTP for password reset is ${otp}. It is valid for 10 minutes.

` + ); + + 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 }); + } +}; + + + diff --git a/app/models/establishmentPasswordResetRequest.model.js b/app/models/establishmentPasswordResetRequest.model.js new file mode 100644 index 0000000..cb63be2 --- /dev/null +++ b/app/models/establishmentPasswordResetRequest.model.js @@ -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; + }; + \ No newline at end of file diff --git a/app/models/establishment_user.model.js b/app/models/establishment_user.model.js index 95741b3..9cb20d3 100644 --- a/app/models/establishment_user.model.js +++ b/app/models/establishment_user.model.js @@ -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, diff --git a/app/models/index.js b/app/models/index.js index 7ac2acb..0089ac5 100644 --- a/app/models/index.js +++ b/app/models/index.js @@ -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, { diff --git a/app/routes/routes.js b/app/routes/routes.js index 9dcbe5b..e1de9c4 100644 --- a/app/routes/routes.js +++ b/app/routes/routes.js @@ -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); + + + diff --git a/app/services/emailHelper.js b/app/services/emailHelper.js new file mode 100644 index 0000000..219bcc7 --- /dev/null +++ b/app/services/emailHelper.js @@ -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, + }); +};