diff --git a/src/backend/src/controllers/rules.controllers.ts b/src/backend/src/controllers/rules.controllers.ts index 3faf13fa45..11357130c8 100644 --- a/src/backend/src/controllers/rules.controllers.ts +++ b/src/backend/src/controllers/rules.controllers.ts @@ -124,7 +124,7 @@ export default class RulesController { static async getAllRulesetTypes(req: Request, res: Response, next: NextFunction) { try { - const rulesets = await RulesService.getAllRulesetTypes(req.organization, req.currentCar?.carId); + const rulesets = await RulesService.getAllRulesetTypes(req.currentUser, req.organization, req.currentCar?.carId); res.status(200).json(rulesets); } catch (error: unknown) { next(error); @@ -135,6 +135,7 @@ export default class RulesController { try { const { rulesetTypeId } = req.params as Record; const rulesets = await RulesService.getRulesetsByRulesetType( + req.currentUser, rulesetTypeId, req.organization.organizationId, req.currentCar?.carId @@ -149,6 +150,7 @@ export default class RulesController { try { const { rulesetTypeId } = req.params as Record; const rulesetType = await RulesService.getRulesetType( + req.currentUser, rulesetTypeId, req.organization.organizationId, req.currentCar?.carId @@ -315,7 +317,7 @@ export default class RulesController { static async getChildRules(req: Request, res: Response, next: NextFunction) { try { const { ruleId: parentRuleId } = req.params as Record; - const childrenRules: Rule[] = await RulesService.getChildRules(parentRuleId, req.organization); + const childrenRules: Rule[] = await RulesService.getChildRules(req.currentUser, parentRuleId, req.organization); res.status(200).json(childrenRules); } catch (error: unknown) { @@ -328,6 +330,7 @@ export default class RulesController { const { rulesetId, projectId } = req.params as Record; const rules = await RulesService.getUnassignedRulesForProjectRuleset( + req.currentUser, rulesetId, projectId, req.organization.organizationId @@ -342,7 +345,7 @@ export default class RulesController { try { const { rulesetId, projectId } = req.params as Record; - const projectRules = await RulesService.getProjectRules(rulesetId, projectId, req.organization); + const projectRules = await RulesService.getProjectRules(req.currentUser, rulesetId, projectId, req.organization); res.status(200).json(projectRules); } catch (error: unknown) { @@ -353,7 +356,7 @@ export default class RulesController { static async getTopLevelRules(req: Request, res: Response, next: NextFunction) { try { const { rulesetId } = req.params as Record; - const rules = await RulesService.getTopLevelRules(rulesetId, req.organization.organizationId); + const rules = await RulesService.getTopLevelRules(req.currentUser, rulesetId, req.organization.organizationId); res.status(200).json(rules); } catch (error: unknown) { next(error); @@ -363,7 +366,7 @@ export default class RulesController { static async getAllRulesForRuleset(req: Request, res: Response, next: NextFunction) { try { const { rulesetId } = req.params as Record; - const rules = await RulesService.getAllRulesForRuleset(rulesetId, req.organization.organizationId); + const rules = await RulesService.getAllRulesForRuleset(req.currentUser, rulesetId, req.organization.organizationId); res.status(200).json(rules); } catch (error: unknown) { next(error); diff --git a/src/backend/src/services/rules.services.ts b/src/backend/src/services/rules.services.ts index 08c25bc204..d421db5362 100644 --- a/src/backend/src/services/rules.services.ts +++ b/src/backend/src/services/rules.services.ts @@ -23,6 +23,7 @@ import { NotFoundException } from '../utils/errors.utils.js'; import { userHasPermission } from '../utils/users.utils.js'; +import { isUserPartOfTeams } from '../utils/teams.utils.js'; import { getProjectRuleQueryArgs, getRulesetQueryArgs, @@ -30,6 +31,7 @@ import { getRulesetTypeQueryArgs, getRuleStatusHistoryQueryArgs } from '../prisma-query-args/rules.query-args.js'; +import { getTeamPreviewQueryArgs } from '../prisma-query-args/teams.query-args.js'; import { ruleTransformer, projectRuleTransformer, @@ -220,9 +222,9 @@ export default class RulesService { referencedRuleIds: string[] = [], imageFileIds: string[] = [] ) { - // Check user has permission (members and above) - if (!(await userHasPermission(user.userId, organization.organizationId, notGuest))) { - throw new AccessDeniedException('Only members and above can create rules'); + // Check user has permission (leadership and above) + if (!(await userHasPermission(user.userId, organization.organizationId, isLeadership))) { + throw new AccessDeniedException('Only leadership and above can create rules'); } // Verify ruleset exists and belongs to organization @@ -360,8 +362,8 @@ export default class RulesService { } }); - if (!(await userHasPermission(deleter.userId, org.organizationId, isAdmin))) { - throw new AccessDeniedAdminOnlyException('delete rules'); + if (!(await userHasPermission(deleter.userId, org.organizationId, isLeadership))) { + throw new AccessDeniedException('Only leadership and above can delete rules'); } if (!rule) throw new NotFoundException('Rule', ruleId); @@ -579,8 +581,8 @@ export default class RulesService { organization: Organization, parentRuleId?: string ) { - if (!(await userHasPermission(submitter.userId, organization.organizationId, isAdmin))) - throw new AccessDeniedAdminOnlyException('edit a rule'); + if (!(await userHasPermission(submitter.userId, organization.organizationId, isLeadership))) + throw new AccessDeniedException('Only leadership and above can edit a rule'); const currentRule = await prisma.rule.findUnique({ where: { ruleId }, @@ -671,8 +673,8 @@ export default class RulesService { * @returns the updated rule */ static async addRuleReferences(submitter: User, ruleId: string, referencedRuleId: string, organization: Organization) { - if (!(await userHasPermission(submitter.userId, organization.organizationId, isAdmin))) - throw new AccessDeniedAdminOnlyException('edit a rule'); + if (!(await userHasPermission(submitter.userId, organization.organizationId, isLeadership))) + throw new AccessDeniedException('Only leadership and above can edit a rule'); const rule = await prisma.rule.findUnique({ where: { ruleId }, @@ -730,8 +732,8 @@ export default class RulesService { * @returns the updated rule */ static async removeRuleReferences(submitter: User, ruleId: string, referencedRuleId: string, organization: Organization) { - if (!(await userHasPermission(submitter.userId, organization.organizationId, isAdmin))) - throw new AccessDeniedAdminOnlyException('edit a rule'); + if (!(await userHasPermission(submitter.userId, organization.organizationId, isLeadership))) + throw new AccessDeniedException('Only leadership and above can edit a rule'); const rule = await prisma.rule.findUnique({ where: { ruleId }, @@ -804,10 +806,15 @@ export default class RulesService { static async deleteRuleset(rulesetId: string, deleterId: string, organizationId: string) { const ruleset = await RulesService.getRulesetWithQueryArgs(rulesetId); + // admins can delete any ruleset; leadership and heads can only delete a ruleset they created themselves + const isCreator = deleterId === ruleset.createdByUserId; const hasPermission = - (await userHasPermission(deleterId, organizationId, isAdmin)) || deleterId === ruleset.createdByUserId; + (await userHasPermission(deleterId, organizationId, isAdmin)) || + (isCreator && (await userHasPermission(deleterId, organizationId, isLeadership))); - if (!hasPermission) throw new AccessDeniedException('Only admins can delete a ruleset.'); + if (!hasPermission) { + throw new AccessDeniedException('You do not have permissions to delete this ruleset.'); + } if (ruleset.active) { throw new HttpException(400, 'Cannot delete an active ruleset. Please deactivate it first.'); @@ -822,7 +829,11 @@ export default class RulesService { return rulesetTransformer(deletedRuleset); } - static async getAllRulesetTypes(organization: Organization, carId?: string): Promise { + static async getAllRulesetTypes(user: User, organization: Organization, carId?: string): Promise { + if (!(await userHasPermission(user.userId, organization.organizationId, notGuest))) { + throw new AccessDeniedGuestException('view ruleset types'); + } + const rulesets = await prisma.ruleset_Type.findMany({ where: { organizationId: organization.organizationId, @@ -835,12 +846,22 @@ export default class RulesService { /** * Gets a ruleset type for a given ruleset type ID + * @param user the user requesting the ruleset type * @param rulesetTypeId id of ruleset type * @param organizationId id of organization * @param carId optional id of the car to scope revision file counts to * @returns ruleset type associated with provided ruleset type ID */ - static async getRulesetType(rulesetTypeId: string, organizationId: string, carId?: string): Promise { + static async getRulesetType( + user: User, + rulesetTypeId: string, + organizationId: string, + carId?: string + ): Promise { + if (!(await userHasPermission(user.userId, organizationId, notGuest))) { + throw new AccessDeniedGuestException('view ruleset types'); + } + const rulesetType = await prisma.ruleset_Type.findUnique({ where: { rulesetTypeId, @@ -863,12 +884,22 @@ export default class RulesService { /** * Gets rulesets for a given ruleset type + * @param user the user requesting the rulesets * @param rulesetTypeId id of ruleset type * @param organizationId id of organization * @param carId optional id of the car to filter rulesets by * @returns rulesets associated with provided ruleset type */ - static async getRulesetsByRulesetType(rulesetTypeId: string, organizationId: string, carId?: string): Promise { + static async getRulesetsByRulesetType( + user: User, + rulesetTypeId: string, + organizationId: string, + carId?: string + ): Promise { + if (!(await userHasPermission(user.userId, organizationId, notGuest))) { + throw new AccessDeniedGuestException('view rulesets'); + } + const rulesets = await prisma.ruleset.findMany({ where: { rulesetTypeId, @@ -890,6 +921,7 @@ export default class RulesService { /** * Sets a rule's general-view status. This status is independent of any project. * It is unaffected by the status of the rule in any project it's assigned to. + * Only leadership and above can update the general-view status. * @param submitter the user updating the status * @param organization the organization of the rule * @param ruleId the id of the rule to update @@ -956,6 +988,7 @@ export default class RulesService { /** * Sets a rule's status within a single project. This status is local to that project. * It does not affect the rule's general-view status, or its status in any other project. + * Leadership and above can update it anywhere; members can only update it for a project whose team they're on. * @param submitter the user updating the status * @param organization the organization of the project rule * @param projectRuleId the id of the project rule to update @@ -968,14 +1001,10 @@ export default class RulesService { projectRuleId: string, status: RuleStatus ): Promise { - if (!(await userHasPermission(submitter.userId, organization.organizationId, isLeadership))) { - throw new AccessDeniedException('You do not have permissions to update rule status'); - } - const projectRule = await prisma.project_Rule.findUnique({ where: { projectRuleId }, include: { - project: { include: { wbsElement: true } }, + project: { include: { wbsElement: true, teams: getTeamPreviewQueryArgs(organization.organizationId) } }, rule: { include: { ruleset: { include: { car: { include: { wbsElement: true } } } } } } } }); @@ -995,6 +1024,11 @@ export default class RulesService { throw new InvalidOrganizationException('Project Rule'); } + const hasOrgWidePermission = await userHasPermission(submitter.userId, organization.organizationId, isLeadership); + if (!hasOrgWidePermission && !isUserPartOfTeams(projectRule.project.teams, submitter)) { + throw new AccessDeniedException('You do not have permissions to update rule status for this project'); + } + const childProjectRuleCount = await prisma.project_Rule.count({ where: { projectId: projectRule.projectId, dateDeleted: null, rule: { parentRuleId: projectRule.ruleId } } }); @@ -1028,15 +1062,15 @@ export default class RulesService { /** * Resets every rule's general-view status back to Pending, for a whole ruleset. * Does not affect any rule's status within a project. Never creates history entries, - * since reverting to PENDING is not tracked. + * since reverting to PENDING is not tracked. Only heads and above can do this. * @param submitter the user resetting the statuses * @param organization the organization of the ruleset * @param rulesetId the id of the ruleset to reset * @returns the number of rules that were reset */ static async resetRulesetStatuses(submitter: User, organization: Organization, rulesetId: string): Promise { - if (!(await userHasPermission(submitter.userId, organization.organizationId, isLeadership))) { - throw new AccessDeniedException('You do not have permissions to update rule status'); + if (!(await userHasPermission(submitter.userId, organization.organizationId, isHead))) { + throw new AccessDeniedException('You do not have permissions to reset rule status'); } const ruleset = await prisma.ruleset.findUnique({ @@ -1068,7 +1102,7 @@ export default class RulesService { * Resets every project rule's status back to Pending, for a single project, scoped to a * single ruleset (a project can have rules from multiple ruleset types). Does not affect * any rule's general-view status, or its status in any other project. Never creates history - * entries, since reverting to PENDING is not tracked. + * entries, since reverting to PENDING is not tracked. Allowed for leadership and up. * @param submitter the user resetting the statuses * @param organization the organization of the project and ruleset * @param rulesetId the ruleset to scope the reset to @@ -1082,7 +1116,7 @@ export default class RulesService { projectId: string ): Promise { if (!(await userHasPermission(submitter.userId, organization.organizationId, isLeadership))) { - throw new AccessDeniedException('You do not have permissions to update rule status'); + throw new AccessDeniedException('You do not have permissions to reset project rule status'); } const ruleset = await prisma.ruleset.findUnique({ @@ -1182,9 +1216,9 @@ export default class RulesService { * */ static async toggleRuleTeam(ruleId: string, teamId: string, user: User, org: Organization) { - // Checks that the user is not a guest - if (!(await userHasPermission(user.userId, org.organizationId, notGuest))) { - throw new AccessDeniedGuestException('Toggle Rule Team'); + // Checks that the user is leadership and above + if (!(await userHasPermission(user.userId, org.organizationId, isLeadership))) { + throw new AccessDeniedException('Only leadership and above can assign rules to teams'); } // Checks that the rule exists and is not deleted @@ -1513,11 +1547,16 @@ export default class RulesService { /** * Gets all subrules of a specific rule. + * @param user the user requesting the child rules * @param ruleId the ID of the parent rule * @param organization the organization the rule belongs to * @returns an array of all child rules (the Rule object) */ - static async getChildRules(ruleId: string, organization: Organization): Promise { + static async getChildRules(user: User, ruleId: string, organization: Organization): Promise { + if (!(await userHasPermission(user.userId, organization.organizationId, notGuest))) { + throw new AccessDeniedGuestException('view rules'); + } + // Verify the parent rule exists and belongs to the organization const parentRule = await prisma.rule.findUnique({ where: { ruleId }, @@ -1561,12 +1600,22 @@ export default class RulesService { /** * Gets rules assignable to a project that are not already assigned to it. * A project can belong to multiple teams, so rules from all of its teams are shown. + * @param user the user requesting the unassigned rules * @param rulesetId ruleset the rules are in * @param projectId the project the rules would be assigned to * @param organizationId the organization id * @returns the rules on one of the project's teams that are not already actively assigned to this project */ - static async getUnassignedRulesForProjectRuleset(rulesetId: string, projectId: string, organizationId: string) { + static async getUnassignedRulesForProjectRuleset( + user: User, + rulesetId: string, + projectId: string, + organizationId: string + ) { + if (!(await userHasPermission(user.userId, organizationId, notGuest))) { + throw new AccessDeniedGuestException('view unassigned rules'); + } + const ruleset = await prisma.ruleset.findUnique({ where: { rulesetId }, select: { @@ -1635,12 +1684,22 @@ export default class RulesService { /** * Gets all rules associated with a specific project and ruleset + * @param user the user requesting the project rules * @param rulesetId the id of the ruleset * @param projectId the id of the project * @param organization the organization the project and ruleset belong to * @returns Array of ProjectRule objects */ - static async getProjectRules(rulesetId: string, projectId: string, organization: Organization): Promise { + static async getProjectRules( + user: User, + rulesetId: string, + projectId: string, + organization: Organization + ): Promise { + if (!(await userHasPermission(user.userId, organization.organizationId, notGuest))) { + throw new AccessDeniedGuestException('view project rules'); + } + const ruleset = await prisma.ruleset.findUnique({ where: { rulesetId }, include: { @@ -1701,10 +1760,15 @@ export default class RulesService { /** * Gets all rules with no parent id + * @param user the user requesting the top-level rules * @param rulesetId id of ruleset * @returns an array of rules with no parent Id */ - static async getTopLevelRules(rulesetId: string, organizationId: string) { + static async getTopLevelRules(user: User, rulesetId: string, organizationId: string) { + if (!(await userHasPermission(user.userId, organizationId, notGuest))) { + throw new AccessDeniedGuestException('view rules'); + } + const ruleset = await prisma.ruleset.findUnique({ where: { rulesetId }, select: { @@ -1744,11 +1808,16 @@ export default class RulesService { /** * Gets every rule in a ruleset in a single query instead of walking it level by level. + * @param user the user requesting the rules * @param rulesetId id of ruleset * @param organizationId the organization the ruleset belongs to * @returns a flat array of every rule in the ruleset */ - static async getAllRulesForRuleset(rulesetId: string, organizationId: string): Promise { + static async getAllRulesForRuleset(user: User, rulesetId: string, organizationId: string): Promise { + if (!(await userHasPermission(user.userId, organizationId, notGuest))) { + throw new AccessDeniedGuestException('view rules'); + } + const ruleset = await prisma.ruleset.findUnique({ where: { rulesetId }, select: { diff --git a/src/backend/tests/unit/rule.test.ts b/src/backend/tests/unit/rule.test.ts index 6a827b3d29..152488030d 100644 --- a/src/backend/tests/unit/rule.test.ts +++ b/src/backend/tests/unit/rule.test.ts @@ -6,6 +6,7 @@ import { wonderwomanGuest, batmanAppAdmin, aquamanLeadership, + greenlanternHead, alfred, flashAdmin } from '../test-data/users.test-data'; @@ -166,7 +167,14 @@ describe('Create Rules Tests', () => { it('fails when guest tries to create a rule', async () => { await expect(RulesService.createRule(wonderwoman, 'T.1.1', 'Some rule', rulesetId, organization)).rejects.toThrow( - new AccessDeniedException('Only members and above can create rules') + new AccessDeniedException('Only leadership and above can create rules') + ); + }); + + it('fails when a member tries to create a rule', async () => { + const member = await createTestUser(financeMember, orgId); + await expect(RulesService.createRule(member, 'T.1.1', 'Some rule', rulesetId, organization)).rejects.toThrow( + new AccessDeniedException('Only leadership and above can create rules') ); }); @@ -267,10 +275,9 @@ describe('Create Rules Tests', () => { ).rejects.toThrow(new DeletedException('Referenced Rule', rule1.ruleId)); }); - it('allows members and above to create rules', async () => { - await RulesService.createRule(aquaman, 'T.1.1', 'Member created rule', rulesetId, organization); - await RulesService.createRule(aquaman, 'T.1.2', 'Leadership created rule', rulesetId, organization); - await RulesService.createRule(superman, 'T.1.3', 'Admin created rule', rulesetId, organization); + it('allows leadership and above to create rules', async () => { + await RulesService.createRule(aquaman, 'T.1.1', 'Leadership created rule', rulesetId, organization); + await RulesService.createRule(superman, 'T.1.2', 'Admin created rule', rulesetId, organization); }); describe('Create ruleset', () => { @@ -410,7 +417,7 @@ describe('Create Rules Tests', () => { await RulesService.createProjectRule(aquaman, organization, grandchild.ruleId, project.projectId); - const projectRules = await RulesService.getProjectRules(rulesetId, project.projectId, organization); + const projectRules = await RulesService.getProjectRules(aquaman, rulesetId, project.projectId, organization); const assignedRuleIds = projectRules.map((pr) => pr.rule.ruleId); expect(assignedRuleIds).toHaveLength(3); // grandchild, child, topLevelRule expect(assignedRuleIds).toEqual(expect.arrayContaining([topLevelRule.ruleId, child.ruleId, grandchild.ruleId])); @@ -445,7 +452,7 @@ describe('Create Rules Tests', () => { // adding sibling must not error or duplicate the already-present parent/root rules await RulesService.createProjectRule(aquaman, organization, grandchild2.ruleId, project.projectId); - const projectRules = await RulesService.getProjectRules(rulesetId, project.projectId, organization); + const projectRules = await RulesService.getProjectRules(aquaman, rulesetId, project.projectId, organization); const assignedRuleIds = projectRules.map((pr) => pr.rule.ruleId); expect(assignedRuleIds).toHaveLength(4); // grandchild1, grandchild2, child, topLevelRule expect(assignedRuleIds).toEqual( @@ -475,7 +482,7 @@ describe('Create Rules Tests', () => { await RulesService.createProjectRule(aquaman, organization, child.ruleId, project.projectId); - const projectRules = await RulesService.getProjectRules(rulesetId, project.projectId, organization); + const projectRules = await RulesService.getProjectRules(aquaman, rulesetId, project.projectId, organization); const assignedRuleIds = projectRules.map((pr) => pr.rule.ruleId); expect(assignedRuleIds).toHaveLength(2); // child and topLevelRule, not grandchild expect(assignedRuleIds).toEqual(expect.arrayContaining([topLevelRule.ruleId, child.ruleId])); @@ -511,7 +518,7 @@ describe('Create Rules Tests', () => { ).rejects.toThrow(new DeletedException('Rule', child.ruleId)); // nothing should have been assigned (not the grandchild, the deleted parent, or the root) - const projectRules = await RulesService.getProjectRules(rulesetId, project.projectId, organization); + const projectRules = await RulesService.getProjectRules(aquaman, rulesetId, project.projectId, organization); expect(projectRules).toHaveLength(0); const grandchildProjectRule = await prisma.project_Rule.findUnique({ @@ -577,7 +584,7 @@ describe('Create Rules Tests', () => { describe('Get rulesets by ruleset type', () => { it('Successful get rulesets by ruleset types', async () => { - const rulesets = await RulesService.getRulesetsByRulesetType(rulesetType.rulesetTypeId, orgId); + const rulesets = await RulesService.getRulesetsByRulesetType(aquaman, rulesetType.rulesetTypeId, orgId); expect(rulesets.length).toBe(1); expect(rulesets[0].name).toBe('2025 FSAE Rules'); expect(rulesets[0].active).toBeTruthy(); @@ -592,7 +599,7 @@ describe('Create Rules Tests', () => { }); await RulesService.deleteRuleset(rulesetId, batman.userId, orgId); - const rulesets = await RulesService.getRulesetsByRulesetType(rulesetType.rulesetTypeId, orgId); + const rulesets = await RulesService.getRulesetsByRulesetType(aquaman, rulesetType.rulesetTypeId, orgId); expect(rulesets.length).toBe(0); }); @@ -607,7 +614,7 @@ describe('Create Rules Tests', () => { createdBy: { connect: { userId: batman.userId } } } }); - const rulesets = await RulesService.getRulesetsByRulesetType(rulesetType.rulesetTypeId, orgId); + const rulesets = await RulesService.getRulesetsByRulesetType(aquaman, rulesetType.rulesetTypeId, orgId); expect(rulesets.length).toBe(2); expect(rulesets[0].name).toBe('2025 FSAE Rules2'); expect(rulesets[1].name).toBe('2025 FSAE Rules'); @@ -640,15 +647,25 @@ describe('Create Rules Tests', () => { }); // 2 total rulesets for this type - const allRulesets = await RulesService.getRulesetsByRulesetType(rulesetType.rulesetTypeId, orgId); + const allRulesets = await RulesService.getRulesetsByRulesetType(aquaman, rulesetType.rulesetTypeId, orgId); expect(allRulesets.length).toBe(2); // 1 ruleset when filtered to the original car - const originalCarRulesets = await RulesService.getRulesetsByRulesetType(rulesetType.rulesetTypeId, orgId, carId); + const originalCarRulesets = await RulesService.getRulesetsByRulesetType( + aquaman, + rulesetType.rulesetTypeId, + orgId, + carId + ); expect(originalCarRulesets.length).toBe(1); expect(originalCarRulesets[0].rulesetId).toBe(rulesetId); - const otherCarRulesets = await RulesService.getRulesetsByRulesetType(rulesetType.rulesetTypeId, orgId, otherCar.carId); + const otherCarRulesets = await RulesService.getRulesetsByRulesetType( + aquaman, + rulesetType.rulesetTypeId, + orgId, + otherCar.carId + ); // 1 ruleset when filtered to the other car expect(otherCarRulesets.length).toBe(1); @@ -661,7 +678,7 @@ describe('Create Rules Tests', () => { const parentRule = await RulesService.createRule(batman, 'T.1', 'Parent Rule', rulesetId, organization); await RulesService.createRule(batman, 'T.1.1', 'Child Rule 1', rulesetId, organization, parentRule.ruleId); await RulesService.createRule(batman, 'T.1.2', 'Child Rule 2', rulesetId, organization, parentRule.ruleId); - const childRules = await RulesService.getChildRules(parentRule.ruleId, organization); + const childRules = await RulesService.getChildRules(aquaman, parentRule.ruleId, organization); expect(childRules.length).toBe(2); expect(childRules[0].ruleCode).toBe('T.1.1'); expect(childRules[1].ruleCode).toBe('T.1.2'); @@ -678,24 +695,24 @@ describe('Create Rules Tests', () => { parentRule.ruleId ); await RulesService.deleteRule(childRule.ruleId, batman, organization); - const childRules = await RulesService.getChildRules(parentRule.ruleId, organization); + const childRules = await RulesService.getChildRules(aquaman, parentRule.ruleId, organization); expect(childRules.length).toBe(0); }); it('Successfully gets child rules after adding child rule', async () => { const parentRule = await RulesService.createRule(batman, 'T.3', 'Parent Rule', rulesetId, organization); await RulesService.createRule(batman, 'T.3.1', 'Child Rule 1', rulesetId, organization, parentRule.ruleId); - const childRulesAfterOne = await RulesService.getChildRules(parentRule.ruleId, organization); + const childRulesAfterOne = await RulesService.getChildRules(aquaman, parentRule.ruleId, organization); expect(childRulesAfterOne.length).toBe(1); await RulesService.createRule(batman, 'T.3.2', 'Child Rule 2', rulesetId, organization, parentRule.ruleId); - const childRulesAfterTwo = await RulesService.getChildRules(parentRule.ruleId, organization); + const childRulesAfterTwo = await RulesService.getChildRules(aquaman, parentRule.ruleId, organization); expect(childRulesAfterTwo.length).toBe(2); expect(childRulesAfterTwo[0].ruleCode).toBe('T.3.1'); expect(childRulesAfterTwo[1].ruleCode).toBe('T.3.2'); }); it('Fails if parent rule does not exist', async () => { - await expect(async () => await RulesService.getChildRules('fake-rule-id', organization)).rejects.toThrow( + await expect(async () => await RulesService.getChildRules(aquaman, 'fake-rule-id', organization)).rejects.toThrow( new NotFoundException('Rule', 'fake-rule-id') ); }); @@ -703,7 +720,7 @@ describe('Create Rules Tests', () => { it('Fails if parent rule is deleted', async () => { const parentRule = await RulesService.createRule(batman, 'T.4', 'Parent Rule', rulesetId, organization); await RulesService.deleteRule(parentRule.ruleId, batman, organization); - await expect(async () => await RulesService.getChildRules(parentRule.ruleId, organization)).rejects.toThrow( + await expect(async () => await RulesService.getChildRules(aquaman, parentRule.ruleId, organization)).rejects.toThrow( new DeletedException('Rule', parentRule.ruleId) ); }); @@ -765,9 +782,9 @@ describe('Create Rules Tests', () => { createdByUserId: otherBatman.userId } }); - await expect(async () => await RulesService.getChildRules(otherParentRule.ruleId, organization)).rejects.toThrow( - new InvalidOrganizationException('Rule') - ); + await expect( + async () => await RulesService.getChildRules(aquaman, otherParentRule.ruleId, organization) + ).rejects.toThrow(new InvalidOrganizationException('Rule')); }); }); describe('Update ruleset status', () => { @@ -925,6 +942,7 @@ describe('Rule Tests', () => { let orgId: string; let otherOrg: Organization; let admin: User; + let head: User; let nonLeadership: User; let guest: User; let project: Project; @@ -936,6 +954,7 @@ describe('Rule Tests', () => { organization = await createTestOrganization(); orgId = organization.organizationId; admin = await createTestUser(supermanAdmin, organization.organizationId); + head = await createTestUser(greenlanternHead, organization.organizationId); nonLeadership = await createTestUser(financeMember, organization.organizationId); guest = await createTestUser(wonderwomanGuest, organization.organizationId); project = await createTestProject(admin, organization.organizationId); @@ -1268,7 +1287,7 @@ describe('Rule Tests', () => { expect(updatedProjectRule.statusUpdatedAt).toBeInstanceOf(Date); }); - it('Set project rule status fails if user does not have permission', async () => { + it('Set project rule status fails if a member is not on the project team', async () => { const car = await createUniqueCar(orgId); const { topLevelRule } = await setupRules(car); const project = await createTestProject(admin, orgId, testTeam.teamId, car.carId, car.wbsElement.carNumber); @@ -1278,7 +1297,7 @@ describe('Rule Tests', () => { await expect( async () => await RulesService.setProjectRuleStatus(nonLeadership, organization, projectRule.projectRuleId, RuleStatus.PASS) - ).rejects.toThrow(new AccessDeniedException('You do not have permissions to update rule status')); + ).rejects.toThrow(new AccessDeniedException('You do not have permissions to update rule status for this project')); }); it('Set project rule status fails if the rule has sub-rules assigned to the project', async () => { @@ -1312,7 +1331,7 @@ describe('Rule Tests', () => { await RulesService.setProjectRuleStatus(admin, organization, projectRule1.projectRuleId, RuleStatus.PASS); await RulesService.setRuleStatus(admin, organization, leafRule1.ruleId, RuleStatus.FAIL); - const projectRules2 = await RulesService.getProjectRules(ruleset1.rulesetId, project2.projectId, organization); + const projectRules2 = await RulesService.getProjectRules(admin, ruleset1.rulesetId, project2.projectId, organization); const rule2Entry = projectRules2.find((pr) => pr.projectRuleId === projectRule2.projectRuleId); expect(rule2Entry?.status).toBe(RuleStatus.PENDING); @@ -1351,7 +1370,11 @@ describe('Rule Tests', () => { // so FAIL rolls all the way up the chain await RulesService.setRuleStatus(admin, organization, childRule.ruleId, RuleStatus.FAIL); - const rulesBeforeDelete = await RulesService.getAllRulesForRuleset(ruleset1.rulesetId, organization.organizationId); + const rulesBeforeDelete = await RulesService.getAllRulesForRuleset( + admin, + ruleset1.rulesetId, + organization.organizationId + ); expect(rulesBeforeDelete.find((r) => r.ruleId === parentRule.ruleId)?.status).toBe(RuleStatus.FAIL); expect(rulesBeforeDelete.find((r) => r.ruleId === grandparentRule.ruleId)?.status).toBe(RuleStatus.FAIL); @@ -1359,7 +1382,11 @@ describe('Rule Tests', () => { // status should reset to Pending, and that change should keep propagating up to grandparentRule await RulesService.deleteRule(childRule.ruleId, admin, organization); - const rulesAfterDelete = await RulesService.getAllRulesForRuleset(ruleset1.rulesetId, organization.organizationId); + const rulesAfterDelete = await RulesService.getAllRulesForRuleset( + admin, + ruleset1.rulesetId, + organization.organizationId + ); const updatedParent = rulesAfterDelete.find((r) => r.ruleId === parentRule.ruleId); const updatedGrandparent = rulesAfterDelete.find((r) => r.ruleId === grandparentRule.ruleId); @@ -1394,7 +1421,7 @@ describe('Rule Tests', () => { // marking the child Pass rolls parent rule up to Pass too await RulesService.setRuleStatus(admin, organization, childRule.ruleId, RuleStatus.PASS); - const rules = await RulesService.getAllRulesForRuleset(ruleset1.rulesetId, organization.organizationId); + const rules = await RulesService.getAllRulesForRuleset(admin, ruleset1.rulesetId, organization.organizationId); const parentRule = rules.find((r) => r.ruleId === rule.ruleId); expect(parentRule!.status).toBe(RuleStatus.PASS); @@ -1407,7 +1434,7 @@ describe('Rule Tests', () => { // deleting the only child makes rule a leaf again, so it should reset to Pending await RulesService.deleteRule(childRule.ruleId, admin, organization); - const allRules = await RulesService.getAllRulesForRuleset(ruleset1.rulesetId, organization.organizationId); + const allRules = await RulesService.getAllRulesForRuleset(admin, ruleset1.rulesetId, organization.organizationId); const updatedRule = allRules.find((r) => r.ruleId === rule.ruleId); expect(updatedRule?.status).toBe(RuleStatus.PENDING); @@ -1463,6 +1490,7 @@ describe('Rule Tests', () => { await RulesService.setProjectRuleStatus(admin, organization, childProjectRule.projectRuleId, RuleStatus.FAIL); const projectRulesBeforeDelete = await RulesService.getProjectRules( + admin, ruleset1.rulesetId, project.projectId, organization @@ -1475,6 +1503,7 @@ describe('Rule Tests', () => { await RulesService.deleteProjectRule(childProjectRule.projectRuleId, admin, organization); const projectRulesAfterDelete = await RulesService.getProjectRules( + admin, ruleset1.rulesetId, project.projectId, organization @@ -1500,7 +1529,7 @@ describe('Rule Tests', () => { expect(count).toBe(2); - const rules = await RulesService.getAllRulesForRuleset(ruleset1.rulesetId, organization.organizationId); + const rules = await RulesService.getAllRulesForRuleset(admin, ruleset1.rulesetId, organization.organizationId); const updatedTopLevel = rules.find((r) => r.ruleId === topLevelRule.ruleId); const updatedLeaf = rules.find((r) => r.ruleId === leafRule1.ruleId); @@ -1529,7 +1558,18 @@ describe('Rule Tests', () => { await expect( async () => await RulesService.resetRulesetStatuses(nonLeadership, organization, ruleset1.rulesetId) - ).rejects.toThrow(new AccessDeniedException('You do not have permissions to update rule status')); + ).rejects.toThrow(new AccessDeniedException('You do not have permissions to reset rule status')); + }); + + it('Reset status succeeds for a head (non-admin) user', async () => { + const car = await createUniqueCar(orgId); + const { ruleset1, leafRule1 } = await setupRules(car); + + await RulesService.setRuleStatus(admin, organization, leafRule1.ruleId, RuleStatus.PASS); + + const count = await RulesService.resetRulesetStatuses(head, organization, ruleset1.rulesetId); + + expect(count).toBe(1); }); it('Reset status only affects the given ruleset', async () => { @@ -1550,7 +1590,7 @@ describe('Rule Tests', () => { await RulesService.resetRulesetStatuses(admin, organization, ruleset1.rulesetId); - const rules = await RulesService.getAllRulesForRuleset(ruleset2.rulesetId, organization.organizationId); + const rules = await RulesService.getAllRulesForRuleset(admin, ruleset2.rulesetId, organization.organizationId); const untouchedRule = rules.find((r) => r.ruleId === otherRule.ruleId); expect(untouchedRule?.status).toBe(RuleStatus.PASS); @@ -1580,7 +1620,7 @@ describe('Rule Tests', () => { expect(count).toBe(1); - const projectRules = await RulesService.getProjectRules(ruleset1.rulesetId, project.projectId, organization); + const projectRules = await RulesService.getProjectRules(admin, ruleset1.rulesetId, project.projectId, organization); const updated = projectRules.find((pr) => pr.projectRuleId === projectRule.projectRuleId); expect(updated?.status).toBe(RuleStatus.PENDING); @@ -1614,7 +1654,7 @@ describe('Rule Tests', () => { await expect( async () => await RulesService.resetProjectRuleStatuses(nonLeadership, organization, ruleset1.rulesetId, project.projectId) - ).rejects.toThrow(new AccessDeniedException('You do not have permissions to update rule status')); + ).rejects.toThrow(new AccessDeniedException('You do not have permissions to reset project rule status')); }); it('Reset project status only affects the given project', async () => { @@ -1641,7 +1681,7 @@ describe('Rule Tests', () => { await RulesService.resetProjectRuleStatuses(admin, organization, ruleset1.rulesetId, project1.projectId); - const project2Rules = await RulesService.getProjectRules(ruleset1.rulesetId, project2.projectId, organization); + const project2Rules = await RulesService.getProjectRules(admin, ruleset1.rulesetId, project2.projectId, organization); const untouched = project2Rules.find((pr) => pr.projectRuleId === projectRule2.projectRuleId); expect(untouched?.status).toBe(RuleStatus.PASS); @@ -1671,15 +1711,43 @@ describe('Rule Tests', () => { await RulesService.resetProjectRuleStatuses(admin, organization, ruleset1.rulesetId, project.projectId); - const projectRules = await RulesService.getProjectRules(ruleset2.rulesetId, project.projectId, organization); + const projectRules = await RulesService.getProjectRules(admin, ruleset2.rulesetId, project.projectId, organization); const untouched = projectRules.find((pr) => pr.projectRuleId === projectRule2.projectRuleId); expect(untouched?.status).toBe(RuleStatus.PASS); }); + + it('Set project rule status succeeds if a member is on the project team', async () => { + const car = await createUniqueCar(orgId); + const { topLevelRule } = await setupRules(car); + const memberProject = await createTestProject(admin, orgId, testTeam.teamId, car.carId, car.wbsElement.carNumber); + await RulesService.toggleRuleTeam(topLevelRule.ruleId, testTeam.teamId, admin, organization); + const projectRule = await RulesService.createProjectRule( + admin, + organization, + topLevelRule.ruleId, + memberProject.projectId + ); + await prisma.team.update({ + where: { teamId: testTeam.teamId }, + data: { members: { connect: { userId: nonLeadership.userId } } } + }); + + const updatedProjectRule = await RulesService.setProjectRuleStatus( + nonLeadership, + organization, + projectRule.projectRuleId, + RuleStatus.PASS + ); + + expect(updatedProjectRule.status).toBe(RuleStatus.PASS); + expect(updatedProjectRule.statusUpdatedBy?.firstName).toBe(nonLeadership.firstName); + expect(updatedProjectRule.statusUpdatedBy?.lastName).toBe(nonLeadership.lastName); + }); }); describe('Edit Rule', () => { - it('Fails if user is not an admin', async () => { + it('Fails if user is not leadership or above', async () => { const car = await createUniqueCar(orgId); const { leafRule1 } = await setupRules(car); await expect( @@ -1692,7 +1760,7 @@ describe('Rule Tests', () => { ['newfile'], organization ) - ).rejects.toThrow(new AccessDeniedAdminOnlyException('edit a rule')); + ).rejects.toThrow(new AccessDeniedException('Only leadership and above can edit a rule')); }); it('Fails if rule doesn`t exist', async () => { @@ -1891,7 +1959,48 @@ describe('Rule Tests', () => { await expect( async () => await RulesService.deleteRuleset(ruleset1.rulesetId, nonLeadership.userId, organization.organizationId) - ).rejects.toThrow(new AccessDeniedException('Only admins can delete a ruleset.')); + ).rejects.toThrow(new AccessDeniedException('You do not have permissions to delete this ruleset.')); + }); + it('Delete ruleset succeeds if a leadership user created the ruleset', async () => { + const car = await createUniqueCar(orgId); + const leadershipUser = await createTestUser(aquamanLeadership, orgId); + const ruleset = await prisma.ruleset.create({ + data: { + name: 'Leadership Ruleset', + fileId: 'leadership-created-ruleset-file', + active: false, + car: { connect: { carId: car.carId } }, + createdBy: { connect: { userId: leadershipUser.userId } }, + rulesetType: { connect: { rulesetTypeId: fsaeRulesetType.rulesetTypeId } } + } + }); + + const deleted = await RulesService.deleteRuleset( + ruleset.rulesetId, + leadershipUser.userId, + organization.organizationId + ); + + expect(deleted).toBeDefined(); + expect(deleted.rulesetId).toBe(ruleset.rulesetId); + }); + it('Delete ruleset fails if a leadership user did not create the ruleset', async () => { + const car = await createUniqueCar(orgId); + const leadershipUser = await createTestUser(aquamanLeadership, orgId); + const ruleset = await prisma.ruleset.create({ + data: { + name: 'Admin Created Ruleset', + fileId: 'admin-created-ruleset-file', + active: false, + car: { connect: { carId: car.carId } }, + createdBy: { connect: { userId: admin.userId } }, + rulesetType: { connect: { rulesetTypeId: fsaeRulesetType.rulesetTypeId } } + } + }); + + await expect( + async () => await RulesService.deleteRuleset(ruleset.rulesetId, leadershipUser.userId, organization.organizationId) + ).rejects.toThrow(new AccessDeniedException('You do not have permissions to delete this ruleset.')); }); it('Delete ruleset fails if ruleset was already deleted', async () => { const car = await createUniqueCar(orgId); @@ -1917,7 +2026,7 @@ describe('Rule Tests', () => { describe('Get all ruleset types', () => { it('Successful get all ruleset types', async () => { - const rulesetTypes = await RulesService.getAllRulesetTypes(organization); + const rulesetTypes = await RulesService.getAllRulesetTypes(admin, organization); expect(rulesetTypes.length).toEqual(2); expect(rulesetTypes[0].name).toEqual('FSAE'); expect(rulesetTypes[1].name).toEqual('Ruleset Type with no Active Rulesets or Anything'); @@ -1930,7 +2039,7 @@ describe('Rule Tests', () => { organizationId: orgId } }); - const rulesetTypes = await RulesService.getAllRulesetTypes(organization); + const rulesetTypes = await RulesService.getAllRulesetTypes(admin, organization); expect(rulesetTypes.length).toEqual(3); expect(rulesetTypes[2].name).toEqual('FSAE2'); }); @@ -1943,7 +2052,7 @@ describe('Rule Tests', () => { deletedByUserId: admin.userId } }); - const rulesetTypes = await RulesService.getAllRulesetTypes(organization); + const rulesetTypes = await RulesService.getAllRulesetTypes(admin, organization); expect(rulesetTypes.length).toEqual(1); }); }); @@ -2036,7 +2145,14 @@ describe('Rule Tests', () => { const { topLevelRule } = await setupRules(car); await expect( async () => await RulesService.toggleRuleTeam(topLevelRule.ruleId, '', guest, organization) - ).rejects.toThrow(new AccessDeniedGuestException('Toggle Rule Team')); + ).rejects.toThrow(new AccessDeniedException('Only leadership and above can assign rules to teams')); + }); + it('Fails if user is a member', async () => { + const car = await createUniqueCar(orgId); + const { topLevelRule } = await setupRules(car); + await expect( + async () => await RulesService.toggleRuleTeam(topLevelRule.ruleId, '', nonLeadership, organization) + ).rejects.toThrow(new AccessDeniedException('Only leadership and above can assign rules to teams')); }); it('Fails if rule does not exist', async () => { await expect(async () => await RulesService.toggleRuleTeam('fake-rule-id', '', admin, organization)).rejects.toThrow( @@ -2197,13 +2313,13 @@ describe('Rule Tests', () => { }); it('Successfully deletes the ruleset type', async () => { - let rulesetTypes = await RulesService.getAllRulesetTypes(organization); + let rulesetTypes = await RulesService.getAllRulesetTypes(admin, organization); expect(rulesetTypes.length).toEqual(2); const appAdmin = await createTestUser(batmanAppAdmin, orgId); const result = await RulesService.deleteRulesetType(appAdmin, fsaeRulesetType.rulesetTypeId, organization); - rulesetTypes = await RulesService.getAllRulesetTypes(organization); + rulesetTypes = await RulesService.getAllRulesetTypes(admin, organization); expect(rulesetTypes.length).toEqual(1); @@ -2224,10 +2340,14 @@ describe('Rule Tests', () => { } }); - let rulesets = await RulesService.getRulesetsByRulesetType(fsaeRulesetType2WithRevisionFiles.rulesetTypeId, orgId); + let rulesets = await RulesService.getRulesetsByRulesetType( + admin, + fsaeRulesetType2WithRevisionFiles.rulesetTypeId, + orgId + ); expect(rulesets.length).toBe(1); await RulesService.deleteRulesetType(admin, fsaeRulesetType2WithRevisionFiles.rulesetTypeId, organization); - rulesets = await RulesService.getRulesetsByRulesetType(fsaeRulesetType2WithRevisionFiles.rulesetTypeId, orgId); + rulesets = await RulesService.getRulesetsByRulesetType(admin, fsaeRulesetType2WithRevisionFiles.rulesetTypeId, orgId); expect(rulesets.length).toBe(0); }); }); @@ -2254,6 +2374,7 @@ describe('Rule Tests', () => { }); await expect( RulesService.getUnassignedRulesForProjectRuleset( + admin, otherRuleset.rulesetId, project.projectId, organization.organizationId @@ -2266,6 +2387,7 @@ describe('Rule Tests', () => { const otherOrgProject = await createTestProject(admin, otherOrg.organizationId); await expect( RulesService.getUnassignedRulesForProjectRuleset( + admin, ruleset1.rulesetId, otherOrgProject.projectId, organization.organizationId @@ -2275,6 +2397,7 @@ describe('Rule Tests', () => { it('fails if ruleset does not exist', async () => { await expect( RulesService.getUnassignedRulesForProjectRuleset( + admin, 'nonexistent-ruleset-id', project.projectId, organization.organizationId @@ -2285,7 +2408,12 @@ describe('Rule Tests', () => { const car = await createUniqueCar(orgId); const { ruleset1 } = await setupRules(car); await expect( - RulesService.getUnassignedRulesForProjectRuleset(ruleset1.rulesetId, 'fake-project-id', organization.organizationId) + RulesService.getUnassignedRulesForProjectRuleset( + admin, + ruleset1.rulesetId, + 'fake-project-id', + organization.organizationId + ) ).rejects.toThrow(new NotFoundException('Project', 'fake-project-id')); }); it("successfully returns rules on the project's teams that are not already assigned to the project", async () => { @@ -2315,6 +2443,7 @@ describe('Rule Tests', () => { } }); const rules = await RulesService.getUnassignedRulesForProjectRuleset( + admin, ruleset1.rulesetId, project.projectId, organization.organizationId @@ -2349,6 +2478,7 @@ describe('Rule Tests', () => { await RulesService.toggleRuleTeam(leafRule1.ruleId, secondTeam.teamId, admin, organization); const rules = await RulesService.getUnassignedRulesForProjectRuleset( + admin, ruleset1.rulesetId, project.projectId, organization.organizationId @@ -2363,6 +2493,7 @@ describe('Rule Tests', () => { const { ruleset1 } = await setupRules(car); const project = await createTestProject(admin, orgId, testTeam.teamId, car.carId, car.wbsElement.carNumber); const rules = await RulesService.getUnassignedRulesForProjectRuleset( + admin, ruleset1.rulesetId, project.projectId, organization.organizationId @@ -2379,7 +2510,12 @@ describe('Rule Tests', () => { await RulesService.toggleRuleTeam(topLevelRule.ruleId, testTeam.teamId, admin, organization); const projectRule = await RulesService.createProjectRule(admin, organization, topLevelRule.ruleId, project.projectId); - const projectRules = await RulesService.getProjectRules(topLevelRule.rulesetId, projectRule.projectId, organization); + const projectRules = await RulesService.getProjectRules( + admin, + topLevelRule.rulesetId, + projectRule.projectId, + organization + ); expect(projectRules.length).toBe(1); expect(projectRules[0].projectRuleId).toBe(projectRule.projectRuleId); @@ -2390,7 +2526,12 @@ describe('Rule Tests', () => { const car = await createUniqueCar(orgId); const { topLevelRule } = await setupRules(car); - const projectRules = await RulesService.getProjectRules(topLevelRule.rulesetId, project.projectId, organization); + const projectRules = await RulesService.getProjectRules( + admin, + topLevelRule.rulesetId, + project.projectId, + organization + ); expect(projectRules.length).toBe(0); }); @@ -2407,13 +2548,13 @@ describe('Rule Tests', () => { }); await expect( - async () => await RulesService.getProjectRules(topLevelRule.rulesetId, project.projectId, organization) + async () => await RulesService.getProjectRules(admin, topLevelRule.rulesetId, project.projectId, organization) ).rejects.toThrow(new DeletedException('Project', project.projectId)); }); it('Get project rules fails if ruleset does not exist', async () => { await expect( - async () => await RulesService.getProjectRules('fake-ruleset-id', project.projectId, organization) + async () => await RulesService.getProjectRules(admin, 'fake-ruleset-id', project.projectId, organization) ).rejects.toThrow(new NotFoundException('Ruleset', 'fake-ruleset-id')); }); @@ -2422,7 +2563,7 @@ describe('Rule Tests', () => { const { topLevelRule } = await setupRules(car); await expect( - async () => await RulesService.getProjectRules(topLevelRule.rulesetId, 'fake-project-id', organization) + async () => await RulesService.getProjectRules(admin, topLevelRule.rulesetId, 'fake-project-id', organization) ).rejects.toThrow(new NotFoundException('Project', 'fake-project-id')); }); @@ -2435,17 +2576,26 @@ describe('Rule Tests', () => { }); await expect( - async () => await RulesService.getProjectRules(topLevelRule.rulesetId, project.projectId, organization) + async () => await RulesService.getProjectRules(admin, topLevelRule.rulesetId, project.projectId, organization) ).rejects.toThrow(new DeletedException('Ruleset', topLevelRule.rulesetId)); }); }); describe('Get Top Level Rules', () => { + it('Fails if user is a guest', async () => { + const car = await createUniqueCar(orgId); + const { ruleset1 } = await setupRules(car); + + await expect(RulesService.getTopLevelRules(guest, ruleset1.rulesetId, organization.organizationId)).rejects.toThrow( + new AccessDeniedGuestException('view rules') + ); + }); + it('Successful get all rules with no parent id', async () => { const car = await createUniqueCar(orgId); const { ruleset1, topLevelRule } = await setupRules(car); - const rules = await RulesService.getTopLevelRules(ruleset1.rulesetId, organization.organizationId); + const rules = await RulesService.getTopLevelRules(admin, ruleset1.rulesetId, organization.organizationId); expect(rules.length).toEqual(3); expect(rules.map((r) => r.ruleCode).sort()).toEqual(['A2', 'B2', 'T']); @@ -2466,7 +2616,7 @@ describe('Rule Tests', () => { } }); - const rules = await RulesService.getTopLevelRules(ruleset1.rulesetId, organization.organizationId); + const rules = await RulesService.getTopLevelRules(admin, ruleset1.rulesetId, organization.organizationId); expect(rules.length).toEqual(4); expect(rules.map((r) => r.ruleCode).sort()).toEqual(['A', 'A2', 'B2', 'T']); @@ -2486,14 +2636,14 @@ describe('Rule Tests', () => { } }); - const rules = await RulesService.getTopLevelRules(ruleset.rulesetId, organization.organizationId); + const rules = await RulesService.getTopLevelRules(admin, ruleset.rulesetId, organization.organizationId); expect(rules.length).toEqual(0); }); it('Does not return child rules', async () => { const car = await createUniqueCar(orgId); const { ruleset1, topLevelRule, leafRule1, leafRule2 } = await setupRules(car); - const rules = await RulesService.getTopLevelRules(ruleset1.rulesetId, organization.organizationId); + const rules = await RulesService.getTopLevelRules(admin, ruleset1.rulesetId, organization.organizationId); expect(rules.length).toEqual(3); expect(rules.find((r) => r.ruleId === topLevelRule.ruleId)).toBeDefined(); @@ -2513,17 +2663,26 @@ describe('Rule Tests', () => { } }); - const rules = await RulesService.getTopLevelRules(ruleset1.rulesetId, organization.organizationId); + const rules = await RulesService.getTopLevelRules(admin, ruleset1.rulesetId, organization.organizationId); expect(rules.find((r) => r.ruleId === topLevelRule.ruleId)).toBeUndefined(); }); }); describe('Get All Rules For Ruleset', () => { + it('Fails if user is a guest', async () => { + const car = await createUniqueCar(orgId); + const { ruleset1 } = await setupRules(car); + + await expect( + RulesService.getAllRulesForRuleset(guest, ruleset1.rulesetId, organization.organizationId) + ).rejects.toThrow(new AccessDeniedGuestException('view rules')); + }); + it('Successfully gets every rule in a ruleset, including children', async () => { const car = await createUniqueCar(orgId); const { ruleset1, topLevelRule, leafRule1, leafRule2, referencedRule, referencingRule } = await setupRules(car); - const rules = await RulesService.getAllRulesForRuleset(ruleset1.rulesetId, organization.organizationId); + const rules = await RulesService.getAllRulesForRuleset(admin, ruleset1.rulesetId, organization.organizationId); expect(rules.length).toEqual(5); const ruleIds = rules.map((r) => r.ruleId); @@ -2538,7 +2697,7 @@ describe('Rule Tests', () => { const car = await createUniqueCar(orgId); const { ruleset1, topLevelRule, leafRule1, leafRule2 } = await setupRules(car); - const rules = await RulesService.getAllRulesForRuleset(ruleset1.rulesetId, organization.organizationId); + const rules = await RulesService.getAllRulesForRuleset(admin, ruleset1.rulesetId, organization.organizationId); const topRule = rules.find((r) => r.ruleId === topLevelRule.ruleId); const leaf1 = rules.find((r) => r.ruleId === leafRule1.ruleId); @@ -2566,7 +2725,7 @@ describe('Rule Tests', () => { } }); - const rules = await RulesService.getAllRulesForRuleset(ruleset1.rulesetId, organization.organizationId); + const rules = await RulesService.getAllRulesForRuleset(admin, ruleset1.rulesetId, organization.organizationId); expect(rules.find((r) => r.ruleId === topLevelRule.ruleId)).toBeDefined(); expect(rules.find((r) => r.ruleId === otherRule.ruleId)).toBeUndefined(); @@ -2581,7 +2740,7 @@ describe('Rule Tests', () => { data: { dateDeleted: new Date(), deletedByUserId: admin.userId } }); - const rules = await RulesService.getAllRulesForRuleset(ruleset1.rulesetId, organization.organizationId); + const rules = await RulesService.getAllRulesForRuleset(admin, ruleset1.rulesetId, organization.organizationId); expect(rules.find((r) => r.ruleId === leafRule1.ruleId)).toBeUndefined(); }); @@ -2589,7 +2748,7 @@ describe('Rule Tests', () => { const car = await createUniqueCar(orgId); const { ruleset1 } = await setupRules(car); - const rules = await RulesService.getAllRulesForRuleset(ruleset1.rulesetId, organization.organizationId); + const rules = await RulesService.getAllRulesForRuleset(admin, ruleset1.rulesetId, organization.organizationId); for (let i = 0; i < rules.length - 1; i++) { expect(rules[i].ruleCode <= rules[i + 1].ruleCode).toBe(true); @@ -2609,14 +2768,14 @@ describe('Rule Tests', () => { } }); - const rules = await RulesService.getAllRulesForRuleset(ruleset.rulesetId, organization.organizationId); + const rules = await RulesService.getAllRulesForRuleset(admin, ruleset.rulesetId, organization.organizationId); expect(rules.length).toEqual(0); }); it('Fails when ruleset does not exist', async () => { - await expect(RulesService.getAllRulesForRuleset('fake-ruleset-id', organization.organizationId)).rejects.toThrow( - new NotFoundException('Ruleset', 'fake-ruleset-id') - ); + await expect( + RulesService.getAllRulesForRuleset(admin, 'fake-ruleset-id', organization.organizationId) + ).rejects.toThrow(new NotFoundException('Ruleset', 'fake-ruleset-id')); }); it('Fails when ruleset is deleted', async () => { @@ -2629,24 +2788,44 @@ describe('Rule Tests', () => { }); await RulesService.deleteRuleset(ruleset1.rulesetId, admin.userId, organization.organizationId); - await expect(RulesService.getAllRulesForRuleset(ruleset1.rulesetId, organization.organizationId)).rejects.toThrow( - new DeletedException('Ruleset', ruleset1.rulesetId) - ); + await expect( + RulesService.getAllRulesForRuleset(admin, ruleset1.rulesetId, organization.organizationId) + ).rejects.toThrow(new DeletedException('Ruleset', ruleset1.rulesetId)); }); it('Fails if ruleset is in the wrong org', async () => { const car = await createUniqueCar(orgId); - const { ruleset1 } = await setupRules(car); + const otherOrgRulesetType = await prisma.ruleset_Type.create({ + data: { + name: 'Other Org FHE', + createdByUserId: admin.userId, + organizationId: otherOrg.organizationId + } + }); + const otherRuleset = await prisma.ruleset.create({ + data: { + name: '2024', + fileId: 'other-fhe-2024-all-rules', + active: true, + rulesetTypeId: otherOrgRulesetType.rulesetTypeId, + carId: car.carId, + createdByUserId: admin.userId + } + }); - await expect(RulesService.getAllRulesForRuleset(ruleset1.rulesetId, otherOrg.organizationId)).rejects.toThrow( - InvalidOrganizationException - ); + await expect( + RulesService.getAllRulesForRuleset(admin, otherRuleset.rulesetId, organization.organizationId) + ).rejects.toThrow(InvalidOrganizationException); }); }); describe('Get Ruleset Type', () => { it('Successfully gets a ruleset type by ID', async () => { - const rulesetType = await RulesService.getRulesetType(fsaeRulesetType.rulesetTypeId, organization.organizationId); + const rulesetType = await RulesService.getRulesetType( + admin, + fsaeRulesetType.rulesetTypeId, + organization.organizationId + ); expect(rulesetType).toBeDefined(); expect(rulesetType.rulesetTypeId).toBe(fsaeRulesetType.rulesetTypeId); expect(rulesetType.name).toBe(fsaeRulesetType.name); @@ -2687,13 +2866,13 @@ describe('Rule Tests', () => { ); }); - it('Fails adding referenced rule if user is not admin', async () => { + it('Fails adding referenced rule if user is not leadership or above', async () => { const car = await createUniqueCar(orgId); const { topLevelRule, referencedRule } = await setupRules(car); await expect( async () => await RulesService.addRuleReferences(nonLeadership, topLevelRule.ruleId, referencedRule.ruleId, organization) - ).rejects.toThrow(new AccessDeniedAdminOnlyException('edit a rule')); + ).rejects.toThrow(new AccessDeniedException('Only leadership and above can edit a rule')); }); it('Fails adding referenced rule if rule does not exist', async () => { @@ -2772,13 +2951,13 @@ describe('Rule Tests', () => { ).rejects.toThrow(new HttpException(400, 'A rule cannot reference itself')); }); - it('Fails removing referenced rule if user is not admin', async () => { + it('Fails removing referenced rule if user is not leadership or above', async () => { const car = await createUniqueCar(orgId); const { referencedRule, referencingRule } = await setupRules(car); await expect( async () => await RulesService.removeRuleReferences(nonLeadership, referencingRule.ruleId, referencedRule.ruleId, organization) - ).rejects.toThrow(new AccessDeniedAdminOnlyException('edit a rule')); + ).rejects.toThrow(new AccessDeniedException('Only leadership and above can edit a rule')); }); it('Fails removing referenced rule if rule does not exist', async () => { diff --git a/src/frontend/src/layouts/Sidebar/Sidebar.tsx b/src/frontend/src/layouts/Sidebar/Sidebar.tsx index 735adb53ba..8105a23613 100644 --- a/src/frontend/src/layouts/Sidebar/Sidebar.tsx +++ b/src/frontend/src/layouts/Sidebar/Sidebar.tsx @@ -164,7 +164,7 @@ const Sidebar = ({ drawerOpen, setDrawerOpen, moveContent, setMoveContent }: Sid icon: , route: routes.SPONSORS }, - { + !onGuestHomePage && { name: 'Rules', icon: , route: routes.RULES diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/ProjectRules/ProjectRulesTab.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/ProjectRules/ProjectRulesTab.tsx index a899d94452..954ed686e9 100644 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/ProjectRules/ProjectRulesTab.tsx +++ b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/ProjectRules/ProjectRulesTab.tsx @@ -19,7 +19,7 @@ import { IconButton, Tooltip } from '@mui/material'; -import { Project, ProjectRule, Rule, RuleStatus } from 'shared'; +import { Project, ProjectRule, Rule, RuleStatus, isLeadership } from 'shared'; import LoadingIndicator from '../../../../components/LoadingIndicator'; import ErrorPage from '../../../ErrorPage'; import RuleRow from '../../../RulesPage/RuleRow'; @@ -37,6 +37,7 @@ import { useCreateProjectRule, useResetProjectRuleStatuses } from '../../../../hooks/rules.hooks'; +import { useCurrentUser } from '../../../../hooks/users.hooks'; import { useToast } from '../../../../hooks/toasts.hooks'; import { InfoOutlined } from '@mui/icons-material'; import { useHistory } from 'react-router-dom'; @@ -44,6 +45,7 @@ import { routes } from '../../../../utils/routes'; import RuleStatusTag from '../../../RulesPage/components/RuleStatusTag'; import { NERButton } from '../../../../components/NERButton'; import { compareRuleCodes } from '../../../../utils/rules.utils'; +import { isUserOnTeam } from '../../../../utils/teams.utils'; interface ProjectRulesTabProps { project: Project; @@ -53,6 +55,7 @@ export const ProjectRulesTab = ({ project }: ProjectRulesTabProps) => { const toast = useToast(); const theme = useTheme(); const history = useHistory(); + const user = useCurrentUser(); // State for modals and popovers const [selectedRulesetTypeIndex, setSelectedRulesetTypeIndex] = useState(0); @@ -98,6 +101,9 @@ export const ProjectRulesTab = ({ project }: ProjectRulesTabProps) => { const teamId = project.teams[0]?.teamId || ''; const teamNames = project.teams.map((team) => team.teamName); + // leadership can update status anywhere; members can only update it for projects whose team they're on + const canUpdateStatus = isLeadership(user.role) || project.teams.some((team) => isUserOnTeam(team, user)); + // Convert project rules to rules for display, merging in each rule's local status for this project // Sorted by rule code so both top-level rows and their children render in stable numeric order const projectRuleList = useMemo(() => { @@ -227,7 +233,7 @@ export const ProjectRulesTab = ({ project }: ProjectRulesTabProps) => { rule={rule} isLeaf={isLeafRule} popoverOpen={isPopoverOpenForRule} - onClick={isLeafRule ? (e) => handleStatusClick(e, rule) : undefined} + onClick={isLeafRule && canUpdateStatus ? (e) => handleStatusClick(e, rule) : undefined} onInfoClick={handleInfoClick} /> ); @@ -268,9 +274,11 @@ export const ProjectRulesTab = ({ project }: ProjectRulesTabProps) => { {areAllExpanded ? 'Collapse All' : 'Expand All'} - setShowResetModal(true)}> - Reset Status - + {isLeadership(user.role) && ( + setShowResetModal(true)}> + Reset Status + + )} )} @@ -355,58 +363,60 @@ export const ProjectRulesTab = ({ project }: ProjectRulesTabProps) => { }} > - - - {/* Assign Rules Tooltip */} - 0 - ? `Assign rules to the ${teamNames.join(', ')} team${ - teamNames.length === 1 ? '' : 's' - } to add them to this project` - : 'Add a team to this project to assign rules' - } - arrow - slotProps={{ tooltip: { sx: { textAlign: 'center' } } }} - > - e.stopPropagation()} - sx={{ - padding: '5px', - color: 'text.secondary' - }} + {isLeadership(user.role) && ( + + + {/* Assign Rules Tooltip */} + 0 + ? `Assign rules to the ${teamNames.join(', ')} team${ + teamNames.length === 1 ? '' : 's' + } to add them to this project` + : 'Add a team to this project to assign rules' + } + arrow + slotProps={{ tooltip: { sx: { textAlign: 'center' } } }} > - - - - {/* Assign Rules Button */} + e.stopPropagation()} + sx={{ + padding: '5px', + color: 'text.secondary' + }} + > + + + + {/* Assign Rules Button */} + + activeRuleset && + history.push( + `${routes.RULESET_EDIT.replace(':rulesetId', activeRuleset.rulesetId)}/assign-rules${ + teamId ? `?teamId=${teamId}` : '' + }` + ) + } + > + Assign Rules + + + {/* Add Rule Button */} - activeRuleset && - history.push( - `${routes.RULESET_EDIT.replace(':rulesetId', activeRuleset.rulesetId)}/assign-rules${ - teamId ? `?teamId=${teamId}` : '' - }` - ) - } + variant="contained" + sx={{ color: '#ededed' }} + onClick={() => setAddRuleModalOpen(true)} + disabled={teamNames.length === 0 || hasNoActiveRuleset} > - Assign Rules + Add Rule - {/* Add Rule Button */} - setAddRuleModalOpen(true)} - disabled={teamNames.length === 0 || hasNoActiveRuleset} - > - Add Rule - - + )} {/* Update Status Popover */} diff --git a/src/frontend/src/pages/RulesPage/Rules.tsx b/src/frontend/src/pages/RulesPage/Rules.tsx index 3ac570f131..6d44288c59 100644 --- a/src/frontend/src/pages/RulesPage/Rules.tsx +++ b/src/frontend/src/pages/RulesPage/Rules.tsx @@ -1,12 +1,26 @@ // switch route page for rules -import { Route, Switch } from 'react-router-dom'; +import { Redirect, Route, Switch } from 'react-router-dom'; +import { isGuest } from 'shared'; import { routes } from '../../utils/routes'; +import { useCurrentUser } from '../../hooks/users.hooks'; import RulesetTypePage from './RulesetTypePage'; import RulesetPage from './RulesetPage'; import RulesetEditPage from './RulesetEditPage'; import RulesetViewPage from './RulesetViewPage'; const RulesPage: React.FC = () => { + const user = useCurrentUser(); + + if (isGuest(user.role)) { + return ( + + ); + } + return ( diff --git a/src/frontend/src/pages/RulesPage/RulesetEditPage.tsx b/src/frontend/src/pages/RulesPage/RulesetEditPage.tsx index f75a9a0f93..ec72cc6cc8 100644 --- a/src/frontend/src/pages/RulesPage/RulesetEditPage.tsx +++ b/src/frontend/src/pages/RulesPage/RulesetEditPage.tsx @@ -6,7 +6,7 @@ import { Box, Button, CircularProgress, Paper, Table, TableBody, TableContainer, TextField, useTheme } from '@mui/material'; import ChevronRightIcon from '@mui/icons-material/ChevronRight'; import { useMemo, useState } from 'react'; -import { useParams } from 'react-router-dom'; +import { Redirect, useParams } from 'react-router-dom'; import PageLayout from '../../components/PageLayout'; import FullPageTabs from '../../components/FullPageTabs'; import { routes } from '../../utils/routes'; @@ -37,8 +37,9 @@ import { useFetchFullRuleTree } from '../../hooks/rules.hooks'; import { countRulesToDelete, compareRuleCodes } from '../../utils/rules.utils'; -import { Rule } from 'shared'; +import { Rule, isLeadership } from 'shared'; import { useToast } from '../../hooks/toasts.hooks'; +import { useCurrentUser } from '../../hooks/users.hooks'; import { useRuleTreeNavigation } from './useRuleTreeNavigation'; /** @@ -47,6 +48,7 @@ import { useRuleTreeNavigation } from './useRuleTreeNavigation'; */ const RulesetEditPage: React.FC = () => { const { rulesetId } = useParams<{ rulesetId: string; tabValue?: string }>(); //why tab value?? + const user = useCurrentUser(); const [tabValue, setTabValue] = useState(0); const defaultTab = 'edit-rules'; @@ -137,6 +139,12 @@ const RulesetEditPage: React.FC = () => { return ; } + // creating, editing, deleting, and assigning rules all require leadership and above + // if the user is not leadership, redirect them to the view page for this ruleset + if (!isLeadership(user.role)) { + return ; + } + const handleAddRuleSection = () => { setShowAddRuleSectionModal(true); }; diff --git a/src/frontend/src/pages/RulesPage/RulesetPage.tsx b/src/frontend/src/pages/RulesPage/RulesetPage.tsx index 5f89869cf8..b8da11e5af 100644 --- a/src/frontend/src/pages/RulesPage/RulesetPage.tsx +++ b/src/frontend/src/pages/RulesPage/RulesetPage.tsx @@ -4,8 +4,10 @@ */ import { useParams } from 'react-router-dom'; import React from 'react'; +import { isLeadership } from 'shared'; import { useToast } from '../../hooks/toasts.hooks'; import { useCreateRuleset, useDeleteRuleset, useParseRuleset } from '../../hooks/rules.hooks'; +import { useCurrentUser } from '../../hooks/users.hooks'; import { NERButton } from '../../components/NERButton'; import AddNewFileModal from './components/AddNewFileModal'; import PageLayout from '../../components/PageLayout'; @@ -22,6 +24,7 @@ import ErrorPage from '../ErrorPage'; */ const RulesetPage: React.FC = () => { const { rulesetTypeId } = useParams<{ rulesetTypeId: string }>(); + const user = useCurrentUser(); const { mutateAsync: createRuleset } = useCreateRuleset(); const { mutateAsync: parseRuleset } = useParseRuleset(); @@ -121,13 +124,15 @@ const RulesetPage: React.FC = () => { }} > {/* Add New File Button */} - setAddFileModalShow(!AddFileModalShow)} - > - Add New File - + {isLeadership(user.role) && ( + setAddFileModalShow(!AddFileModalShow)} + > + Add New File + + )} setAddFileModalShow(false)} diff --git a/src/frontend/src/pages/RulesPage/RulesetViewPage.tsx b/src/frontend/src/pages/RulesPage/RulesetViewPage.tsx index a10dc159b7..6674f736ff 100644 --- a/src/frontend/src/pages/RulesPage/RulesetViewPage.tsx +++ b/src/frontend/src/pages/RulesPage/RulesetViewPage.tsx @@ -1,4 +1,5 @@ import React, { useState } from 'react'; +import { isHead } from 'shared'; import FullPageTabs from '../../components/FullPageTabs'; import PageLayout from '../../components/PageLayout'; import { NERButton } from '../../components/NERButton'; @@ -18,10 +19,12 @@ import { useFetchFullRuleTree, useResetRulesetStatuses } from '../../hooks/rules.hooks'; +import { useCurrentUser } from '../../hooks/users.hooks'; import { useRuleTreeNavigation } from './useRuleTreeNavigation'; import { useTeamRuleOrganization } from './useTeamRuleOrganization'; const RulesetViewPage = () => { + const user = useCurrentUser(); const [tabIndex, setTabIndex] = useState(0); const [showResetModal, setShowResetModal] = useState(false); // bumped after a reset to force RulesetGeneralView to remount, clearing any open popover/history modal @@ -126,9 +129,11 @@ const RulesetViewPage = () => { {areAllExpanded ? 'Collapse All' : 'Expand All'} - setShowResetModal(true)}> - Reset Status - + {isHead(user.role) && ( + setShowResetModal(true)}> + Reset Status + + )} )} {tabIndex === 1 && ( diff --git a/src/frontend/src/pages/RulesPage/components/RulesetGeneralView.tsx b/src/frontend/src/pages/RulesPage/components/RulesetGeneralView.tsx index 0552aa8aff..182b24de72 100644 --- a/src/frontend/src/pages/RulesPage/components/RulesetGeneralView.tsx +++ b/src/frontend/src/pages/RulesPage/components/RulesetGeneralView.tsx @@ -1,12 +1,13 @@ import React, { useMemo, useState } from 'react'; import { Box, Paper, Table, TableBody, TableContainer, useTheme } from '@mui/material'; -import { Rule, RuleStatus } from 'shared'; +import { Rule, RuleStatus, isLeadership } from 'shared'; import RuleRow from '../RuleRow'; import RuleStatusTag from './RuleStatusTag'; import RuleContent from './RuleContent'; import RuleStatusHistoryModal from './RuleStatusHistoryModal'; import UpdateStatusPopover from '../../ProjectDetailPage/ProjectViewContainer/ProjectRules/UpdateStatusPopover'; import { useSetRuleStatus } from '../../../hooks/rules.hooks'; +import { useCurrentUser } from '../../../hooks/users.hooks'; import { useToast } from '../../../hooks/toasts.hooks'; import { compareRuleCodes } from '../../../utils/rules.utils'; @@ -31,6 +32,7 @@ const RulesetGeneralView: React.FC = ({ }) => { const theme = useTheme(); const toast = useToast(); + const user = useCurrentUser(); const [statusPopoverAnchor, setStatusPopoverAnchor] = useState(null); const [selectedRule, setSelectedRule] = useState(null); const [historyModalRule, setHistoryModalRule] = useState(null); @@ -79,10 +81,14 @@ const RulesetGeneralView: React.FC = ({ rule={r} isLeaf={r.subRuleIds.length === 0} popoverOpen={selectedRule?.ruleId === r.ruleId && Boolean(statusPopoverAnchor)} - onClick={(e) => { - setSelectedRule(r); - setStatusPopoverAnchor(e.currentTarget); - }} + onClick={ + isLeadership(user.role) + ? (e) => { + setSelectedRule(r); + setStatusPopoverAnchor(e.currentTarget); + } + : undefined + } onInfoClick={setHistoryModalRule} /> )} diff --git a/src/frontend/src/pages/RulesPage/components/RulesetTable.tsx b/src/frontend/src/pages/RulesPage/components/RulesetTable.tsx index d494d3d2d6..1ca51d644f 100644 --- a/src/frontend/src/pages/RulesPage/components/RulesetTable.tsx +++ b/src/frontend/src/pages/RulesPage/components/RulesetTable.tsx @@ -23,7 +23,8 @@ import { useHistory, useParams } from 'react-router-dom'; import LoadingIndicator from '../../../components/LoadingIndicator'; import ErrorPage from '../../ErrorPage'; import { useDeleteRuleset, useRulesetsByType, useUpdateRuleset } from '../../../hooks/rules.hooks'; -import { Ruleset } from 'shared'; +import { useCurrentUser } from '../../../hooks/users.hooks'; +import { Ruleset, isLeadership } from 'shared'; import { routes } from '../../../utils/routes'; import { useToast } from '../../../hooks/toasts.hooks'; import { Delete } from '@mui/icons-material'; @@ -43,6 +44,7 @@ const RulesetTable: React.FC = () => { const { rulesetTypeId } = useParams(); const toast = useToast(); const history = useHistory(); + const user = useCurrentUser(); const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('md')); @@ -54,6 +56,8 @@ const RulesetTable: React.FC = () => { return ruleset.ruleAmount > 0; }; + const canDelete = isLeadership(user.role); + // Table header configuration const headCells = [ { id: 'fileName', label: 'File Name' }, @@ -62,7 +66,7 @@ const RulesetTable: React.FC = () => { { id: 'car', label: 'Car' }, { id: 'isActive', label: 'Active?' }, { id: 'actions', label: 'Actions' }, - { id: 'delete', label: '' } + ...(canDelete ? [{ id: 'delete', label: '' }] : []) ]; const handleToggleActive = (ruleset: Ruleset) => { @@ -192,27 +196,29 @@ const RulesetTable: React.FC = () => { /> - handleEditRuleset(ruleset.rulesetId)} - disabled={!hasRules(ruleset)} - sx={{ - backgroundColor: theme.palette.grey[800], - color: theme.palette.getContrastText(theme.palette.grey[600]), - '&:hover': { - backgroundColor: theme.palette.grey[700] - }, - marginRight: '10px', - padding: '4px', - lineHeight: 1, - borderRadius: '6px', - '&.Mui-disabled': { - backgroundColor: theme.palette.grey[900], - color: theme.palette.grey[600] - } - }} - > - Edit/Assign Rules - + {isLeadership(user.role) && ( + handleEditRuleset(ruleset.rulesetId)} + disabled={!hasRules(ruleset)} + sx={{ + backgroundColor: theme.palette.grey[800], + color: theme.palette.getContrastText(theme.palette.grey[600]), + '&:hover': { + backgroundColor: theme.palette.grey[700] + }, + marginRight: '10px', + padding: '4px', + lineHeight: 1, + borderRadius: '6px', + '&.Mui-disabled': { + backgroundColor: theme.palette.grey[900], + color: theme.palette.grey[600] + } + }} + > + Edit/Assign Rules + + )} handleViewRuleset(ruleset.rulesetId)} disabled={!hasRules(ruleset)} @@ -233,7 +239,13 @@ const RulesetTable: React.FC = () => { > View Rules - + {canDelete && ( + + )} @@ -261,7 +273,7 @@ const RulesetTable: React.FC = () => { {/* Table rows with ruleset data */} {rulesets.length === 0 ? ( - + No Rulesets Found @@ -291,27 +303,29 @@ const RulesetTable: React.FC = () => { /> - handleEditRuleset(ruleset.rulesetId)} - disabled={!hasRules(ruleset)} - sx={{ - backgroundColor: theme.palette.grey[800], - color: theme.palette.getContrastText(theme.palette.grey[600]), - '&:hover': { - backgroundColor: theme.palette.grey[700] - }, - marginRight: '10px', - padding: '4px', - lineHeight: 1, - borderRadius: '6px', - '&.Mui-disabled': { - backgroundColor: theme.palette.grey[900], - color: theme.palette.grey[600] - } - }} - > - Edit/Assign Rules - + {isLeadership(user.role) && ( + handleEditRuleset(ruleset.rulesetId)} + disabled={!hasRules(ruleset)} + sx={{ + backgroundColor: theme.palette.grey[800], + color: theme.palette.getContrastText(theme.palette.grey[600]), + '&:hover': { + backgroundColor: theme.palette.grey[700] + }, + marginRight: '10px', + padding: '4px', + lineHeight: 1, + borderRadius: '6px', + '&.Mui-disabled': { + backgroundColor: theme.palette.grey[900], + color: theme.palette.grey[600] + } + }} + > + Edit/Assign Rules + + )} handleViewRuleset(ruleset.rulesetId)} disabled={!hasRules(ruleset)} @@ -333,13 +347,15 @@ const RulesetTable: React.FC = () => { View Rules - - - + {canDelete && ( + + + + )} )) )}