GWM : password reset and QuarterlyWindowsConfiguration

This commit is contained in:
Gowtham M 2025-10-25 14:43:11 +05:30
parent 59975c709a
commit b0c7e1c8d3
11 changed files with 533 additions and 56 deletions

View File

@ -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 {

View File

@ -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 });
}
};

View File

@ -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");

View File

@ -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

View File

@ -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;
};

View File

@ -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);

View File

@ -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,
};
}
};

View File

@ -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;

45
package-lock.json generated
View File

@ -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",

View File

@ -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"
}
}

View File

@ -61,6 +61,7 @@ app.get("/api/test", (req, res) => {
// Start server
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => {