Website : rimsha.abasa.com
backdoor
Home
Console
Upload
information
Create File
Create Folder
About
Tools
:
/
var
/
www
/
mudeerapi.abasa.com
/
nodetest
/
src
/
controllers
/
Filename :
user.controller.js
back
Copy
import bcrypt from 'bcrypt'; import jwt from 'jsonwebtoken'; import mongoose from 'mongoose'; import { user } from '../models/user.models.js'; import { Department } from '../models/department.model.js'; import nodemailer from "nodemailer"; import transporter from '../config/transporter.js'; import { sendLoginNotification } from '../services/notificationService.js'; import { convertToUsd } from '../services/currencyService.js'; import { S3Client, DeleteObjectCommand } from '@aws-sdk/client-s3'; import attendance from '../models/attendance.model.js'; import UserStatus from '../models/userStatus.model.js'; import { Notification } from '../models/notification.model.js'; import EventActivity from '../models/eventActivity.models.js'; import activeWindow from '../models/activeWindow.models.js'; import { invalidateCache } from '../utils/invalidationHelper.js'; import { redisClient } from '../db/redis.js'; import { blacklistUserTokens } from '../utils/tokenBlacklist.js'; import crypto from 'crypto'; import dotenv from "dotenv"; dotenv.config({ path: `.env` }); const s3Client = new S3Client({ region: process.env.AWS_REGION, credentials: { accessKeyId: process.env.AWS_ACCESS_KEY_ID, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, }, }); export const signup = async (req, res, next) => { try { const allowedRoles = ['super_admin', 'admin', 'department_head', 'employee']; if (!allowedRoles.includes(req.body.role)) { return res.status(400).json({ message: "Invalid role" }); } // Check if email already exists const existingUser = await user.findOne({ email: req.body.email }); if (existingUser) { return res.status(400).json({ message: "Email already in use" }); } // Validate department if provided if (req.body.department) { if (!mongoose.Types.ObjectId.isValid(req.body.department)) { return res.status(400).json({ message: "Invalid department ID format" }); } const department = await Department.findById(req.body.department); if (!department) { return res.status(400).json({ message: "Department not found" }); } // If user is going to be a department head, check if department already has a head if (req.body.role === 'department_head') { if (department.head) { return res.status(400).json({ message: "Department already has a head assigned" }); } } } // Hash password const hash = await bcrypt.hash(req.body.password, 10); // Create new user const newUser = new user({ _id: new mongoose.Types.ObjectId(), first_name: req.body.first_name, last_name: req.body.last_name, gender: req.body.gender, email: req.body.email, password: hash, number: req.body.number, Nationality: req.body.Nationality, role: req.body.role?.toLowerCase(), department: req.body.department, day_off: req.body.day_off, workinghours: req.body.workinghours, break_duration: req.body.break_duration, start_time: req.body.start_time, end_time: req.body.end_time }); const savedUser = await newUser.save(); // Add user to department if department is provided if (req.body.department) { // If user is a department head, also set them as head of the department if (req.body.role === 'department_head') { await Department.findByIdAndUpdate( req.body.department, { head: savedUser._id } ); // Enforce invariant: head must never also be in employees[] await Department.findByIdAndUpdate( req.body.department, { $pull: { employees: savedUser._id } } ); } else { // Add non-head users to department's employees array (use $addToSet to prevent duplicates) await Department.findByIdAndUpdate( req.body.department, { $addToSet: { employees: savedUser._id } } ); } } res.status(200).json({ newUser: savedUser }); } catch (err) { console.log(err); res.status(500).json({ error: err.message || err }); } }; export const updateUser = (req, res, next) => { const userId = req.params.id; // assuming the user ID is passed as a route parameter const rawRequestedRole = req.body.role; const requestedRole = rawRequestedRole ? rawRequestedRole.toLowerCase() : null; // Determine acting user's role from token (already validated by middleware) let actingRole = null; try { const headerToken = req.headers.authorization && req.headers.authorization.split(" ")[1]; if (headerToken) { const decoded = jwt.verify(headerToken, "this is dummy text"); actingRole = decoded.role; } } catch (e) { // If token is invalid here, let the existing middleware/flow handle auth errors } user.findById(userId) .then(existingUser => { if (!existingUser) { return res.status(404).json({ message: "User not found" }); } // Email is immutable: never allow changing or wiping it from settings updates. if (Object.prototype.hasOwnProperty.call(req.body, 'email')) { const requestedEmail = req.body.email; if (requestedEmail && requestedEmail !== existingUser.email) { return res.status(400).json({ message: "Email cannot be changed" }); } } // Only reject when client is trying to SET role TO department_head (not when preserving it or omitting role) if (requestedRole === 'department_head' && existingUser.role !== 'department_head') { return res.status(400).json({ message: 'Role cannot be set to department head from settings. Assign department head from the department page.' }); } // If role is provided, only allow employee/admin/super_admin, or department_head when preserving existing if (requestedRole && requestedRole !== 'department_head') { const allowedUpdateRoles = ['employee', 'admin', 'super_admin']; if (!allowedUpdateRoles.includes(requestedRole)) { return res.status(400).json({ message: 'Role can only be set to employee, admin, or super_admin. To assign Department Head, use the Department screen.' }); } } // Enforce who can change which roles if (requestedRole && actingRole === 'admin') { // Admins cannot change super_admin users at all if (existingUser.role === 'super_admin') { return res.status(403).json({ message: 'Admins cannot modify super_admin users. Only super_admin can update them.' }); } // Admins cannot assign super_admin role if (requestedRole === 'super_admin') { return res.status(403).json({ message: 'Admins can only change roles to employee or admin. Only super_admin can assign the super_admin role.' }); } } // Only update department when explicitly sent (avoid wiping department when client omits it, e.g. after promoting to head) const hasDepartmentKey = Object.prototype.hasOwnProperty.call(req.body, 'department'); const rawDept = req.body.department; const department = hasDepartmentKey ? (rawDept && rawDept !== 'none' && mongoose.Types.ObjectId.isValid(rawDept) ? rawDept : null) : undefined; // Prepare an object with updated fields. // Only set fields explicitly provided to avoid wiping values with `undefined`. // NOTE: email is intentionally excluded (immutable). const updatedData = {}; const allowedFields = [ 'first_name', 'last_name', 'gender', 'number', 'Nationality', 'day_off', 'workinghours', 'break_duration', 'start_time', 'end_time', 'joined_on', ]; for (const f of allowedFields) { if (Object.prototype.hasOwnProperty.call(req.body, f) && req.body[f] !== undefined) { updatedData[f] = req.body[f]; } } if (Object.prototype.hasOwnProperty.call(updatedData, 'joined_on')) { const raw = updatedData.joined_on; const parsed = raw instanceof Date ? raw : new Date(String(raw)); if (Number.isNaN(parsed.getTime())) { return res.status(400).json({ message: 'Invalid joined_on date' }); } // Disallow today or any future date (compare by UTC day boundary). const todayStartUtc = new Date(); todayStartUtc.setUTCHours(0, 0, 0, 0); const joinedStartUtc = new Date(parsed); joinedStartUtc.setUTCHours(0, 0, 0, 0); if (joinedStartUtc.getTime() >= todayStartUtc.getTime()) { return res.status(400).json({ message: 'joined_on cannot be today or in the future' }); } updatedData.joined_on = parsed; } if (requestedRole) { updatedData.role = requestedRole; } if (hasDepartmentKey) { updatedData.department = department; } const prevDeptId = existingUser.department ? existingUser.department.toString() : null; const nextDeptId = hasDepartmentKey ? (department ? String(department) : null) : null; // Keep Department.head consistent when demoting a department head from Settings. // Otherwise Department pages will still show them as "Head" because department.head points to them. const roleChanged = Boolean(requestedRole && requestedRole !== existingUser.role); const demotingDeptHead = Boolean(existingUser.role === 'department_head' && requestedRole && requestedRole !== 'department_head'); const roleSideEffects = Promise.resolve() .then(async () => { if (demotingDeptHead) { await Department.updateMany( { head: existingUser._id }, { $unset: { head: 1 } } ); } }) .then(async () => { // If department is explicitly changed/cleared, keep Department docs consistent. if (!hasDepartmentKey) return; // Remove from previous department employees and head (if applicable) when moving away / clearing. if (prevDeptId && prevDeptId !== nextDeptId) { await Department.updateOne( { _id: prevDeptId }, { $pull: { employees: existingUser._id } } ); await Department.updateOne( { _id: prevDeptId, head: existingUser._id }, { $unset: { head: 1 } } ); } // If assigning to a new department, add to employees unless they are (or will become) a department head. if (nextDeptId) { const roleAfter = requestedRole || existingUser.role; if (roleAfter !== 'department_head') { await Department.updateOne( { _id: nextDeptId }, { $addToSet: { employees: existingUser._id } } ); } // Enforce invariant: head must never also be in employees[] const nextDept = await Department.findById(nextDeptId).select('head').lean(); if (nextDept?.head) { await Department.updateOne( { _id: nextDeptId }, { $pull: { employees: nextDept.head } } ); } } }) .then(async () => { if (roleChanged) { await blacklistUserTokens(existingUser._id); } }) .then(async () => { if (roleChanged || demotingDeptHead) { await invalidateCache.user(existingUser._id); } }); // Hash password if it's being updated if (req.body.password) { bcrypt.hash(req.body.password, 10, (err, hash) => { if (err) { return res.status(500).json({ error: err }); } else { updatedData.password = hash; // Update user with hashed password roleSideEffects .then(() => user.findByIdAndUpdate(userId, { $set: updatedData }, { new: true })) .then(result => { if (!result) { return res.status(404).json({ message: "User not found" }); } res.status(200).json({ updatedUser: result }); }) .catch(err => { console.log(err); res.status(500).json({ error: err }); }); } }); } else { // Update user without changing password roleSideEffects .then(() => user.findByIdAndUpdate(userId, { $set: updatedData }, { new: true })) .then(result => { if (!result) { return res.status(404).json({ message: "User not found" }); } res.status(200).json({ updatedUser: result }); }) .catch(err => { console.log(err); res.status(500).json({ error: err }); }); } }) .catch(err => { console.log(err); res.status(500).json({ error: err }); }); }; export const updateFinancialSettings = async (req, res) => { try { const userId = req.params.id; if (!mongoose.Types.ObjectId.isValid(userId)) { return res.status(400).json({ message: 'Invalid user ID' }); } const token = req.headers.authorization?.split(' ')[1]; if (!token) return res.status(401).json({ message: 'Unauthorized' }); const decoded = jwt.verify(token, 'this is dummy text'); const currentUserDoc = await user.findOne({ email: decoded.email }).select('_id role').lean(); if (!currentUserDoc) return res.status(401).json({ message: 'Unauthorized' }); const canUpdate = currentUserDoc._id.toString() === userId || (currentUserDoc.role === 'admin' || currentUserDoc.role === 'super_admin'); if (!canUpdate) return res.status(403).json({ message: 'Access denied' }); const target = await user .findById(userId) .select('baseSalaryAmount baseSalaryCurrency bankAccountName bankName bankAccountNumber bankSwiftCode bankRoutingNumber bankAccountType bankAddress financialCustomFields') .lean(); if (!target) return res.status(404).json({ message: 'User not found' }); const financialFields = [ 'baseSalaryAmount', 'baseSalaryCurrency', 'bankAccountName', 'bankName', 'bankAccountNumber', 'bankSwiftCode', 'bankRoutingNumber', 'bankAccountType', 'bankAddress' ]; const updatedData = {}; financialFields.forEach((f) => { if (req.body[f] !== undefined) updatedData[f] = req.body[f]; }); // Custom key/value fields for Salary & Bank Details if (req.body.customFields !== undefined) { const raw = req.body.customFields; if (raw === null) { updatedData.financialCustomFields = {}; } else { const isPlainObject = typeof raw === 'object' && raw !== null && !Array.isArray(raw) && (Object.getPrototypeOf(raw) === Object.prototype || Object.getPrototypeOf(raw) === null); if (!isPlainObject) { return res.status(400).json({ message: 'customFields must be an object of { key: value }' }); } const entries = Object.entries(raw); const MAX_FIELDS = 50; if (entries.length > MAX_FIELDS) { return res.status(400).json({ message: `customFields cannot exceed ${MAX_FIELDS} entries` }); } const sanitized = {}; for (const [k, v] of entries) { const key = String(k ?? '').trim(); const value = String(v ?? '').trim(); if (!key) { return res.status(400).json({ message: 'customFields keys cannot be empty' }); } if (key.length > 100) { return res.status(400).json({ message: 'customFields key must be <= 100 chars' }); } if (value.length > 100) { return res.status(400).json({ message: 'customFields value must be <= 100 chars' }); } sanitized[key] = value; } updatedData.financialCustomFields = sanitized; } } if (updatedData.baseSalaryCurrency !== undefined) { updatedData.baseSalaryCurrency = String(updatedData.baseSalaryCurrency).trim().toUpperCase(); } if (updatedData.baseSalaryAmount !== undefined && updatedData.baseSalaryAmount != null) { const n = Number(updatedData.baseSalaryAmount); if (Number.isFinite(n) && n < 0) { return res.status(400).json({ message: 'baseSalaryAmount cannot be negative' }); } } if (updatedData.baseSalaryAmount !== undefined || updatedData.baseSalaryCurrency !== undefined) { const amount = updatedData.baseSalaryAmount !== undefined ? updatedData.baseSalaryAmount : target.baseSalaryAmount; const currency = (updatedData.baseSalaryCurrency || target.baseSalaryCurrency || 'USD').toString().trim().toUpperCase(); if (currency === 'USD') { updatedData.baseSalaryUsd = amount != null ? Number(amount) : null; } else if (amount != null && currency) { try { const { amountUsd } = await convertToUsd(Number(amount), currency); updatedData.baseSalaryUsd = amountUsd; } catch (e) { console.warn('Currency conversion failed for financial settings:', e.message); } } } // Section is optional overall, but if any financial input exists (salary/bank/custom), // require salary amount + currency + account name + bank name + account number. const after = { ...(target || {}), ...(updatedData || {}) }; const salaryAmountNumber = after.baseSalaryAmount == null ? null : Number(after.baseSalaryAmount); const salaryAmountIsSet = salaryAmountNumber != null && Number.isFinite(salaryAmountNumber) && salaryAmountNumber > 0; const currencyAfter = String(after.baseSalaryCurrency ?? '').trim(); const bankAccountNameAfter = String(after.bankAccountName ?? '').trim(); const bankNameAfter = String(after.bankName ?? '').trim(); const bankAccountNumberAfter = String(after.bankAccountNumber ?? '').trim(); const anyOtherBankFieldFilled = Boolean(String(after.bankSwiftCode ?? '').trim()) || Boolean(String(after.bankRoutingNumber ?? '').trim()) || Boolean(String(after.bankAccountType ?? '').trim()) || Boolean(String(after.bankAddress ?? '').trim()); const customAfter = after.financialCustomFields; const customEntries = customAfter && typeof customAfter === 'object' ? Object.entries(customAfter) : []; const anyCustomFieldFilled = customEntries.some(([k, v]) => String(k ?? '').trim() && String(v ?? '').trim()); const hasAnyFinancialInput = salaryAmountIsSet || Boolean(currencyAfter) || Boolean(bankAccountNameAfter) || Boolean(bankNameAfter) || Boolean(bankAccountNumberAfter) || anyOtherBankFieldFilled || anyCustomFieldFilled; if (hasAnyFinancialInput) { if (!salaryAmountIsSet) { return res.status(400).json({ message: 'baseSalaryAmount is required when saving salary/bank details' }); } if (!currencyAfter) { return res.status(400).json({ message: 'baseSalaryCurrency is required when saving salary/bank details' }); } if (!bankAccountNameAfter) { return res.status(400).json({ message: 'bankAccountName is required when saving salary/bank details' }); } if (!bankNameAfter) { return res.status(400).json({ message: 'bankName is required when saving salary/bank details' }); } if (!bankAccountNumberAfter) { return res.status(400).json({ message: 'bankAccountNumber is required when saving salary/bank details' }); } } if (Object.keys(updatedData).length === 0) { return res.status(400).json({ message: 'No financial fields to update' }); } const updated = await user.findByIdAndUpdate( userId, { $set: updatedData }, { new: true } ).select('-password').lean(); return res.status(200).json({ updatedUser: updated }); } catch (err) { if (err.name === 'JsonWebTokenError') return res.status(401).json({ message: 'Unauthorized' }); console.error(err); return res.status(500).json({ error: err.message || 'Server error' }); } }; export const test = (req, res, next) => { res.status(200).json({ message: 'Test route working - new ci pipeline' }); }; export const login = (req, res, next) => { // Validate email and password const { email, password } = req.body; // Check if both are empty if (!email && !password) { return res.status(400).json({ message: "Enter your email and password", errors: { email: "Enter your email", password: "Enter your password" } }); } // Check if email is empty if (!email) { return res.status(400).json({ message: "Enter your email", errors: { email: "Enter your email" } }); } // Check if password is empty if (!password) { return res.status(400).json({ message: "Enter your password", errors: { password: "Enter your password" } }); } user.find({ email: req.body.email }) .exec() .then(users => { if (users.length < 1) { return res.status(404).json({ message: "User does not exist" }); } // Check if user is active if (users[0].isActive === false) { return res.status(403).json({ message: "Your account has been deactivated. Please contact an administrator." }); } bcrypt.compare(req.body.password, users[0].password, (err, result) => { if (!result) { return res.status(401).json({ message: "Incorrect password" }); } if (result) { const token = jwt.sign({ _id: users[0]._id, first_name: users[0].first_name, email: users[0].email, last_name: users[0].last_name, role: users[0].role, gender: users[0].gender, department: users[0].department }, "this is dummy text", { expiresIn: "10d" }); res.status(200).json({ _id: users[0]._id, first_name: users[0].first_name, email: users[0].email, last_name: users[0].last_name, gender: users[0].gender, number: users[0].number, Nationality: users[0].Nationality, role: users[0].role, department: users[0].department, day_off: users[0].day_off, workinghours: users[0].workinghours, start_time: users[0].start_time, end_time: users[0].end_time, joined_on: users[0].joined_on ?? new Date('2026-01-01T00:00:00.000Z'), token: token }); } }); }) .catch(err => { res.status(500).json({ error: err }); }); }; export const getUsers = async (req, res, next) => { try { let query = {}; const token = req.headers.authorization.split(" ")[1]; const decodedToken = jwt.verify(token, "this is dummy text"); // different query filter for department_head if (decodedToken.role === 'department_head') { if (!decodedToken.department) { return res.status(400).json({ message: "Department head has no department assigned" }); } query = { department: new mongoose.Types.ObjectId(decodedToken.department) }; } // filter for employee role - only return their own info if (decodedToken.role === 'employee') { query = { email: decodedToken.email }; } // Normalize/validate query params. Avoid letting employee tokens override self-filter. const isEmployeeToken = decodedToken.role === 'employee'; const allowedRoles = new Set(['super_admin', 'admin', 'department_head', 'employee']); if (!isEmployeeToken && req.query.role) { const roleParam = String(req.query.role).toLowerCase().trim(); if (!allowedRoles.has(roleParam)) { return res.status(400).json({ message: `Invalid role filter: ${req.query.role}` }); } query.role = roleParam; } if (!isEmployeeToken && req.query.department) { query.department = req.query.department; } // Handle isActive filter if (req.query.isActive !== undefined) { const isActiveValue = req.query.isActive === 'true' || req.query.isActive === true; query.isActive = isActiveValue; } // Get today's date range for attendance lookup (UTC) const todayStart = new Date(); todayStart.setUTCHours(0, 0, 0, 0); const todayEnd = new Date(); todayEnd.setUTCHours(23, 59, 59, 999); // Aggregate users with their status information const usersWithStatus = await user.aggregate([ { $match: query }, // Join with department collection { $lookup: { from: 'departments', localField: 'department', foreignField: '_id', as: 'departmentInfo' } }, // Add department name field { $addFields: { departmentName: { $arrayElemAt: ['$departmentInfo.name', 0] } } }, // Join with attendance collection for today's record { $lookup: { from: 'attendances', let: { userId: { $toString: '$_id' } }, pipeline: [ { $match: { $expr: { $eq: ['$user_id', '$$userId'] }, createdAt: { $gte: todayStart, $lte: todayEnd } } }, { $sort: { createdAt: -1 } }, { $limit: 1 } ], as: 'todayAttendance' } }, // Join with userStatus collection { $lookup: { from: 'userstatuses', let: { userId: '$_id' }, pipeline: [ { $match: { $expr: { $eq: ['$user_id', '$$userId'] } } } ], as: 'status' } }, // Unwind status array to a single object (or null) { $addFields: { status: { $arrayElemAt: ['$status', 0] }, attendanceStatus: { $arrayElemAt: ['$todayAttendance.attendanceStatus', 0] }, checkInTime: { $arrayElemAt: ['$todayAttendance.createdAt', 0] }, // New fields for checkout and break (backwards compatible - defaults to null/0) checkOutTime: { $arrayElemAt: ['$todayAttendance.checkOut', 0] }, totalWorkingHours: { $ifNull: [{ $arrayElemAt: ['$todayAttendance.totalWorkingHours', 0] }, null] }, breakDuration: { $ifNull: [{ $arrayElemAt: ['$todayAttendance.breakDuration', 0] }, 0] }, activeBreakStartTime: { $let: { vars: { activeBreak: { $arrayElemAt: [ { $filter: { input: { $ifNull: [{ $arrayElemAt: ['$todayAttendance.breakSessions', 0] }, []] }, as: 'session', cond: { $or: [ { $eq: ['$$session.endTime', null] }, { $eq: [{ $type: '$$session.endTime' }, 'null'] } ] } } }, 0 ] } }, in: { $cond: { if: { $ne: ['$$activeBreak', null] }, then: '$$activeBreak.startTime', else: null } } } }, isOnBreak: { $cond: [ { $eq: [{ $arrayElemAt: ['$status.currentStatus', 0] }, 'break'] }, true, false ] } } }, { $addFields: { joined_on: { $ifNull: ['$joined_on', new Date('2026-01-01T00:00:00.000Z')] } } }, { $project: { password: 0, departmentInfo: 0, todayAttendance: 0 } } ]); res.status(200).json({ users: usersWithStatus }); } catch (err) { console.error(err); res.status(500).json({ error: err }); } }; export const getEmployeeById = async (req, res, next) => { try { const id = req.params.id; if (!mongoose.Types.ObjectId.isValid(id)) { return res.status(400).json({ message: 'Invalid ID format' }); } const token = req.headers.authorization.split(" ")[1]; const decodedToken = jwt.verify(token, "this is dummy text"); if (decodedToken.role === 'department_head') { if (!decodedToken.department) { return res.status(400).json({ message: "Department head has no department assigned" }); } // Check if the requested user belongs to the department head's department const requestedUser = await user.findById(id); if (!requestedUser) { return res.status(404).json({ message: 'User not found' }); } if (requestedUser.department?.toString() !== decodedToken.department) { return res.status(403).json({ message: 'Access denied. User not in your department' }); } } else if (decodedToken.role === 'employee') { // Employees can only view their own profile if (decodedToken.email) { const requestedUser = await user.findById(id); if (!requestedUser) { return res.status(404).json({ message: 'User not found' }); } if (requestedUser.email !== decodedToken.email) { return res.status(403).json({ message: 'Access denied. You can only view your own profile' }); } } else { return res.status(403).json({ message: 'Access denied' }); } } // super_admin and admin can access any employee (no additional checks needed) // Get today's date range for attendance lookup (UTC) const todayStart = new Date(); todayStart.setUTCHours(0, 0, 0, 0); const todayEnd = new Date(); todayEnd.setUTCHours(23, 59, 59, 999); const foundUser = await user.aggregate([ { $match: { _id: new mongoose.Types.ObjectId(id) } }, // Join with department collection { $lookup: { from: 'departments', localField: 'department', foreignField: '_id', as: 'departmentInfo' } }, // Add department name field { $addFields: { departmentName: { $arrayElemAt: ['$departmentInfo.name', 0] } } }, // Join with attendance collection for today's record { $lookup: { from: 'attendances', let: { userId: { $toString: '$_id' } }, pipeline: [ { $match: { $expr: { $eq: ['$user_id', '$$userId'] }, createdAt: { $gte: todayStart, $lte: todayEnd } } }, { $sort: { createdAt: -1 } }, { $limit: 1 } ], as: 'todayAttendance' } }, // Join with userStatus collection { $lookup: { from: 'userstatuses', let: { userId: '$_id' }, pipeline: [ { $match: { $expr: { $eq: ['$user_id', '$$userId'] } } } ], as: 'status' } }, // Unwind status array to a single object (or null) { $addFields: { status: { $arrayElemAt: ['$status', 0] }, attendanceStatus: { $arrayElemAt: ['$todayAttendance.attendanceStatus', 0] }, checkInTime: { $arrayElemAt: ['$todayAttendance.createdAt', 0] }, // New fields for checkout and break (backwards compatible - defaults to null/0) checkOutTime: { $arrayElemAt: ['$todayAttendance.checkOut', 0] }, totalWorkingHours: { $ifNull: [{ $arrayElemAt: ['$todayAttendance.totalWorkingHours', 0] }, null] }, breakDuration: { $ifNull: [{ $arrayElemAt: ['$todayAttendance.breakDuration', 0] }, 0] }, activeBreakStartTime: { $let: { vars: { activeBreak: { $arrayElemAt: [ { $filter: { input: { $ifNull: [{ $arrayElemAt: ['$todayAttendance.breakSessions', 0] }, []] }, as: 'session', cond: { $or: [ { $eq: ['$$session.endTime', null] }, { $eq: [{ $type: '$$session.endTime' }, 'null'] } ] } } }, 0 ] } }, in: { $cond: { if: { $ne: ['$$activeBreak', null] }, then: '$$activeBreak.startTime', else: null } } } }, isOnBreak: { $cond: [ { $eq: [{ $arrayElemAt: ['$status.currentStatus', 0] }, 'break'] }, true, false ] } } }, // Include eventActivity for backward compatibility { $lookup: { from: 'eventactivities', let: { userId: '$_id' }, pipeline: [ { $match: { $expr: { $eq: ['$user_id', '$$userId'] }, createdAt: { $gte: todayStart } } }, { $sort: { createdAt: -1 } }, { $project: { _id: 1, user_id: 1, activeTab: 1, keystroke: 1, mouseclick: 1, hoursPassed: 1, shiftStatus: 1, createdAt: 1 } } ], as: 'eventActivity' } }, { $addFields: { joined_on: { $ifNull: ['$joined_on', new Date('2026-01-01T00:00:00.000Z')] } } }, { $project: { password: 0, departmentInfo: 0, todayAttendance: 0 } } ]); if (!foundUser.length) { return res.status(404).json({ message: 'User not found' }); } res.status(200).json({ user: foundUser[0] }); } catch (err) { console.log(err); res.status(500).json({ error: err }); } }; export const uploadProfilePicture = async (req, res) => { try { const id = req.params.id; if (!mongoose.Types.ObjectId.isValid(id)) { return res.status(400).json({ message: 'Invalid ID format' }); } if (!req.file || !req.file.location) { return res.status(400).json({ message: 'No image file uploaded' }); } const token = req.headers.authorization?.split(" ")[1]; if (!token) return res.status(401).json({ message: 'Unauthorized' }); const decodedToken = jwt.verify(token, "this is dummy text"); const requestedUser = await user.findById(id); if (!requestedUser) return res.status(404).json({ message: 'User not found' }); if (decodedToken.role === 'department_head') { if (!decodedToken.department || requestedUser.department?.toString() !== decodedToken.department) { return res.status(403).json({ message: 'Access denied. User not in your department' }); } } else if (decodedToken.role === 'employee') { if (requestedUser.email !== decodedToken.email) { return res.status(403).json({ message: 'Access denied. You can only update your own profile picture' }); } } // super_admin and admin can update any // Delete previous profile picture from S3 (best-effort) to keep only the latest. if (requestedUser.profilePicture) { try { const prevUrl = new URL(requestedUser.profilePicture); const prevKey = prevUrl.pathname.startsWith('/') ? prevUrl.pathname.slice(1) : prevUrl.pathname; // Only attempt deletion for our expected prefix. if (prevKey && prevKey.startsWith('pfps/')) { const bucketName = process.env.AWS_BUCKET_NAME || 'mudeer-bucket'; await s3Client.send(new DeleteObjectCommand({ Bucket: bucketName, Key: prevKey, })); } } catch (e) { // Ignore deletion failures (cache/cdn URLs, already-deleted object, etc.) console.warn('Failed to delete previous profile picture from S3:', e?.message || e); } } await user.findByIdAndUpdate(id, { profilePicture: req.file.location }); return res.status(200).json({ profilePicture: req.file.location }); } catch (err) { if (err.name === 'JsonWebTokenError') return res.status(401).json({ message: 'Unauthorized' }); console.error(err); return res.status(500).json({ message: err.message || 'Failed to upload profile picture' }); } }; // Request OTP for password reset export const requestPasswordOtp = async (req, res) => { try { const { email } = req.body; // Check if user exists const User = await user.findOne({ email }); if (!User) { return res.status(400).json({ message: "User does not exist" }); } // Check if user is active if (User.isActive === false) { return res.status(403).json({ message: "Your account has been deactivated. Please contact an administrator." }); } // Rate limiting: Check request count (max 3 per hour) const requestKey = `otp_requests:${email}`; const requestCount = await redisClient.get(requestKey); if (requestCount && parseInt(requestCount) >= 3) { return res.status(429).json({ message: "Too many OTP requests. Please try again after some time." }); } // Generate 6-digit OTP const otp = crypto.randomInt(100000, 999999).toString(); // Hash OTP with bcrypt before storing const salt = await bcrypt.genSalt(10); const hashedOtp = await bcrypt.hash(otp, salt); // Store hashed OTP in Redis with 10-minute TTL (600 seconds) const otpKey = `otp:${email}`; await redisClient.setEx(otpKey, 600, hashedOtp); // Reset attempt counter const attemptKey = `otp_attempts:${email}`; await redisClient.setEx(attemptKey, 600, '0'); // Increment request counter (1 hour TTL) const currentCount = requestCount ? parseInt(requestCount) : 0; await redisClient.setEx(requestKey, 3600, (currentCount + 1).toString()); // Send OTP via email await transporter.sendMail({ from: process.env.SMTP_FROM, to: User.email, subject: "Password Reset OTP", html: ` <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;"> <h1 style="color: #234297;">Mudeer</h1> <h2 style="color: #333;">Password Reset OTP</h2> <p style="font-size: 16px; color: #666;">Your password reset OTP is:</p> <div style="background-color: #f5f5f5; padding: 20px; text-align: center; margin: 20px 0; border-radius: 8px;"> <h1 style="font-size: 48px; letter-spacing: 8px; color: #234297; margin: 0; font-weight: bold;">${otp}</h1> </div> <p style="font-size: 14px; color: #999;">This code expires in 10 minutes.</p> <p style="font-size: 14px; color: #999;">If you didn't request this, please ignore this email.</p> </div>`, }); return res.status(200).json({ message: "OTP sent to your email. Please check your inbox." }); } catch (error) { console.error("Error in requestPasswordOtp:", error); return res.status(500).json({ message: "Internal server error", error: error.message }); } }; // Verify OTP and reset password export const verifyOtpAndResetPassword = async (req, res) => { try { const { email, otp, newPassword } = req.body; // Validate request payload if (!email || !otp || !newPassword) { return res.status(400).json({ message: "Email, OTP, and new password are required" }); } // Check if user exists const User = await user.findOne({ email }); if (!User) { return res.status(400).json({ message: "User not found" }); } // Check if user is active if (User.isActive === false) { return res.status(403).json({ message: "Your account has been deactivated. Please contact an administrator." }); } // Check attempt limit (max 5 attempts) const attemptKey = `otp_attempts:${email}`; const attemptCount = await redisClient.get(attemptKey); if (attemptCount && parseInt(attemptCount) >= 5) { return res.status(429).json({ message: "Too many verification attempts. Please request a new OTP." }); } // Retrieve hashed OTP from Redis const otpKey = `otp:${email}`; const hashedOtp = await redisClient.get(otpKey); if (!hashedOtp) { // Increment attempt counter const currentAttempts = attemptCount ? parseInt(attemptCount) : 0; await redisClient.setEx(attemptKey, 600, (currentAttempts + 1).toString()); return res.status(400).json({ message: "OTP has expired or is invalid. Please request a new OTP." }); } // Verify OTP const isOtpValid = await bcrypt.compare(otp, hashedOtp); if (!isOtpValid) { // Increment attempt counter const currentAttempts = attemptCount ? parseInt(attemptCount) : 0; await redisClient.setEx(attemptKey, 600, (currentAttempts + 1).toString()); return res.status(400).json({ message: "Invalid OTP. Please try again." }); } // Password validation if (newPassword.length < 6) { return res.status(422).json({ message: "Password must be at least 6 characters long" }); } if (!/\d/.test(newPassword)) { return res.status(422).json({ message: "Password must contain at least one number" }); } // Hash new password const salt = await bcrypt.genSalt(10); const hashedPassword = await bcrypt.hash(newPassword, salt); // Update password const updatedUser = await user.findOneAndUpdate( { email }, { password: hashedPassword }, { new: true } ); if (!updatedUser) { return res.status(500).json({ message: "Failed to update user password" }); } // Clear OTP and attempt counter from Redis await redisClient.del(otpKey); await redisClient.del(attemptKey); // Blacklist existing JWTs so user is logged out everywhere try { await blacklistUserTokens(updatedUser._id); } catch (e) { console.error('Failed to blacklist tokens after password reset via OTP:', e); } return res.status(200).json({ message: "Password updated successfully" }); } catch (error) { console.error("Error in verifyOtpAndResetPassword:", error); if (error instanceof Error) { return res.status(500).json({ message: "Internal server error", error: error.message }); } return res.status(500).json({ message: "Internal server error", error: 'UNKNOWN ERROR' }); } }; export const updatePassword = async (req, res, next) => { try { const { currentPassword, newPassword, confirmPassword, userId: targetUserId } = req.body; // Password validation (shared) if (!newPassword || !confirmPassword) { return res.status(400).json({ message: "New password and confirmation are required" }); } if (newPassword.length < 6) { return res.status(422).json({ message: "Password must be at least 6 characters long" }); } if (!/\d/.test(newPassword)) { return res.status(422).json({ message: "Password must contain at least one number" }); } if (newPassword !== confirmPassword) { return res.status(422).json({ message: "Passwords do not match" }); } const token = req.headers.authorization.split(" ")[1]; const decodedToken = jwt.verify(token, "this is dummy text"); const currentUser = await user.findOne({ email: decodedToken.email }); if (!currentUser) { return res.status(404).json({ message: "User not found" }); } const isSuperAdminResettingOther = currentUser.role === 'super_admin' && targetUserId && targetUserId !== currentUser._id.toString(); if (isSuperAdminResettingOther) { // Superadmin resetting another user's password: no current password required if (!mongoose.Types.ObjectId.isValid(targetUserId)) { return res.status(400).json({ message: "Invalid user ID" }); } const targetUser = await user.findById(targetUserId); if (!targetUser) { return res.status(404).json({ message: "Target user not found" }); } const hash = await bcrypt.hash(newPassword, 10); const updatedUser = await user.findByIdAndUpdate( targetUserId, { $set: { password: hash } }, { new: true } ).select('-password'); // Blacklist target user's tokens so they are logged out try { await blacklistUserTokens(targetUserId); } catch (e) { console.error('Failed to blacklist tokens for target user after admin password update:', e); } return res.status(200).json({ message: "Password updated successfully", updatedUser: updatedUser }); } // Self or non-superadmin: require current password if (!currentPassword) { return res.status(400).json({ message: "Current password is required" }); } const isCurrentPasswordValid = await bcrypt.compare(currentPassword, currentUser.password); if (!isCurrentPasswordValid) { return res.status(401).json({ message: "Current password is incorrect" }); } const hash = await bcrypt.hash(newPassword, 10); const updatedUser = await user.findByIdAndUpdate( currentUser._id, { $set: { password: hash } }, { new: true } ).select('-password'); // Blacklist current user's tokens to force re-login everywhere try { await blacklistUserTokens(currentUser._id); } catch (e) { console.error('Failed to blacklist tokens for user after self password update:', e); } res.status(200).json({ message: "Password updated successfully", updatedUser: updatedUser }); } catch (err) { if (err.name === 'JsonWebTokenError') { return res.status(401).json({ message: "Invalid token" }); } if (err.name === 'TokenExpiredError') { return res.status(401).json({ message: "Token expired" }); } console.log(err); res.status(500).json({ error: err.message || err }); } }; export const updateFcmToken = async (req, res, next) => { try { const { fcmToken } = req.body; if (!fcmToken) { return res.status(400).json({ message: "FCM token is required" }); } // Get user ID from JWT token const token = req.headers.authorization.split(" ")[1]; const decodedToken = jwt.verify(token, "this is dummy text"); const updatedUser = await user.findOneAndUpdate( { email: decodedToken.email }, { $set: { fcmToken: fcmToken } }, { new: true } ).select('-password -fcmToken'); if (!updatedUser) { return res.status(404).json({ message: "User not found" }); } res.status(200).json({ message: "FCM token updated successfully", user: updatedUser }); } catch (err) { if (err.name === 'JsonWebTokenError') { return res.status(401).json({ message: "Invalid token" }); } if (err.name === 'TokenExpiredError') { return res.status(401).json({ message: "Token expired" }); } console.log(err); res.status(500).json({ error: err.message || err }); } }; export const deactivateUser = async (req, res, next) => { try { const { id: userIdToDeactivate } = req.params; const token = req.headers.authorization?.split(' ')[1]; if (!token) return res.status(401).json({ message: "No token provided" }); const decodedToken = jwt.verify(token, "this is dummy text"); const requester = await user.findOne({ email: decodedToken.email }); if (!requester) { return res.status(404).json({ message: "Requester not found" }); } if (requester._id.toString() === userIdToDeactivate) { return res.status(400).json({ message: "Cannot deactivate yourself" }); } const userToDeactivate = await user.findById(userIdToDeactivate); if (!userToDeactivate) { return res.status(404).json({ message: "User not found" }); } if (userToDeactivate.isActive === false) { return res.status(400).json({ message: "User is already deactivated" }); } if (userToDeactivate.role === 'super_admin') { return res.status(403).json({ message: "Super admin cannot be deactivated" }); } // Set isActive to false await user.findByIdAndUpdate( userIdToDeactivate, { $set: { isActive: false } }, { new: true } ); // Invalidate cache for user lists await invalidateCache.user(userIdToDeactivate); res.status(200).json({ message: "User deactivated successfully", userId: userIdToDeactivate }); } catch (err) { if (err.name === 'JsonWebTokenError') { return res.status(401).json({ message: "Invalid token" }); } if (err.name === 'TokenExpiredError') { return res.status(401).json({ message: "Token expired" }); } console.error(err); res.status(500).json({ error: err.message || err }); } }; export const reactivateUser = async (req, res, next) => { try { const { id: userIdToReactivate } = req.params; const token = req.headers.authorization?.split(' ')[1]; if (!token) return res.status(401).json({ message: "No token provided" }); const decodedToken = jwt.verify(token, "this is dummy text"); const requester = await user.findOne({ email: decodedToken.email }); if (!requester) { return res.status(404).json({ message: "Requester not found" }); } const userToReactivate = await user.findById(userIdToReactivate); if (!userToReactivate) { return res.status(404).json({ message: "User not found" }); } if (userToReactivate.isActive === true) { return res.status(400).json({ message: "User is already active" }); } await user.findByIdAndUpdate( userIdToReactivate, { $set: { isActive: true } }, { new: true } ); await invalidateCache.user(userIdToReactivate); res.status(200).json({ message: "User reactivated successfully", userId: userIdToReactivate }); } catch (err) { if (err.name === 'JsonWebTokenError') { return res.status(401).json({ message: "Invalid token" }); } if (err.name === 'TokenExpiredError') { return res.status(401).json({ message: "Token expired" }); } console.error(err); res.status(500).json({ error: err.message || err }); } }; export const deleteUserData = async (req, res, next) => { try { const { id: userIdToDelete } = req.params; const token = req.headers.authorization?.split(' ')[1]; if (!token) return res.status(401).json({ message: "No token provided" }); const decodedToken = jwt.verify(token, "this is dummy text"); const requester = await user.findOne({ email: decodedToken.email }); if (!requester) { return res.status(404).json({ message: "Requester not found" }); } if (requester._id.toString() === userIdToDelete) { return res.status(400).json({ message: "Cannot delete yourself" }); } const userToDelete = await user.findById(userIdToDelete); if (!userToDelete) return res.status(404).json({ message: "User not found" }); const stats = {}; // Delete attendances const attendanceResult = await attendance.deleteMany({ user_id: userIdToDelete }); stats.attendances = attendanceResult.deletedCount; // Delete user statuses const statusResult = await UserStatus.deleteMany({ user_id: userIdToDelete }); stats.userStatuses = statusResult.deletedCount; // Delete notifications (both sent and received) const notificationResult = await Notification.deleteMany({ $or: [{ recipient: userIdToDelete }, { sender: userIdToDelete }] }); stats.notifications = notificationResult.deletedCount; // Delete event activities const eventResult = await EventActivity.deleteMany({ user_id: userIdToDelete }); stats.eventActivities = eventResult.deletedCount; // Delete S3 images and activeWindows const windows = await activeWindow.find({ user_id: userIdToDelete }); stats.activeWindows = windows.length; stats.s3ImagesDeleted = 0; for (const window of windows) { if (window.image) { try { const url = new URL(window.image); const key = url.pathname.substring(1); // Remove leading / const bucketName = process.env.AWS_BUCKET_NAME || 'mudeer-bucket' await s3Client.send(new DeleteObjectCommand({ Bucket: bucketName, Key: key, })); stats.s3ImagesDeleted++; } catch (s3Err) { console.error(`Failed to delete S3 image: ${window.image}`, s3Err); } } } await activeWindow.deleteMany({ user_id: userIdToDelete }); // Remove from department employees array if (userToDelete.department) { await Department.updateOne( { _id: userToDelete.department }, { $pull: { employees: userIdToDelete } } ); } // Unset department head if applicable await Department.updateMany( { head: userIdToDelete }, { $set: { head: null } } ); // Deactivate user instead of deleting await user.findByIdAndUpdate( userIdToDelete, { $set: { isActive: false } }, { new: true } ); // Invalidate cache for user lists await invalidateCache.user(userIdToDelete); res.status(200).json({ message: "User deactivated and all associated data deleted successfully", deletionStats: stats }); } catch (err) { if (err.name === 'JsonWebTokenError') { return res.status(401).json({ message: "Invalid token" }); } if (err.name === 'TokenExpiredError') { return res.status(401).json({ message: "Token expired" }); } console.error(err); res.status(500).json({ error: err.message || err }); } };