const db = require("../models"); const bcrypt = require("bcryptjs"); const Establishment = db.Establishment; const EstablishmentUser = db.EstablishmentUser; const EstablishmentPasswordResetRequest = db.EstablishmentPasswordResetRequest; const EstablishmentProduct = db.EstablishmentProduct; const CityTown = db.CityTown; const Emirate = db.Emirate; const user = db.user; const User = db.user; const Product = db.Product; const ExcelJS = require("exceljs"); const { Op, Sequelize } = require("sequelize"); const { sendEmail } = require("../services/emailHelper"); const { sendEmailService } = require("../services/email.service"); const logger = require("../services/logger"); const fs = require("fs"); const csv = require("csv-parser"); const path = require("path"); const { version } = require("os"); const sequelize = db.sequelize; const { sanitizeForLog } = require("../utils/sanitize"); const sanitize = require("sanitize-html"); const crypto = require('crypto'); const sanitizeStringValue = (value) => typeof value === "string" ? sanitize(value, { allowedTags: [], allowedAttributes: {} }) : value; exports.testEmail = async (req, res) => { placeHolderData = { contact_name : '', portal_url : process.env.FE_BASE_URL, username: '', password: '', support_email : process.env.SUPPORT_EMAIL, support_phone : process.env.SUPPORT_PHONE, } await sendEmailService('gowthamceline46@gmail.com', 'establishment_user_creation_to_user', placeHolderData); }; exports.createEstablishment = async (req, res) => { try { // convert empty "" → null BEFORE destructure [ "establishment_code", "factory_name", "permanent_factory_code", "industry_code", "industry_code_production", "license_number", "isic_code" ].forEach(key => { if (req.body[key] === "") req.body[key] = null; }); const { establishment_code, factory_name, permanent_factory_code, industry_code, industry_code_production, industry_code_mismatch_remarks, license_number, isic_code, description, // Establishment Contact Details establishment_address, establishment_city_town_id, establishment_emirate_id, establishment_postal_code, establishment_po_box, establishment_makani_number, establishment_contact_person_name, establishment_contact_person_designation, establishment_mobile_number, establishment_contact_email, establishment_website, // Corporate Details corporate_same_as_establishment, corporate_name, corporate_address, corporate_city_town_id, corporate_emirate_id, corporate_postal_code, corporate_po_box, corporate_makani_number, corporate_contact_person_name, corporate_contact_person_designation, corporate_mobile_number, corporate_email, corporate_website, emirati_male, emirati_female, non_emirati_male, non_emirati_female, total_emirati, total_employees, created_by, establishment_user, establishment_products } = req.body; // Basic Validation if (!establishment_code || !factory_name || !establishment_user) { return res.status(400).send({ status: "failed", message: "Missing required fields: establishment_code, factory_name, email, establishment_user", }); } if (!establishment_city_town_id || !corporate_city_town_id) { return res.status(400).send({ status: "failed", message: "Missing required field: city/town", }); } if (!Array.isArray(establishment_products) || establishment_products.length === 0) { return res.status(400).send({ status: "failed", message: "Please select at least one product." }); } for (const item of establishment_products) { const id = Number(item.product_id); if (!id || isNaN(id) || id <= 1) { return res.status(400).send({ status: "failed", message: "Invalid product_id. product_id must be a number greater than 1 and cannot be 0." }); } } const existingEstablishment = await Establishment.findOne({where: { establishment_code }, }); if (existingEstablishment) { return res.status(400).send({status: "failed",message: "Establishment code already exists", }); } const existingUser = await EstablishmentUser.findOne({ where: { email: establishment_user.email } }); if (existingUser) { return res.status(400).send({ status: "failed", message: "User email already exists" }); } // Create establishment record const establishment = await Establishment.create({ establishment_code: sanitizeStringValue(establishment_code), factory_name: sanitizeStringValue(factory_name), permanent_factory_code: sanitizeStringValue(permanent_factory_code), industry_code: sanitizeStringValue(industry_code), industry_code_production: sanitizeStringValue(industry_code_production), industry_code_mismatch_remarks: sanitizeStringValue(industry_code_mismatch_remarks), license_number: sanitizeStringValue(license_number), isic_code: sanitizeStringValue(isic_code), description: sanitizeStringValue(description), establishment_address: sanitizeStringValue(establishment_address), establishment_city_town_id, establishment_emirate_id, establishment_postal_code: sanitizeStringValue(establishment_postal_code), establishment_po_box: sanitizeStringValue(establishment_po_box), establishment_makani_number: sanitizeStringValue(establishment_makani_number), establishment_contact_person_name: sanitizeStringValue(establishment_contact_person_name), establishment_contact_person_designation: sanitizeStringValue(establishment_contact_person_designation), establishment_mobile_number: sanitizeStringValue(establishment_mobile_number), establishment_contact_email: sanitizeStringValue(establishment_contact_email), establishment_website: sanitizeStringValue(establishment_website), corporate_same_as_establishment, corporate_name: sanitizeStringValue(corporate_name), corporate_address: sanitizeStringValue(corporate_address), corporate_city_town_id, corporate_emirate_id, corporate_postal_code: sanitizeStringValue(corporate_postal_code), corporate_po_box: sanitizeStringValue(corporate_po_box), corporate_makani_number: sanitizeStringValue(corporate_makani_number), corporate_contact_person_name: sanitizeStringValue(corporate_contact_person_name), corporate_contact_person_designation: sanitizeStringValue(corporate_contact_person_designation), corporate_mobile_number: sanitizeStringValue(corporate_mobile_number), corporate_email: sanitizeStringValue(corporate_email), corporate_website: sanitizeStringValue(corporate_website), emirati_male, emirati_female, non_emirati_male, non_emirati_female, total_emirati, total_employees, created_by: req.body.created_by || req.user.id, created_at: new Date(), }); // Store password before hashing for email purpose only const plainPasswordForEmail = establishment_user.password; // Hash password const hashedPassword = await bcrypt.hash(establishment_user.password, 10); // Create linked Establishment User const user = await EstablishmentUser.create({ establishment_id: establishment.id, name: establishment_user.name, email: establishment_user.email, password: hashedPassword, created_by: req.body.created_by || req.user.id, }); // Prepare sanitized data for email notification const emailContactName = sanitizeStringValue(establishment_user.name); const emailUsername = sanitizeStringValue(establishment_user.email); const emailPortalUrl = process.env.FE_BASE_URL; const emailSupportEmail = process.env.SUPPORT_EMAIL; const emailSupportPhone = process.env.SUPPORT_PHONE; const placeHolderData = { contact_name: emailContactName, portal_url: emailPortalUrl, username: emailUsername, password: plainPasswordForEmail, support_email: emailSupportEmail, support_phone: emailSupportPhone, }; // Send email notification to user await sendEmailService(emailUsername, 'establishment_user_creation_to_user', placeHolderData); if (Array.isArray(establishment_products) && establishment_products.length > 0) { // remove duplicates inside request const uniqueProducts = establishment_products.reduce((acc, item) => { if (!acc.some(p => p.product_id === item.product_id)) { acc.push(item); } return acc; }, []); const existing = await EstablishmentProduct.findAll({ where: { establishment_id: establishment.id, product_id: uniqueProducts.map(x => x.product_id) }, attributes: ["product_id"] }); const existingIds = existing.map(x => x.product_id); const insertData = uniqueProducts .filter(x => !existingIds.includes(x.product_id)) .map(x => ({ establishment_id: establishment.id, product_id: x.product_id, created_by: x.created_by || null, action_done_by: x.action_done_by || "admin_user", created_at: new Date() })); if (insertData.length > 0) { await EstablishmentProduct.bulkCreate(insertData); } } return res.status(201).send({ status: "success", message: "Establishment and linked user created successfully." }); } catch (err) { if (err.name === "SequelizeUniqueConstraintError") { const field = err.errors[0].path; return res.status(400).json({ status: "failed", message: `${field} already exists` }); } logger.error(`Error creating establishment: ${err.message }`); logger.error(`Stack trace: ${err.stack}`); return res.status(500).send({status: "failed",message: "Internal server error" }); } }; exports.getAllEstablishments = async (req, res) => { try { // Set security headers to prevent clickjacking res.setHeader('X-Frame-Options', 'DENY'); res.setHeader('X-Content-Type-Options', 'nosniff'); res.setHeader('X-XSS-Protection', '1; mode=block'); res.setHeader('Content-Security-Policy', "frame-ancestors 'none'"); let { page = 1, limit = 10, search = "", emirate_id, isic_code, status, sort_by = "created_at", sort_order = "DESC", export: exportType, } = req.query; page = parseInt(page); limit = parseInt(limit); const offset = (page - 1) * limit; // Build dynamic where clause const whereClause = {}; // Filters if (status) whereClause.is_active = status === "active" ? 1 : 0; if (isic_code) whereClause.isic_code = isic_code; if (emirate_id) whereClause.establishment_emirate_id = emirate_id; // Search by name, establishment_code, or contact emails if (search) { whereClause[Op.or] = [ { factory_name: { [Op.like]: `%${search}%` } }, { establishment_code: { [Op.like]: `%${search}%` } }, { establishment_contact_email: { [Op.like]: `%${search}%` } }, { corporate_email: { [Op.like]: `%${search}%` } }, ]; } // Fetch Data const data = await Establishment.findAll({ where: whereClause, attributes: { include: [ [ Sequelize.literal(`( SELECT COUNT(*) FROM establishment_products ep WHERE ep.establishment_id = establishments.id )`), "product_count" ], [ Sequelize.literal(`( SELECT name FROM admin_users au WHERE au.id = establishments.created_by )`), "created_by_name" ] ] }, include: [ { model: CityTown, as: "establishment_city", attributes: ["name"] }, { model: Emirate, as: "establishment_emirate", attributes: ["name"] }, { model: CityTown, as: "corporate_city", attributes: ["name"] }, { model: Emirate, as: "corporate_emirate", attributes: ["name"] }, { model: user, as: "created_user", attributes: ["name"] }, ], order: [[sort_by, sort_order]], ...(exportType ? {} : { limit, offset }), }); // If export = excel → generate file if (exportType && exportType.toLowerCase() === "excel") { const workbook = new ExcelJS.Workbook(); const worksheet = workbook.addWorksheet("Establishments"); // Define headers worksheet.columns = [ { header: "ID", key: "id", width: 10 }, { header: "Establishment Name", key: "factory_name", width: 30 }, { header: "Contact Name", key: "establishment_contact_person_name", width: 30 }, { header: "Emirate", key: "emirate_name", width: 20 }, { header: "ISIC Code", key: "isic_code", width: 20 }, { header: "Establishment ID", key: "establishment_code", width: 20 }, { header: "Total Employees", key: "total_employees", width: 20 }, { header: "Created By", key: "created_user", width: 20 }, { header: "Created On", key: "created_at", width: 20 }, { header: "Last Updated", key: "updated_at", width: 20 }, { header: "Status", key: "status", width: 15 }, ]; // Format data data.forEach((item) => { worksheet.addRow({ id: item.id, factory_name: item.factory_name, establishment_contact_person_name: item.establishment_contact_person_name, emirate_name: item.establishment_emirate?.name || "-", isic_code: item.isic_code || "-", establishment_code: item.establishment_code, total_employees: item.total_employees, created_user: item.created_user?.name || "-", created_at: new Date(item.created_at).toLocaleDateString(), updated_at: new Date(item.updated_at).toLocaleDateString(), status: item.is_active ? "Active" : "Inactive", }); }); // Styling header worksheet.getRow(1).eachCell((cell) => { cell.font = { bold: true }; cell.alignment = { horizontal: "center" }; cell.border = { top: { style: "thin" }, left: { style: "thin" }, bottom: { style: "thin" }, right: { style: "thin" }, }; }); // Set security headers for Excel download res.setHeader('X-Frame-Options', 'DENY'); res.setHeader('X-Content-Type-Options', 'nosniff'); res.setHeader( "Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ); res.setHeader( "Content-Disposition", `attachment; filename=Establishments_${Date.now()}.xlsx` ); await workbook.xlsx.write(res); return res.end(); } // Otherwise return JSON (paginated) const totalCount = await Establishment.count({ where: whereClause }); return res.status(200).json({ status: "success", message: "Fetched establishments successfully", data, pagination: { total_records: totalCount, current_page: page, total_pages: Math.ceil(totalCount / limit), limit, }, }); } catch (err) { logger.error(`Error on get all establishment: ${err.message }`); logger.error(`Stack trace: ${err.stack}`); res.setHeader('X-Frame-Options', 'DENY'); res.setHeader('X-Content-Type-Options', 'nosniff'); return res.status(500).json({ status: "failed", message: "Internal server error", }); } }; // Get one establishment exports.getEstablishmentById = async (req, res) => { try { const data = await Establishment.findByPk(req.params.id, { include: [ { model: EstablishmentUser, as: "users", attributes: ["id", "name", "email", "gender", "is_active"], }, { model: CityTown, as: "establishment_city", attributes: ["name"], }, { model: Emirate, as: "establishment_emirate", attributes: ["name"], },{ model: CityTown, as: "corporate_city", attributes: ["name"], }, { model: Emirate, as: "corporate_emirate", attributes: ["name"], }, { model: user, as: "created_user", attributes: ["name"], }, { model: EstablishmentProduct, as: "establishment_products", where: { is_active: 1 }, attributes: [ "id", "establishment_id", "product_id", "action_done_by", [ Sequelize.literal(` CASE WHEN establishment_products.action_done_by = 'admin_users' THEN (SELECT name FROM admin_users WHERE admin_users.id = establishment_products.created_by) WHEN establishment_products.action_done_by = 'establishment_users' THEN (SELECT name FROM establishment_users WHERE establishment_users.id = establishment_products.created_by) ELSE NULL END `), "created_user" ] ], include: [ { model: Product, as: "product", attributes: ["product_name", "hs_code", "hs_description"], }, ], } ], }); if (!data) return res.status(404).send({'status':"failed",'message':"Record Not found",'data': "" }); return res.status(200).send({'status':"success",'message':"Fetched successfully",'data': data }); } catch (err) { logger.error(`Error on get establishment by Id: ${ err.message }`); logger.error(`Stack trace: ${err.stack}`); return res.status(500).send({'status':"failed",'message': "Internal server error" }); } }; // Update establishment exports.updateEstablishment = async (req, res) => { try { // convert empty "" → null BEFORE destructure [ "establishment_code", "factory_name", "permanent_factory_code", "industry_code", "industry_code_production", "license_number", "isic_code" ].forEach(key => { if (req.body[key] === "") req.body[key] = null; }); const { id } = req.params; const { establishment_products, establishment_user, ...estData } = req.body; const user = await EstablishmentUser.findOne({where:{establishment_id:id}}); const resolveActionDoneBy = (item) => { if (item?.action_done_by) return item.action_done_by; if (req.user.role === "Admin") return "admin_users"; return "establishment_users"; }; const establishment = await Establishment.findByPk(id); if (!establishment) { return res.status(404).json({ status: "failed", message: "Record not found" }); } // check duplicate establishment_code if (estData.establishment_code) { const exists = await Establishment.findOne({ where: { establishment_code: estData.establishment_code, id: { [Op.ne]: id } } }); if (exists) { return res.status(400).json({ status:"failed", message:"establishment_code already exists" }); } } estData.updated_by = estData.updated_by || req.user.id; estData.updated_at = estData.updated_at || new Date(); // update establishment await establishment.update(estData); if (req.body.is_active === true || req.body.is_active === false) { await EstablishmentUser.update( { is_active : req.body.is_active, updated_by: req.user.id, updated_at: new Date() }, { where: { establishment_id: id } } ); await EstablishmentProduct.update( { is_active : req.body.is_active, updated_by: req.user.id, updated_at: new Date() }, { where: { establishment_id: id } } ); } // update user if needed if (establishment_user) { if (user) { let updateUser = { name: establishment_user.name, email: establishment_user.email }; if (establishment_user.password) { updateUser.password = await bcrypt.hash(establishment_user.password,10); } updateUser.updated_by = updateUser.updated_by || req.user.id; updateUser.updated_at = updateUser.updated_at || new Date(); await user.update(updateUser); } } if (Array.isArray(establishment_products)) { const incoming = establishment_products; const incomingIds = incoming.map(e => e.product_id); const oldProducts = await EstablishmentProduct.findAll({ where: { establishment_id: id } }); const oldIds = oldProducts.map(e => e.product_id); // SOFT-REMOVE products not present anymore const removeIds = oldIds.filter(v => !incomingIds.includes(v)); if (removeIds.length > 0) { // find matching incoming data for audit fields const removedData = incoming.find( p => removeIds.includes(p.product_id) ); await EstablishmentProduct.update( { is_active: 0, updated_by: removedData?.updated_by , // action_done_by: removedData?.action_done_by || null, action_done_by: req.user.role === "Admin" ? "admin_users" : "establishment_users", updated_at: new Date() }, { where: { establishment_id: id, product_id: removeIds } } ); } // ADD new active products const newItems = incoming.filter(p => !oldIds.includes(p.product_id)); for (const item of newItems) { await EstablishmentProduct.create({ establishment_id: id, product_id: item.product_id, is_active: 1, created_by: item.created_by || null, // action_done_by: item.action_done_by, action_done_by: resolveActionDoneBy(item), created_at: new Date() }); } // UPDATE existing product metadata const updateItems = incoming.filter(p => oldIds.includes(p.product_id)); for (const item of updateItems) { await EstablishmentProduct.update( { is_active: 1, updated_by: item.updated_by || null, // action_done_by: item.action_done_by, action_done_by: resolveActionDoneBy(item), updated_at: new Date() }, { where: { establishment_id: id, product_id: item.product_id } } ); } } return res.json({ status:"success", message:"Updated successfully" }); } catch(err) { if (err.name === "SequelizeUniqueConstraintError") { const field = err.errors[0].path; return res.status(400).json({ status:"failed", message:`${field} already exists` }); } logger.error(`Error updating establishment: ${ err.message }`); logger.error(`Stack trace: ${err.stack}`); return res.status(500).json({ status:"failed", message: "Internal server error" }); } }; // Delete establishment exports.deleteEstablishment = async (req, res) => { try { const deleted = await Establishment.destroy({ where: { id: req.params.id } }); if (!deleted) return res.status(404).send({'status':"failed",'message':"Record Not found",'data': "" }); return res.status(200).send({'status':"success",'message':"Deleted successfully",'data': "" }); } catch (err) { logger.error(`Error deleting establishment: ${ err.message }`); return res.status(500).send({'status':"failed",'message': "Internal server error" }); } }; exports.getAllEmirates = async (req, res) => { try { const emirates = await Emirate.findAll({ attributes: ["id", "name", "code"], order: [["name", "ASC"]], }); return res.status(200).send({status: "success", message: "Emirates fetched successfully", data: emirates,}); } catch (err) { logger.error(`Error on get all emirates: ${ err.message }`); logger.error(`Stack trace: ${err.stack}`); return res.status(500).send({status: "failed",message: "Internal server error", }); } }; exports.getAllCityTowns = async (req, res) => { try { let { emirate_id } = req.query; if (emirate_id) { emirate_id = Number(emirate_id); if (isNaN(emirate_id)) { return res.status(400).send({ status: "failed", message: "Invalid emirate_id" }); } const emirateExists = await Emirate.findOne({ where: { id: emirate_id } }); if (!emirateExists) { return res.status(404).send({ status: "failed", message: "Emirate not found" }); } } const whereClause = {}; if (emirate_id) { whereClause.emirate_id = emirate_id; } const cities = await CityTown.findAll({ where: whereClause, attributes: ["id", "name", "emirate_id"], order: [["name", "ASC"]], }); const sanitizedCities = cities.map(city => ({ id: city.id, name: sanitizeStringValue(city.name), emirate_id: city.emirate_id, })); return res.status(200).send({status: "success", message: "City/Town fetched successfully", data: sanitizedCities, }); } catch (err) { logger.error(`Error getting all city towns: ${ err.message }`); logger.error(`Stack trace: ${err.stack}`); return res.status(500).send({ status: "failed", message: "Internal server error" }); } }; 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) { logger.error(`Error on getall request: ${ err.message }`); logger.error(`Stack trace: ${err.stack}`); res.status(500).json({ status: "failed", message: "Internal server error" }); } }; 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, created_by: req.user.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); logger.error(`Error on create request: ${ err.message }`); logger.error(`Stack trace: ${err.stack}`); res.status(500).json({ status: "failed", message: "Internal server error"}); } }; exports.forgotPasswordRequestOTP = async (req, res) => { try { const { user_type , establishment_name, establishment_code, registered_email } = req.body; if(user_type == 'establishment_user') { // 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.

` ); }else{ const adminUser = await user.findOne({where: { email: registered_email },}); if (!adminUser) return res.status(404).json({ status: "failed", message: "Admin user not found" }); // 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 adminUser.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 ${adminUser.name},

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

` ); } const safeEmail = sanitizeForLog(registered_email); logger.info(`OTP sent email: ${safeEmail}`); return res.status(200).json({ status: "success", message: "OTP sent successfully to registered email", }); } catch (err) { logger.error(err.message); logger.error(`Stack trace: ${err.stack}`); return res.status(500).json({ status: "failed", message: "Internal server error" }); } }; exports.forgotPasswordVerifyOTP = async (req, res) => { try { const { user_type , 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" }); if(user_type == 'establishment_user') { const user = await EstablishmentUser.scope("withSensitive").findOne({ where: { email: registered_email } }); console.log(user); if (!user || !user.reset_otp) return res.status(404).json({ status: "failed", message: "verification code 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: "Verification code has expired. Please request a new one" }); // Compare OTP const isOtpValid = await bcrypt.compare(otp, user.reset_otp); if (!isOtpValid) return res.status(400).json({ status: "failed", message: "Invalid verification code" }); // Update password const hashedPassword = await bcrypt.hash(password, 10); await user.update({ password: hashedPassword, reset_otp: null, reset_otp_expires_at: null, }); }else{ const adminUser = await user.scope("withSensitive").findOne({ where: { email: registered_email } }); if (!adminUser || !adminUser.reset_otp) return res.status(404).json({ status: "failed", message: "verification code not found or invalid user" }); // Check OTP expiry if (new Date() > new Date(adminUser.reset_otp_expires_at)) return res.status(400).json({ status: "failed", message: "Verification code has expired. Please request a new one" }); // Compare OTP const isOtpValid = await bcrypt.compare(otp, adminUser.reset_otp); if (!isOtpValid) return res.status(400).json({ status: "failed", message: "Invalid verification code" }); // Update password const hashedPassword = await bcrypt.hash(password, 10); await adminUser.update({ password: hashedPassword, reset_otp: null, reset_otp_expires_at: null, }); } const safeEmail = sanitizeForLog(registered_email); logger.info(`Password reset successful for user: ${safeEmail}`); return res.status(200).json({ status: "success", message: "Password reset successfully", }); } catch (err) { logger.error(err.message); logger.error(`Stack trace: ${err.stack}`); return res.status(500).json({ status: "failed", message: "Internal server error" }); } }; exports.requestOTPForLogin = async (req, res) => { try { const { registered_email } = req.body; if (!registered_email) { return res.status(400).json({ status: "failed", message: "Email is required", }); } // Check which model contains the user let loggingUser = await EstablishmentUser.findOne({ where: { email: registered_email } }); let userModel = EstablishmentUser; if (!loggingUser) { loggingUser = await User.findOne({ where: { email: registered_email, is_active: true } }); userModel = User; } if (!loggingUser) { logger.warn(`OTP requested for non-existing email: ${sanitizeForLog(registered_email)}`); return res.status(404).json({ status: "failed", message: "Invalid login request. Please check your email or register to continue.", }); } // Secure OTP generation const otp = crypto.randomInt(100000, 999999).toString(); // Hash OTP before storing const hashedOtp = await bcrypt.hash(otp, 10); // Store OTP in the correct model with WHERE clause await userModel.update( { login_otp: hashedOtp, login_otp_expires_at: new Date(Date.now() + 10 * 60 * 1000), }, { where: { email: registered_email } } ); const placeHolderData = { username: loggingUser.name, verfication_code: otp, support_email: process.env.SUPPORT_EMAIL, support_phone: process.env.SUPPORT_PHONE }; await sendEmailService(registered_email, "sign_in_verification_code", placeHolderData); logger.info(`Login OTP email triggered for: ${sanitizeForLog(registered_email)}`); return res.status(200).json({ status: "success", message: "OTP sent successfully to your registered email", }); } catch (err) { logger.error(`OTP request failed: ${err.message}`); logger.error(err.stack); return res.status(500).json({ status: "failed", message: "Internal Server Error", }); } }; exports.verifyOTPForLogin = async (req, res) => { try { const { registered_email, otp } = req.body; // Validation if (!registered_email || !otp) { return res.status(400).json({ status: "failed", message: "Email and OTP are required" }); } // Find user from either model let user = await EstablishmentUser.scope("withSensitive").findOne({ where: { email: registered_email } }); if (!user) { user = await User.scope("withSensitive").findOne({ where: { email: registered_email, is_active: true } }); } // User validation if (!user) { return res.status(404).json({ status: "failed", message: "User not found" }); } if (!user.login_otp) { return res.status(404).json({ status: "failed", message: "Verification code not found or invalid user" }); } // Check OTP expiry if (new Date() > new Date(user.login_otp_expires_at)) { return res.status(400).json({ status: "failed", message: "Verification code has expired. Please request a new one" }); } // Verify OTP const isOtpValid = await bcrypt.compare(otp, user.login_otp); if (!isOtpValid) { return res.status(400).json({ status: "failed", message: "Invalid verification code" }); } // Clear OTP after successful verification await user.update({ login_otp: null, login_otp_expires_at: null, }); const safeEmail = sanitizeForLog(registered_email); logger.info(`OTP verification successful for user: ${safeEmail}`); return res.status(200).json({ status: "success", message: "Email verified successfully!", }); } catch (err) { logger.error(`OTP verification error: ${err.message}`); logger.error(`Stack trace: ${err.stack}`); return res.status(500).json({ status: "failed", message: "Internal server error" }); } }; const GENERIC_ERROR_MSG = "Invalid data. Please check the instructions given and upload again."; function sanitizeFilePath(userInput, allowedDirectory) { if (!userInput || typeof userInput !== 'string') { throw new Error('Invalid file path input'); } let sanitized = userInput.replace(/\.\./g, ''); sanitized = sanitized.replace(/[\/\\]+/g, path.sep); const filename = path.basename(sanitized); const fullPath = path.join(allowedDirectory, filename); const resolvedPath = path.resolve(fullPath); const resolvedBase = path.resolve(allowedDirectory); if (!resolvedPath.startsWith(resolvedBase)) { throw new Error('Path traversal attempt detected'); } return resolvedPath; } function validateFileExists(filePath) { if (!filePath) { return false; } try { return fs.existsSync(filePath); } catch (err) { return false; } } /** * Safe file deletion with error handling */ function deleteFileSecure(filePath) { if (!filePath) { return; } try { if (validateFileExists(filePath)) { fs.unlinkSync(filePath); } } catch (err) { if (logger && logger.error) { logger.error('File deletion error: ' + err.message); logger.error(`Stack trace: ${err.stack}`); } } } exports.establishmentBulkUpload = async (req, res) => { let transaction = null; let sanitizedPath = null; try { // Check if file was uploaded if (!req.file) { return res.status(400).send({ status: "failed", message: "No file uploaded." }); } const { UPLOAD_DIR: ALLOWED_UPLOAD_DIR } = require('../config/upload.config'); try { sanitizedPath = sanitizeFilePath(req.file.path, ALLOWED_UPLOAD_DIR); } catch (sanitizeError) { try { const unsafePath = req.file.path; if (unsafePath && fs.existsSync(unsafePath)) { fs.unlinkSync(unsafePath); } } catch (cleanupErr) { // Silent fail on cleanup } return res.status(400).send({ status: "failed", message: "Invalid file path detected" }); } if (!validateFileExists(sanitizedPath)) { return res.status(400).send({ status: "failed", message: "File not found after validation" }); } const originalName = path.basename(req.file.originalname); const fileExt = path.extname(originalName).toLowerCase(); if (fileExt !== '.csv') { deleteFileSecure(sanitizedPath); return res.status(400).send({ status: "failed", message: "Invalid file type. Only CSV files are allowed." }); } const allowedMimeTypes = ['text/csv', 'application/csv', 'text/plain']; if (req.file.mimetype && !allowedMimeTypes.some(mime => mime === req.file.mimetype)) { deleteFileSecure(sanitizedPath); return res.status(400).send({ status: "failed", message: "Invalid MIME type. Only CSV files are allowed." }); } const rows = []; await new Promise((resolve, reject) => { fs.createReadStream(sanitizedPath) .pipe(csv()) .on("data", (rawRow) => { const row = {}; for (const key in rawRow) { if (!rawRow.hasOwnProperty(key)) continue; const normalizedKey = key .replace(/\*/g, "") .replace(/\([^)]*\)/g, "") .trim() .replace(/[\/\-\s]+/g, "_") .replace(/[^\w]+/g, "") .toLowerCase() .replace(/_{2,}/g, "_") .replace(/^_+|_+$/g, ""); row[normalizedKey] = rawRow[key] ? rawRow[key].trim() : ""; } const cleaned = Object.values(row).map(v => (v || "").replace(/\s+/g, "").trim()); const empty = cleaned.every(v => v === ""); if (!empty) rows.push(row); }) .on("end", resolve) .on("error", reject); }); if (rows.length === 0) { deleteFileSecure(sanitizedPath); return res.status(400).send({ status: "failed", message: "CSV file is empty." }); } // ------------------------------------------------------- // REQUIRED HEADERS // ------------------------------------------------------- const required = ["establishment_id", "factory_name", "user_name", "email", "emirate"]; const firstKeys = Object.keys(rows[0]); const missingHeaders = required.filter(h => !firstKeys.includes(h)); if (missingHeaders.length > 0) { deleteFileSecure(sanitizedPath); return res.status(400).send({ status: "failed", message: "Missing required columns: " + missingHeaders.join(", ") }); } // ------------------------------------------------------- // LOAD REFERENCE TABLES ONCE // ------------------------------------------------------- const emirates = await Emirate.findAll({ attributes: ["id", "name"] }); const emirateMap = {}; for (let i = 0; i < emirates.length; i++) { const e = emirates[i]; emirateMap[e.name.trim().toLowerCase()] = e.id; } const cityTowns = await CityTown.findAll({ attributes: ["id", "name"] }); const cityTownMap = {}; for (let i = 0; i < cityTowns.length; i++) { const ct = cityTowns[i]; cityTownMap[ct.name.trim().toLowerCase()] = ct.id; } function normalizeHS(val) { if (!val) return ""; return val.toString().normalize("NFKD").replace(/[^\d]/g, "").trim(); } const products = await Product.findAll({ attributes: ["id", "hs_code"] }); const productMap = {}; for (let i = 0; i < products.length; i++) { const p = products[i]; const c = normalizeHS(p.hs_code); if (c.length >= 1 && c.length <= 10) { productMap[c] = p.id; } } const existing = await Establishment.findAll({ attributes: [ "establishment_code", "factory_name", "establishment_contact_email", "industry_code_production", "permanent_factory_code", "industry_code" ] }); const existingEstSet = new Set(); const existingFactorySet = new Set(); const existingEmailSet = new Set(); const existingIndustryCodeSet = new Set(); const existingPermanentFactoryCodeSet = new Set(); const existingIndustryCodeBusinessSet = new Set(); for (let i = 0; i < existing.length; i++) { const e = existing[i]; existingEstSet.add(e.establishment_code); existingFactorySet.add((e.factory_name || "").toLowerCase()); existingEmailSet.add((e.establishment_contact_email || "").toLowerCase()); if (e.industry_code_production) existingIndustryCodeSet.add(e.industry_code_production); if (e.permanent_factory_code) existingPermanentFactoryCodeSet.add(e.permanent_factory_code); if (e.industry_code) existingIndustryCodeBusinessSet.add(e.industry_code); } // FILE duplicate trackers const fileEstSet = new Set(); const fileFactorySet = new Set(); const fileEmailSet = new Set(); const fileIndustryCodeSet = new Set(); const filePermanentFactoryCodeSet = new Set(); const fileIndustryCodeBusinessSet = new Set(); const errors = []; const prepared = []; for (let i = 0; i < rows.length; i++) { const r = rows[i]; const rowNum = i + 2; const est = r.establishment_id; const factory = r.factory_name; const email = r.email; const emirate = r.emirate; const missing = []; if (!est) missing.push("Establishment ID"); if (!factory) missing.push("Factory Name"); if (!r.user_name) missing.push("User Name"); if (!email) missing.push("Email"); if (!emirate) missing.push("Emirate"); if (missing.length) { errors.push({ row: rowNum, error: `Missing required fields: ${missing.join(", ")}` }); continue; } if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { errors.push({ row: rowNum, error: "Invalid Email format" }); continue; } // FILE-level duplicates const estKey = est.trim(); const factoryKey = factory.trim().toLowerCase(); const emailKey = email.trim().toLowerCase(); if (fileEstSet.has(estKey)) { errors.push({ row: rowNum, error: `Duplicate Establishment ID in file: ${est}` }); continue; } if (fileFactorySet.has(factoryKey)) { errors.push({ row: rowNum, error: `Duplicate Factory Name in file: ${factory}` }); continue; } if (fileEmailSet.has(emailKey)) { errors.push({ row: rowNum, error: `Duplicate Email in file: ${email}` }); continue; } // Check industry_code_production duplicates (if provided) const industryCodeProd = r.industry_code_current_production ? r.industry_code_current_production.trim() : null; if (industryCodeProd) { if (fileIndustryCodeSet.has(industryCodeProd)) { errors.push({ row: rowNum, error: `Duplicate Industry Code (Current Production) in file: ${industryCodeProd}` }); continue; } if (existingIndustryCodeSet.has(industryCodeProd)) { errors.push({ row: rowNum, error: `Industry Code (Current Production) already exists in database: ${industryCodeProd}` }); continue; } fileIndustryCodeSet.add(industryCodeProd); } const permanentFactoryCode = r.permanent_factory_code ? r.permanent_factory_code.trim() : null; if (permanentFactoryCode) { if (filePermanentFactoryCodeSet.has(permanentFactoryCode)) { errors.push({ row: rowNum, error: `Duplicate Permanent Factory Code in file: ${permanentFactoryCode}` }); continue; } if (existingPermanentFactoryCodeSet.has(permanentFactoryCode)) { errors.push({ row: rowNum, error: `Permanent Factory Code already exists in database: ${permanentFactoryCode}` }); continue; } filePermanentFactoryCodeSet.add(permanentFactoryCode); } // Check industry_code_business_register duplicates (if provided) const industryCodeBusiness = r.industry_code_business_register ? r.industry_code_business_register.trim() : null; if (industryCodeBusiness) { if (fileIndustryCodeBusinessSet.has(industryCodeBusiness)) { errors.push({ row: rowNum, error: `Duplicate Industry Code (Business Register) in file: ${industryCodeBusiness}` }); continue; } if (existingIndustryCodeBusinessSet.has(industryCodeBusiness)) { errors.push({ row: rowNum, error: `Industry Code (Business Register) already exists in database: ${industryCodeBusiness}` }); continue; } fileIndustryCodeBusinessSet.add(industryCodeBusiness); } fileEstSet.add(estKey); fileFactorySet.add(factoryKey); fileEmailSet.add(emailKey); if (existingEstSet.has(estKey)) { errors.push({ row: rowNum, error: `Establishment ID already exists: ${est}` }); continue; } if (existingFactorySet.has(factoryKey)) { errors.push({ row: rowNum, error: `Factory Name already exists: ${factory}` }); continue; } if (existingEmailSet.has(emailKey)) { errors.push({ row: rowNum, error: `Email already exists: ${email}` }); continue; } // Emirate validation const emirateId = emirateMap[emirate.toLowerCase()]; if (!emirateId) { errors.push({ row: rowNum, error: `Invalid Emirate: ${emirate}` }); continue; } // HS Code validation const hsCols = Object.keys(r).filter(k => k.startsWith("hs")); const hsRaw = hsCols.map(k => r[k]).filter(Boolean); if (hsRaw.length === 0) { errors.push({ row: rowNum, error: "At least one HS code is required" }); continue; } const cleaned = hsRaw.map(h => normalizeHS(h)); const invalid = cleaned.filter(c => !productMap[c]); if (invalid.length > 0) { errors.push({ row: rowNum, error: `Invalid HS Code(s): ${invalid.join(", ")}` }); continue; } prepared.push({ r: r, rowNum: rowNum, est: est, factory: factory, email: email, emirateId: emirateId, productIds: cleaned.map(c => productMap[c]) }); } if (errors.length > 0) { deleteFileSecure(sanitizedPath); return res.status(400).send({ status: "failed", message: "Invalid data. Please check and upload again.", errors: errors }); } //CITY/TOWN VALIDATION (OPTIONAL BUT MUST BE VALID) for (let j = 0; j < prepared.length; j++) { const p = prepared[j]; const r = p.r; const cityTownValue = r.city_town ? r.city_town.trim().toLowerCase() : ""; if (cityTownValue) { const id = cityTownMap[cityTownValue]; if (!id) { deleteFileSecure(sanitizedPath); return res.status(400).send({ status: "failed", message: "Invalid City/Town found. Upload stopped.", errors: [{ row: p.rowNum, error: `Invalid City/Town: ${r.city_town}` }] }); } r.city_town_id = id; } else { r.city_town_id = null; } } transaction = await sequelize.transaction(); for (let k = 0; k < prepared.length; k++) { const p = prepared[k]; const r = p.r; const est = await Establishment.create({ establishment_code: p.est, license_number: p.est, factory_name: p.factory, establishment_contact_email: p.email, establishment_emirate_id: p.emirateId, permanent_factory_code: r.permanent_factory_code || null, industry_code: r.industry_code_business_register || null, industry_code_production: r.industry_code_current_production || null, industry_code_mismatch_remarks: r.industry_code_mismatch_remarks || null, description: r.description || null, establishment_address: r.establishment_address || null, establishment_city_town_id: r.city_town_id || null, establishment_postal_code: r.postal_code || null, establishment_po_box: r.po_box || null, establishment_makani_number: r.makani_number || null, establishment_contact_person_name: r.contact_person_name || null, establishment_contact_person_designation: r.contact_person_designation || null, establishment_mobile_number: r.mobile_number || null, establishment_website: r.website || null, emirati_male: Number(r.number_of_emirati_male || 0), emirati_female: Number(r.number_of_emirati_female || 0), non_emirati_male: Number(r.number_of_non_emirati_male || 0), non_emirati_female: Number(r.number_of_non_emirati_female || 0), total_emirati: Number(r.number_of_emirati_male || 0) + Number(r.number_of_emirati_female || 0), total_employees: Number(r.number_of_emirati_male || 0) + Number(r.number_of_emirati_female || 0) + Number(r.number_of_non_emirati_male || 0) + Number(r.number_of_non_emirati_female || 0), created_by: req.user.id }, { transaction: transaction }); const generateSecurePassword = (length = 12) => { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$%'; let password = ''; const randomBytes = crypto.randomBytes(length); for (let i = 0; i < length; i++) { password += chars[randomBytes[i] % chars.length]; } return password; }; const autoPassword = generateSecurePassword(12); const hashed = await bcrypt.hash(autoPassword, 10); await EstablishmentUser.create({ establishment_id: est.id, name: r.user_name, email: p.email, password: hashed, created_by: req.user.id }, { transaction: transaction }); const placeHolderData = { contact_name: r.user_name, portal_url: process.env.FE_BASE_URL, username: p.email, password: autoPassword, support_email: process.env.SUPPORT_EMAIL, support_phone: process.env.SUPPORT_PHONE }; await sendEmailService(p.email, "establishment_user_creation_to_user", placeHolderData); const productInserts = []; for (let m = 0; m < p.productIds.length; m++) { productInserts.push({ establishment_id: est.id, product_id: p.productIds[m], created_by: req.user.id }); } await EstablishmentProduct.bulkCreate(productInserts, { transaction: transaction }); } await transaction.commit(); transaction = null; deleteFileSecure(sanitizedPath); return res.status(200).send({ status: "success", message: `${prepared.length} establishments inserted successfully.`, summary: { total_records: rows.length, imported: prepared.length, errors: [] } }); } catch (err) { if (transaction) { try { await transaction.rollback(); if (logger && logger.error) { logger.error("Transaction rolled back successfully due to error"); } } catch (rollbackErr) { if (logger && logger.error) { logger.error("Rollback error: " + rollbackErr.message); } } } deleteFileSecure(sanitizedPath); if (logger && logger.error) { logger.error("Fatal Error in bulk upload: " + err.message); if (err.name) { logger.error("Error Name: " + err.name); } } // Handle Sequelize validation errors if (err.name === "SequelizeUniqueConstraintError" || err.name === "SequelizeValidationError") { if (logger && logger.error) { logger.error("Validation Errors:"); if (err.errors && Array.isArray(err.errors)) { for (let i = 0; i < err.errors.length; i++) { const validationError = err.errors[i]; logger.error(` - Field: ${validationError.path}, Value: ${validationError.value}, Message: ${validationError.message}`); } } } const errorList = []; if (err.errors && Array.isArray(err.errors)) { for (let i = 0; i < err.errors.length; i++) { const e = err.errors[i]; errorList.push({ field: e.path, value: e.value, message: e.message }); } } return res.status(400).send({ status: "failed", message: "Database validation error: " + (err.errors && err.errors[0] ? err.errors[0].message : err.message), errors: errorList }); } // Handle foreign key constraint errors if (err.name === "SequelizeForeignKeyConstraintError") { if (logger && logger.error) { logger.error("Foreign Key Constraint Error: " + err.message); } return res.status(400).send({ status: "failed", message: "Foreign key constraint error. Please check your reference data.", error: err.message }); } if (process.env.NODE_ENV === 'development') { if (logger && logger.error) { logger.error("Stack trace: " + err.stack); } } return res.status(500).send({ status: "failed", message: "Unexpected error occurred during bulk upload" }); } }; exports.downloadCompanyProfileSample = async (req, res) => { try { const safeBasePath = path.resolve(__dirname, "../downloads_csv"); const safeFilePath = path.join(safeBasePath, "company_profile_upload_sample.csv"); // Verify file exists BEFORE sending if (!fs.existsSync(safeFilePath)) { return res.status(404).send({ status: "failed", message: "File not found", }); } return res.download(safeFilePath, "company_profile_upload_sample.csv"); } catch (error) { logger.error('Error on downloading company profile sample file' + error.message ); logger.error(`Stack trace: ${error.stack}`); return res.status(500).send({ status: "failed", message: "Internal server error" }); } };