From e20241ff743399ff351b496ff997bd2ce392973c Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Tue, 5 May 2026 23:19:25 +0000 Subject: [PATCH] 1.2 --- backend/src/db/api/employee_pay_types.js | 24 ++++++++-- backend/src/db/api/job_logs.js | 24 +++++++++- backend/src/db/api/pay_types.js | 50 +++++++++++++++++++-- backend/src/db/api/users.js | 32 +++++++++++++- backend/src/routes/employee_pay_types.js | 4 +- backend/src/routes/job_logs.js | 4 +- backend/src/routes/pay_types.js | 4 +- backend/src/routes/users.js | 10 ++--- backend/src/security/payrollAccess.js | 19 ++++++++ backend/src/services/job_logs.js | 53 +++++++++++++++++++++-- frontend/src/components/AsideMenuList.tsx | 8 ++-- frontend/src/helpers/accessControl.ts | 24 ++++++++++ frontend/src/layouts/Authenticated.tsx | 11 ++++- frontend/src/pages/dashboard.tsx | 7 +-- frontend/tsconfig.tsbuildinfo | 1 + 15 files changed, 243 insertions(+), 32 deletions(-) create mode 100644 backend/src/security/payrollAccess.js create mode 100644 frontend/src/helpers/accessControl.ts create mode 100644 frontend/tsconfig.tsbuildinfo diff --git a/backend/src/db/api/employee_pay_types.js b/backend/src/db/api/employee_pay_types.js index 9a63562..dcbc60a 100644 --- a/backend/src/db/api/employee_pay_types.js +++ b/backend/src/db/api/employee_pay_types.js @@ -3,6 +3,7 @@ const db = require('../models'); const FileDBApi = require('./file'); const crypto = require('crypto'); const Utils = require('../utils'); +const { isRestrictedPayrollUser } = require('../../security/payrollAccess'); @@ -205,12 +206,17 @@ module.exports = class Employee_pay_typesDBApi { static async findBy(where, options) { const transaction = (options && options.transaction) || undefined; + const currentUser = options?.currentUser; const employee_pay_types = await db.employee_pay_types.findOne( { where }, { transaction }, ); + if (employee_pay_types && isRestrictedPayrollUser(currentUser) && employee_pay_types.employeeId !== currentUser.id) { + return null; + } + if (!employee_pay_types) { return employee_pay_types; } @@ -249,6 +255,8 @@ module.exports = class Employee_pay_typesDBApi { filter, options ) { + const currentUser = options?.currentUser; + const effectiveEmployeeFilter = isRestrictedPayrollUser(currentUser) ? currentUser.id : filter.employee; const limit = filter.limit || 0; let offset = 0; let where = {}; @@ -270,12 +278,12 @@ module.exports = class Employee_pay_typesDBApi { model: db.users, as: 'employee', - where: filter.employee ? { + where: effectiveEmployeeFilter ? { [Op.or]: [ - { id: { [Op.in]: filter.employee.split('|').map(term => Utils.uuid(term)) } }, + { id: { [Op.in]: effectiveEmployeeFilter.split('|').map(term => Utils.uuid(term)) } }, { firstName: { - [Op.or]: filter.employee.split('|').map(term => ({ [Op.iLike]: `%${term}%` })) + [Op.or]: effectiveEmployeeFilter.split('|').map(term => ({ [Op.iLike]: `%${term}%` })) } }, ] @@ -447,7 +455,8 @@ module.exports = class Employee_pay_typesDBApi { } } - static async findAllAutocomplete(query, limit, offset, ) { + static async findAllAutocomplete(query, limit, offset, options) { + const currentUser = options?.currentUser; let where = {}; @@ -465,6 +474,13 @@ module.exports = class Employee_pay_typesDBApi { }; } + if (isRestrictedPayrollUser(currentUser)) { + where = { + ...where, + employeeId: currentUser.id, + }; + } + const records = await db.employee_pay_types.findAll({ attributes: [ 'id', 'active' ], where, diff --git a/backend/src/db/api/job_logs.js b/backend/src/db/api/job_logs.js index fbc095f..0832b01 100644 --- a/backend/src/db/api/job_logs.js +++ b/backend/src/db/api/job_logs.js @@ -3,6 +3,7 @@ const db = require('../models'); const FileDBApi = require('./file'); const crypto = require('crypto'); const Utils = require('../utils'); +const { isRestrictedPayrollUser } = require('../../security/payrollAccess'); @@ -320,12 +321,17 @@ module.exports = class Job_logsDBApi { static async findBy(where, options) { const transaction = (options && options.transaction) || undefined; + const currentUser = options?.currentUser; const job_logs = await db.job_logs.findOne( { where }, { transaction }, ); + if (job_logs && isRestrictedPayrollUser(currentUser) && job_logs.employeeId !== currentUser.id) { + return null; + } + if (!job_logs) { return job_logs; } @@ -382,6 +388,7 @@ module.exports = class Job_logsDBApi { filter, options ) { + const currentUser = options?.currentUser; const limit = filter.limit || 0; let offset = 0; let where = {}; @@ -486,6 +493,13 @@ module.exports = class Job_logsDBApi { ]; + if (isRestrictedPayrollUser(currentUser)) { + where = { + ...where, + employeeId: currentUser.id, + }; + } + if (filter) { if (filter.id) { where = { @@ -752,7 +766,8 @@ module.exports = class Job_logsDBApi { } } - static async findAllAutocomplete(query, limit, offset, ) { + static async findAllAutocomplete(query, limit, offset, options) { + const currentUser = options?.currentUser; let where = {}; @@ -770,6 +785,13 @@ module.exports = class Job_logsDBApi { }; } + if (isRestrictedPayrollUser(currentUser)) { + where = { + ...where, + employeeId: currentUser.id, + }; + } + const records = await db.job_logs.findAll({ attributes: [ 'id', 'job_address' ], where, diff --git a/backend/src/db/api/pay_types.js b/backend/src/db/api/pay_types.js index 28916b3..80d2cae 100644 --- a/backend/src/db/api/pay_types.js +++ b/backend/src/db/api/pay_types.js @@ -3,6 +3,7 @@ const db = require('../models'); const FileDBApi = require('./file'); const crypto = require('crypto'); const Utils = require('../utils'); +const { isRestrictedPayrollUser } = require('../../security/payrollAccess'); @@ -218,9 +219,20 @@ module.exports = class Pay_typesDBApi { static async findBy(where, options) { const transaction = (options && options.transaction) || undefined; + const currentUser = options?.currentUser; + const accessInclude = isRestrictedPayrollUser(currentUser) ? [{ + model: db.employee_pay_types, + as: 'employee_pay_types_pay_type', + required: true, + attributes: [], + where: { + employeeId: currentUser.id, + active: true, + }, + }] : undefined; const pay_types = await db.pay_types.findOne( - { where }, + { where, include: accessInclude }, { transaction }, ); @@ -238,13 +250,15 @@ module.exports = class Pay_typesDBApi { output.employee_pay_types_pay_type = await pay_types.getEmployee_pay_types_pay_type({ - transaction + transaction, + where: isRestrictedPayrollUser(currentUser) ? { employeeId: currentUser.id, active: true } : undefined, }); output.job_logs_pay_type = await pay_types.getJob_logs_pay_type({ - transaction + transaction, + where: isRestrictedPayrollUser(currentUser) ? { employeeId: currentUser.id } : undefined, }); @@ -260,6 +274,7 @@ module.exports = class Pay_typesDBApi { filter, options ) { + const currentUser = options?.currentUser; const limit = filter.limit || 0; let offset = 0; let where = {}; @@ -281,6 +296,22 @@ module.exports = class Pay_typesDBApi { ]; + if (isRestrictedPayrollUser(currentUser)) { + include = [ + { + model: db.employee_pay_types, + as: 'employee_pay_types_pay_type', + required: true, + attributes: [], + where: { + employeeId: currentUser.id, + active: true, + }, + }, + ...include, + ]; + } + if (filter) { if (filter.id) { where = { @@ -449,7 +480,8 @@ module.exports = class Pay_typesDBApi { } } - static async findAllAutocomplete(query, limit, offset, ) { + static async findAllAutocomplete(query, limit, offset, options) { + const currentUser = options?.currentUser; let where = {}; @@ -470,6 +502,16 @@ module.exports = class Pay_typesDBApi { const records = await db.pay_types.findAll({ attributes: [ 'id', 'name' ], where, + include: isRestrictedPayrollUser(currentUser) ? [{ + model: db.employee_pay_types, + as: 'employee_pay_types_pay_type', + required: true, + attributes: [], + where: { + employeeId: currentUser.id, + active: true, + }, + }] : undefined, limit: limit ? Number(limit) : undefined, offset: offset ? Number(offset) : undefined, orderBy: [['name', 'ASC']], diff --git a/backend/src/db/api/users.js b/backend/src/db/api/users.js index 1ecccb9..858f8fc 100644 --- a/backend/src/db/api/users.js +++ b/backend/src/db/api/users.js @@ -3,6 +3,7 @@ const db = require('../models'); const FileDBApi = require('./file'); const crypto = require('crypto'); const Utils = require('../utils'); +const { isRestrictedPayrollUser } = require('../../security/payrollAccess'); const bcrypt = require('bcrypt'); const config = require('../../config'); @@ -387,9 +388,19 @@ module.exports = class UsersDBApi { static async findBy(where, options) { const transaction = (options && options.transaction) || undefined; + const currentUser = options?.currentUser; + const effectiveWhere = { ...(where || {}) }; + + if (isRestrictedPayrollUser(currentUser)) { + if (effectiveWhere.id && Utils.uuid(effectiveWhere.id) !== currentUser.id) { + return null; + } + + effectiveWhere.id = currentUser.id; + } const users = await db.users.findOne( - { where }, + { where: effectiveWhere }, { transaction }, ); @@ -408,6 +419,7 @@ module.exports = class UsersDBApi { output.employee_pay_types_employee = await users.getEmployee_pay_types_employee({ transaction, + where: isRestrictedPayrollUser(currentUser) ? { employeeId: currentUser.id, active: true } : undefined, include: [ { model: db.pay_types, @@ -460,6 +472,7 @@ module.exports = class UsersDBApi { filter, options ) { + const currentUser = options?.currentUser; const limit = filter.limit || 0; let offset = 0; let where = {}; @@ -524,6 +537,13 @@ module.exports = class UsersDBApi { ]; + if (isRestrictedPayrollUser(currentUser)) { + where = { + ...where, + id: currentUser.id, + }; + } + if (filter) { if (filter.id) { where = { @@ -783,7 +803,8 @@ module.exports = class UsersDBApi { } } - static async findAllAutocomplete(query, limit, offset, ) { + static async findAllAutocomplete(query, limit, offset, options) { + const currentUser = options?.currentUser; let where = {}; @@ -801,6 +822,13 @@ module.exports = class UsersDBApi { }; } + if (isRestrictedPayrollUser(currentUser)) { + where = { + ...where, + id: currentUser.id, + }; + } + const records = await db.users.findAll({ attributes: [ 'id', 'firstName' ], where, diff --git a/backend/src/routes/employee_pay_types.js b/backend/src/routes/employee_pay_types.js index 311b5d8..a4f7519 100644 --- a/backend/src/routes/employee_pay_types.js +++ b/backend/src/routes/employee_pay_types.js @@ -335,7 +335,6 @@ router.get('/count', wrapAsync(async (req, res) => { const currentUser = req.currentUser; const payload = await Employee_pay_typesDBApi.findAll( req.query, - null, { countOnly: true, currentUser } ); @@ -373,7 +372,7 @@ router.get('/autocomplete', async (req, res) => { req.query.query, req.query.limit, req.query.offset, - + { currentUser: req.currentUser }, ); res.status(200).send(payload); @@ -414,6 +413,7 @@ router.get('/autocomplete', async (req, res) => { router.get('/:id', wrapAsync(async (req, res) => { const payload = await Employee_pay_typesDBApi.findBy( { id: req.params.id }, + { currentUser: req.currentUser }, ); diff --git a/backend/src/routes/job_logs.js b/backend/src/routes/job_logs.js index 2c72b52..f8aa59a 100644 --- a/backend/src/routes/job_logs.js +++ b/backend/src/routes/job_logs.js @@ -355,7 +355,6 @@ router.get('/count', wrapAsync(async (req, res) => { const currentUser = req.currentUser; const payload = await Job_logsDBApi.findAll( req.query, - null, { countOnly: true, currentUser } ); @@ -393,7 +392,7 @@ router.get('/autocomplete', async (req, res) => { req.query.query, req.query.limit, req.query.offset, - + { currentUser: req.currentUser }, ); res.status(200).send(payload); @@ -434,6 +433,7 @@ router.get('/autocomplete', async (req, res) => { router.get('/:id', wrapAsync(async (req, res) => { const payload = await Job_logsDBApi.findBy( { id: req.params.id }, + { currentUser: req.currentUser }, ); diff --git a/backend/src/routes/pay_types.js b/backend/src/routes/pay_types.js index 12e47c2..2f80e20 100644 --- a/backend/src/routes/pay_types.js +++ b/backend/src/routes/pay_types.js @@ -348,7 +348,6 @@ router.get('/count', wrapAsync(async (req, res) => { const currentUser = req.currentUser; const payload = await Pay_typesDBApi.findAll( req.query, - null, { countOnly: true, currentUser } ); @@ -386,7 +385,7 @@ router.get('/autocomplete', async (req, res) => { req.query.query, req.query.limit, req.query.offset, - + { currentUser: req.currentUser }, ); res.status(200).send(payload); @@ -427,6 +426,7 @@ router.get('/autocomplete', async (req, res) => { router.get('/:id', wrapAsync(async (req, res) => { const payload = await Pay_typesDBApi.findBy( { id: req.params.id }, + { currentUser: req.currentUser }, ); diff --git a/backend/src/routes/users.js b/backend/src/routes/users.js index 19df9ae..52ae470 100644 --- a/backend/src/routes/users.js +++ b/backend/src/routes/users.js @@ -347,7 +347,6 @@ router.get('/count', wrapAsync(async (req, res) => { const currentUser = req.currentUser; const payload = await UsersDBApi.findAll( req.query, - null, { countOnly: true, currentUser } ); @@ -385,7 +384,7 @@ router.get('/autocomplete', async (req, res) => { req.query.query, req.query.limit, req.query.offset, - + { currentUser: req.currentUser }, ); res.status(200).send(payload); @@ -426,11 +425,12 @@ router.get('/autocomplete', async (req, res) => { router.get('/:id', wrapAsync(async (req, res) => { const payload = await UsersDBApi.findBy( { id: req.params.id }, + { currentUser: req.currentUser }, ); - - + + if (payload) { delete payload.password; - + } res.status(200).send(payload); })); diff --git a/backend/src/security/payrollAccess.js b/backend/src/security/payrollAccess.js new file mode 100644 index 0000000..96421b0 --- /dev/null +++ b/backend/src/security/payrollAccess.js @@ -0,0 +1,19 @@ +const PRIVILEGED_PAYROLL_ROLE_NAMES = new Set([ + 'Administrator', + 'System Owner', + 'Payroll Manager', + 'Operations Manager', +]); + +function isRestrictedPayrollUser(currentUser) { + if (!currentUser?.id) { + return false; + } + + return !PRIVILEGED_PAYROLL_ROLE_NAMES.has(currentUser?.app_role?.name); +} + +module.exports = { + PRIVILEGED_PAYROLL_ROLE_NAMES, + isRestrictedPayrollUser, +}; diff --git a/backend/src/services/job_logs.js b/backend/src/services/job_logs.js index f21fc42..b91172c 100644 --- a/backend/src/services/job_logs.js +++ b/backend/src/services/job_logs.js @@ -7,8 +7,28 @@ const csv = require('csv-parser'); const axios = require('axios'); const config = require('../config'); const stream = require('stream'); +const { isRestrictedPayrollUser } = require('../security/payrollAccess'); module.exports = class Job_logsService { + static async ensureRestrictedPayTypeAccess(payTypeId, currentUser, transaction) { + if (!isRestrictedPayrollUser(currentUser) || !payTypeId) { + return; + } + + const assignment = await db.employee_pay_types.findOne({ + where: { + employeeId: currentUser.id, + pay_typeId: payTypeId, + active: true, + }, + transaction, + }); + + if (!assignment) { + throw new ValidationError('errors.forbidden.message'); + } + } + static async create(data, currentUser) { const transaction = await db.sequelize.transaction(); try { @@ -23,8 +43,16 @@ module.exports = class Job_logsService { customerId = customer.id; } + const jobPayload = { ...data, customer: customerId }; + + if (isRestrictedPayrollUser(currentUser)) { + jobPayload.employee = currentUser.id; + } + + await Job_logsService.ensureRestrictedPayTypeAccess(jobPayload.pay_type, currentUser, transaction); + const createdJob = await Job_logsDBApi.create( - { ...data, customer: customerId }, + jobPayload, { currentUser, transaction, @@ -93,7 +121,10 @@ module.exports = class Job_logsService { try { let job_logs = await Job_logsDBApi.findBy( {id}, - {transaction}, + { + transaction, + currentUser, + }, ); if (!job_logs) { @@ -101,6 +132,10 @@ module.exports = class Job_logsService { 'job_logsNotFound', ); } + + if (isRestrictedPayrollUser(currentUser) && job_logs.employeeId !== currentUser.id) { + throw new ValidationError('errors.forbidden.message'); + } let customerId = data.customer; @@ -113,9 +148,21 @@ module.exports = class Job_logsService { customerId = customer.id; } + const jobPayload = { ...data, customer: customerId }; + + if (isRestrictedPayrollUser(currentUser)) { + jobPayload.employee = currentUser.id; + } + + await Job_logsService.ensureRestrictedPayTypeAccess( + jobPayload.pay_type !== undefined ? jobPayload.pay_type : job_logs.pay_typeId, + currentUser, + transaction, + ); + const updatedJob_logs = await Job_logsDBApi.update( id, - { ...data, customer: customerId }, + jobPayload, { currentUser, transaction, diff --git a/frontend/src/components/AsideMenuList.tsx b/frontend/src/components/AsideMenuList.tsx index 9e33ea1..231247c 100644 --- a/frontend/src/components/AsideMenuList.tsx +++ b/frontend/src/components/AsideMenuList.tsx @@ -3,6 +3,7 @@ import { MenuAsideItem } from '../interfaces' import AsideMenuItem from './AsideMenuItem' import {useAppSelector} from "../stores/hooks"; import {hasPermission} from "../helpers/userPermissions"; +import { isBlockedWorkerRoute, isRestrictedPayrollUser } from '../helpers/accessControl'; type Props = { menu: MenuAsideItem[] @@ -18,9 +19,10 @@ export default function AsideMenuList({ menu, isDropdownList = false, className return (