From b0c7e1c8d361a62e203f43186606d8524a24fa6e Mon Sep 17 00:00:00 2001 From: Gowtham M Date: Sat, 25 Oct 2025 14:43:11 +0530 Subject: [PATCH] GWM : password reset and QuarterlyWindowsConfiguration --- .../establishment_user.controller.js | 56 ++++- ...uarterlyWindowsConfiguration.controller.js | 85 +++++++ app/controllers/submission.controller.js | 1 + app/models/index.js | 1 + .../quarterlyWindowsConfiguration.model.js | 59 +++++ app/routes/routes.js | 211 ++++++++++++++---- app/services/email.service.js | 95 ++++++++ app/services/logger.js | 31 ++- package-lock.json | 45 +++- package.json | 4 +- server.js | 1 + 11 files changed, 533 insertions(+), 56 deletions(-) create mode 100644 app/controllers/quarterlyWindowsConfiguration.controller.js create mode 100644 app/models/quarterlyWindowsConfiguration.model.js create mode 100644 app/services/email.service.js diff --git a/app/controllers/establishment_user.controller.js b/app/controllers/establishment_user.controller.js index 02b64bc..6421235 100644 --- a/app/controllers/establishment_user.controller.js +++ b/app/controllers/establishment_user.controller.js @@ -54,13 +54,15 @@ exports.getUserById = async (req, res) => { // Update user exports.updateUser = async (req, res) => { try { - - const { name, email, password, is_active } = req.body; + + const { name, email, is_active, password } = req.body; const data = {}; - if (name) data.name = name; - if (email) data.email = email; - if (is_active !== undefined) data.is_active = is_active; - if (password) data.password = await bcrypt.hash(password, 10); + + // Allow updates only if fields are provided + if (name !== undefined && name !== null) data.name = name; + if (email !== undefined && email !== null) data.email = email; + if (is_active !== undefined && is_active !== null) data.is_active = is_active; + if (password && password.trim() !== "") data.password = await bcrypt.hash(password, 10); const [updated] = await EstablishmentUser.update(data, { where: { id: req.params.id }, @@ -75,6 +77,48 @@ exports.updateUser = async (req, res) => { } }; +// Change Establishment User Password +exports.changeUserPassword = async (req, res) => { + try { + const { old_password, new_password, confirm_password } = req.body; + const userId = req.params.id; + + // Validate inputs + if (!old_password || !new_password || !confirm_password) { + return res.status(400).send({ status: "failed", message: "All password fields are required" }); + } + + if (new_password !== confirm_password) { + return res.status(400).send({ status: "failed", message: "New password and confirm password do not match" }); + } + + // Find user + const user = await EstablishmentUser.findByPk(userId); + if (!user) { + return res.status(404).send({ status: "failed", message: "User not found" }); + } + + // Verify old password + const isMatch = await bcrypt.compare(old_password, user.password); + if (!isMatch) { + return res.status(400).send({ status: "failed", message: "Old password is incorrect" }); + } + + // Hash and update new password + const hashedPassword = await bcrypt.hash(new_password, 10); + await EstablishmentUser.update( + { password: hashedPassword }, + { where: { id: userId } } + ); + + return res.status(200).send({status: "success", message: "Password updated successfully",}); + + } catch (err) { + return res.status(500).send({ status: "failed", message: err.message }); + } +}; + + // Delete user exports.deleteUser = async (req, res) => { try { diff --git a/app/controllers/quarterlyWindowsConfiguration.controller.js b/app/controllers/quarterlyWindowsConfiguration.controller.js new file mode 100644 index 0000000..3522282 --- /dev/null +++ b/app/controllers/quarterlyWindowsConfiguration.controller.js @@ -0,0 +1,85 @@ +const db = require("../models"); +const QuarterlyWindowsConfiguration = db.QuarterlyWindowsConfiguration; + +// Create new configuration +exports.createConfig = async (req, res) => { + try { + const data = await QuarterlyWindowsConfiguration.create(req.body); + res.status(201).send({ + status: "success", + message: "Quarterly window configuration created successfully", + data, + }); + } catch (err) { + res.status(500).send({ status: "failed", message: err.message }); + } +}; + +// Get all configurations +exports.getAllConfigs = async (req, res) => { + try { + const data = await QuarterlyWindowsConfiguration.findAll({ + order: [["year", "DESC"]], + }); + res + .status(200) + .send({ status: "success", message: "Fetched successfully", data }); + } catch (err) { + res.status(500).send({ status: "failed", message: err.message }); + } +}; + +// Get single configuration by ID +exports.getConfigById = async (req, res) => { + try { + const data = await QuarterlyWindowsConfiguration.findByPk(req.params.id); + if (!data) + return res + .status(404) + .send({ status: "failed", message: "Record not found" }); + + res + .status(200) + .send({ status: "success", message: "Fetched successfully", data }); + } catch (err) { + res.status(500).send({ status: "failed", message: err.message }); + } +}; + +// Update configuration +exports.updateConfig = async (req, res) => { + try { + const [updated] = await QuarterlyWindowsConfiguration.update(req.body, { + where: { id: req.params.id }, + }); + if (!updated) + return res + .status(404) + .send({ status: "failed", message: "Record not found" }); + + res + .status(200) + .send({ status: "success", message: "Updated successfully" }); + } catch (err) { + res.status(500).send({ status: "failed", message: err.message }); + } +}; + +// Delete configuration +exports.deleteConfig = async (req, res) => { + try { + const deleted = await QuarterlyWindowsConfiguration.destroy({ + where: { id: req.params.id }, + }); + if (!deleted) + return res + .status(404) + .send({ status: "failed", message: "Record not found" }); + + res + .status(200) + .send({ status: "success", message: "Deleted successfully" }); + } catch (err) { + res.status(500).send({ status: "failed", message: err.message }); + } +}; diff --git a/app/controllers/submission.controller.js b/app/controllers/submission.controller.js index 1988b4c..5d76a40 100644 --- a/app/controllers/submission.controller.js +++ b/app/controllers/submission.controller.js @@ -3,6 +3,7 @@ const Submission = db.Submission; const SubmissionProduct = db.SubmissionProduct; const Product = db.Product; const { Op } = require("sequelize"); +const { sendEmail } = require("../services/email.service"); diff --git a/app/models/index.js b/app/models/index.js index 96d3805..34378a2 100644 --- a/app/models/index.js +++ b/app/models/index.js @@ -24,6 +24,7 @@ db.SubmissionProduct = require("./submissionProduct.model")(sequelize, DataTypes 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); // Associations diff --git a/app/models/quarterlyWindowsConfiguration.model.js b/app/models/quarterlyWindowsConfiguration.model.js new file mode 100644 index 0000000..f9a8a33 --- /dev/null +++ b/app/models/quarterlyWindowsConfiguration.model.js @@ -0,0 +1,59 @@ +module.exports = (sequelize, DataTypes) => { + const QuarterlyWindowsConfiguration = sequelize.define( + "quarterly_windows_configuration_master", + { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + }, + year: { + type: DataTypes.INTEGER, + allowNull: false, + }, + quarter: { + type: DataTypes.STRING(10), + allowNull: false, + }, + start_date: { + type: DataTypes.DATE, + allowNull: false, + }, + end_date: { + type: DataTypes.DATE, + allowNull: false, + }, + grace_periods_days: { + type: DataTypes.INTEGER, + allowNull: true, + }, + is_active: { + type: DataTypes.BOOLEAN, + defaultValue: true, + }, + created_at: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW, + }, + created_by: { + type: DataTypes.INTEGER, + allowNull: true, + }, + updated_at: { + type: DataTypes.DATE, + allowNull: true, + }, + updated_by: { + type: DataTypes.INTEGER, + allowNull: true, + }, + }, + { + tableName: "quarterly_windows_configuration_master", + timestamps: false, + } + ); + + return QuarterlyWindowsConfiguration; + }; + \ No newline at end of file diff --git a/app/routes/routes.js b/app/routes/routes.js index 9184c75..519d20f 100644 --- a/app/routes/routes.js +++ b/app/routes/routes.js @@ -16,6 +16,7 @@ const submissionController = require("../controllers/submission.controller"); const ConfigController = require("../controllers/config.controller"); const dashboardController = require("../controllers/dashboard.controller"); const notificationTemplateController = require("../controllers/notificationTemplate.controller"); +const quarterlyWindowsController = require("../controllers/quarterlyWindowsConfiguration.controller"); @@ -48,6 +49,8 @@ const notificationTemplateController = require("../controllers/notificationTempl * description: * - name: Notification Templates * description: Manage notification templates + * - name: Quarterly Windows Configuration + * description: Manage quarterly windows configuration master data */ @@ -652,6 +655,55 @@ router.get("/establishment-users/:id",[verifySignature, verifyToken], establishm */ router.put("/establishment-users/:id",[verifySignature, verifyToken], establishmentUserController.updateUser); +/** + * @swagger + * /api/establishment-users/{id}/change-password: + * put: + * summary: Change Establishment User Password + * description: Allows an establishment user to change their password after verifying the old password. + * tags: [Establishment Users] + * security: + * - bearerAuth: [] + * parameters: + * - name: id + * in: path + * required: true + * description: ID of the establishment user + * schema: + * type: integer + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - old_password + * - new_password + * - confirm_password + * properties: + * old_password: + * type: string + * example: "OldPassword@123" + * new_password: + * type: string + * example: "NewPassword@123" + * confirm_password: + * type: string + * example: "NewPassword@123" + * responses: + * 200: + * description: Password updated successfully + * 400: + * description: Validation or password mismatch error + * 404: + * description: User not found + * 500: + * description: Internal server error + */ +router.put("/establishment-users/:id/change-password", establishmentUserController.changeUserPassword); + + /** * @swagger * /api/establishment-users/{id}: @@ -1832,11 +1884,14 @@ router.get("/establishment_dashboard",[verifySignature, verifyToken],dashboardCo * get: * summary: Get all notification templates * tags: [Notification Templates] + * security: + * - appSignature: [] + * - bearerAuth: [] * responses: * 200: * description: Fetched successfully */ -router.get("/notification_templates", notificationTemplateController.getAllTemplates); +router.get("/notification_templates",[verifySignature, verifyToken], notificationTemplateController.getAllTemplates); /** * @swagger @@ -1844,6 +1899,9 @@ router.get("/notification_templates", notificationTemplateController.getAllTempl * get: * summary: Get a single notification template * tags: [Notification Templates] + * security: + * - appSignature: [] + * - bearerAuth: [] * parameters: * - name: id * in: path @@ -1852,31 +1910,9 @@ router.get("/notification_templates", notificationTemplateController.getAllTempl * 200: * description: Fetched successfully */ -router.get("/notification_templates/:id", notificationTemplateController.getTemplateById); +router.get("/notification_templates/:id",[verifySignature, verifyToken], notificationTemplateController.getTemplateById); + -// /** -// * @swagger -// * /api/notification_templates: -// * post: -// * summary: Create a new notification template -// * tags: [Notification Templates] -// * requestBody: -// * required: true -// * content: -// * application/json: -// * schema: -// * type: object -// * properties: -// * title: { type: string } -// * template_code: { type: string } -// * subject: { type: string } -// * body_html: { type: string } -// * placeholders: { type: array, items: { type: string } } -// * responses: -// * 201: -// * description: Created successfully -// */ -// router.post("/notification_templates", notificationTemplateController.createTemplate); /** * @swagger @@ -1884,6 +1920,9 @@ router.get("/notification_templates/:id", notificationTemplateController.getTemp * put: * summary: Update a notification template * tags: [Notification Templates] + * security: + * - appSignature: [] + * - bearerAuth: [] * parameters: * - name: id * in: path @@ -1903,23 +1942,113 @@ router.get("/notification_templates/:id", notificationTemplateController.getTemp * 200: * description: Updated successfully */ -router.put("/notification_templates/:id", notificationTemplateController.updateTemplate); +router.put("/notification_templates/:id",[verifySignature, verifyToken], notificationTemplateController.updateTemplate); + + + + + + + + + + + + + + +/** + * @swagger + * /api/quarterly_windows: + * post: + * summary: Create new quarterly configuration + * tags: [Quarterly Windows Configuration] + * security: + * - appSignature: [] + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * year: { type: integer } + * quarter: { type: string, example: "Q1" } + * start_date: { type: string, format: date } + * end_date: { type: string, format: date } + * grace_periods_days: { type: integer } + * is_active: { type: boolean } + * responses: + * 201: + * description: Created successfully + */ +router.post("/quarterly_windows",[verifySignature, verifyToken], quarterlyWindowsController.createConfig); + +/** + * @swagger + * /api/quarterly_windows: + * get: + * summary: Get all quarterly configurations + * tags: [Quarterly Windows Configuration] + * security: + * - appSignature: [] + * - bearerAuth: [] + * responses: + * 200: + * description: Fetched successfully + */ +router.get("/quarterly_windows",[verifySignature, verifyToken], quarterlyWindowsController.getAllConfigs); + +/** + * @swagger + * /api/quarterly_windows/{id}: + * get: + * summary: Get configuration by ID + * tags: [Quarterly Windows Configuration] + * security: + * - appSignature: [] + * - bearerAuth: [] + * parameters: + * - name: id + * in: path + * required: true + * responses: + * 200: + * description: Fetched successfully + */ +router.get("/quarterly_windows/:id",[verifySignature, verifyToken], quarterlyWindowsController.getConfigById); + +/** + * @swagger + * /api/quarterly_windows/{id}: + * put: + * summary: Update configuration by ID + * tags: [Quarterly Windows Configuration] + * security: + * - appSignature: [] + * - bearerAuth: [] + * parameters: + * - name: id + * in: path + * required: true + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * responses: + * 200: + * description: Updated successfully + */ +router.put("/quarterly_windows/:id",[verifySignature, verifyToken], quarterlyWindowsController.updateConfig); + + + + + -// /** -// * @swagger -// * /api/notification_templates/{id}: -// * delete: -// * summary: Delete a notification template -// * tags: [Notification Templates] -// * parameters: -// * - name: id -// * in: path -// * required: true -// * responses: -// * 200: -// * description: Deleted successfully -// */ -// router.delete("/notification_templates/:id", notificationTemplateController.deleteTemplate); diff --git a/app/services/email.service.js b/app/services/email.service.js new file mode 100644 index 0000000..0151310 --- /dev/null +++ b/app/services/email.service.js @@ -0,0 +1,95 @@ +const nodemailer = require("nodemailer"); +const { NotificationTemplate } = require("../models"); // adjust path if needed +const logger = require("../services/logger"); +require("dotenv").config(); + +const transporter = nodemailer.createTransport({ + host: process.env.SMTP_HOST, + port: process.env.SMTP_PORT, + secure: process.env.SMTP_SECURE === "true", // true for 465, false for 587 + auth: { + user: process.env.SMTP_USER, + pass: process.env.SMTP_PASS, + }, +}); + +/** + * Replace placeholders in template HTML with actual data + */ +function replacePlaceholders(templateHtml, data) { + let html = templateHtml; + for (const key in data) { + const regex = new RegExp(`{{\\s*${key}\\s*}}`, "g"); + html = html.replace(regex, data[key]); + } + return html; +} + + + + + + +exports.sendEmail = async (to, templateCode, data = {}) => { + const logPrefix = `[EMAIL SERVICE][${new Date().toISOString()}]`; + + logger.info(`${logPrefix} → Starting email send process`); + logger.info(`${logPrefix} → Template Code: ${templateCode}`); + logger.info(`${logPrefix} → Recipient: ${to}`); + logger.info(`${logPrefix} → Placeholder Data: ${JSON.stringify(data)}`); + + try { + // 1. Fetch template from DB + const template = await NotificationTemplate.findOne({ + where: { template_code: templateCode }, + }); + + if (!template) { + logger.error(`${logPrefix} ❌ Template not found for code: ${templateCode}`); + throw new Error(`Template not found for code: ${templateCode}`); + } + + // 2. Replace placeholders + const html = replacePlaceholders(template.template_html, data); + + // 3. Prepare mail options + const mailOptions = { + from: process.env.SMTP_FROM || process.env.SMTP_USER, + to, + subject: template.subject || "Notification", + html, + }; + + logger.info(`${logPrefix} ✉️ Sending email using SMTP...`); + + // 4. Send mail + const info = await transporter.sendMail(mailOptions); + + logger.info(`${logPrefix} ✅ Email sent successfully`); + logger.info(`${logPrefix} Message ID: ${info.messageId}`); + logger.info(`${logPrefix} Response: ${info.response}`); + + return { + status: "success", + message: "Email sent successfully", + messageId: info.messageId, + response: info.response, + to, + subject: template.subject, + templateCode, + }; + + } catch (err) { + logger.error(`${logPrefix} ❌ Email sending failed`); + logger.error(`${logPrefix} Error: ${err.message}`); + if (err.stack) logger.error(`${logPrefix} Stack: ${err.stack}`); + + return { + status: "failed", + message: err.message, + templateCode, + to, + error: err.stack || err.message, + }; + } + }; \ No newline at end of file diff --git a/app/services/logger.js b/app/services/logger.js index 880bb1f..743837f 100644 --- a/app/services/logger.js +++ b/app/services/logger.js @@ -1,15 +1,32 @@ -const { createLogger, transports, format } = require('winston'); -const path = require('path'); +const { createLogger, format, transports } = require("winston"); +const path = require("path"); +require("winston-daily-rotate-file"); + +const logDir = path.join(__dirname, "../writable/logs"); + +const dailyRotateFileTransport = new transports.DailyRotateFile({ + filename: path.join(logDir, "app-%DATE%.log"), + datePattern: "YYYY-MM-DD", + zippedArchive: false, + maxSize: "20m", + maxFiles: "30d", // keep logs for 30 days +}); const logger = createLogger({ + level: "info", format: format.combine( - format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), - format.printf(info => `${info.timestamp} ${info.level}: ${info.message}`) + format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), + format.printf( + (info) => `${info.timestamp} [${info.level.toUpperCase()}]: ${info.message}` + ) ), transports: [ - new transports.File({ filename: path.join(__dirname, '../writable/logs/app.log') }), - new transports.Console() - ] + dailyRotateFileTransport, + new transports.Console({ + format: format.combine(format.colorize(), format.simple()), + }), + ], + exitOnError: false, }); module.exports = logger; diff --git a/package-lock.json b/package-lock.json index f9d0fb8..800003e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,11 +19,13 @@ "morgan": "^1.10.1", "multer": "^2.0.2", "mysql2": "^3.15.2", + "nodemailer": "^7.0.10", "nodemon": "^3.1.10", "sequelize": "^6.37.7", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1", - "winston": "^3.18.3" + "winston": "^3.18.3", + "winston-daily-rotate-file": "^5.0.0" } }, "node_modules/@apidevtools/json-schema-ref-parser": { @@ -670,6 +672,14 @@ "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" }, + "node_modules/file-stream-rotator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/file-stream-rotator/-/file-stream-rotator-0.6.1.tgz", + "integrity": "sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==", + "dependencies": { + "moment": "^2.29.1" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -1400,6 +1410,14 @@ "node": ">= 0.6" } }, + "node_modules/nodemailer": { + "version": "7.0.10", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.10.tgz", + "integrity": "sha512-Us/Se1WtT0ylXgNFfyFSx4LElllVLJXQjWi2Xz17xWw7amDKO2MLtFnVp1WACy7GkVGs+oBlRopVNUzlrGSw1w==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/nodemon": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", @@ -1443,6 +1461,14 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -2112,6 +2138,23 @@ "node": ">= 12.0.0" } }, + "node_modules/winston-daily-rotate-file": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/winston-daily-rotate-file/-/winston-daily-rotate-file-5.0.0.tgz", + "integrity": "sha512-JDjiXXkM5qvwY06733vf09I2wnMXpZEhxEVOSPenZMii+g7pcDcTBt2MRugnoi8BwVSuCT2jfRXBUy+n1Zz/Yw==", + "dependencies": { + "file-stream-rotator": "^0.6.1", + "object-hash": "^3.0.0", + "triple-beam": "^1.4.1", + "winston-transport": "^4.7.0" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "winston": "^3" + } + }, "node_modules/winston-transport": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", diff --git a/package.json b/package.json index 18d6c69..e4f2f26 100644 --- a/package.json +++ b/package.json @@ -21,10 +21,12 @@ "morgan": "^1.10.1", "multer": "^2.0.2", "mysql2": "^3.15.2", + "nodemailer": "^7.0.10", "nodemon": "^3.1.10", "sequelize": "^6.37.7", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1", - "winston": "^3.18.3" + "winston": "^3.18.3", + "winston-daily-rotate-file": "^5.0.0" } } diff --git a/server.js b/server.js index bc67d36..36628cf 100644 --- a/server.js +++ b/server.js @@ -61,6 +61,7 @@ app.get("/api/test", (req, res) => { + // Start server const PORT = process.env.PORT || 5000; app.listen(PORT, () => {