From bc58f6e08eb75a9203d92d7b0a38d1da9aaf7b82 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 4 Feb 2026 17:48:58 +0000 Subject: [PATCH 1/5] add user role hierearchy lookup and management function --- server/src/types/user.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/server/src/types/user.ts b/server/src/types/user.ts index 41c009791..2ee07d487 100644 --- a/server/src/types/user.ts +++ b/server/src/types/user.ts @@ -1,6 +1,17 @@ export const UserRoles = ["user", "admin", "superadmin", "demo"] as const; export type UserRole = (typeof UserRoles)[number]; +export const RoleHierarchy: Record = { + demo: 0, + user: 1, + admin: 2, + superadmin: 3, +}; + +export const canManageRole = (actorRole: UserRole, targetRole: UserRole): boolean => { + return RoleHierarchy[actorRole] > RoleHierarchy[targetRole]; +}; + export interface UserProfileImage { data?: Buffer; contentType?: string; From 4ecc737438eef43c9d808ac20548f1765d201c16 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 4 Feb 2026 17:49:19 +0000 Subject: [PATCH 2/5] add helper method to extract user roles --- server/src/controllers/controllerUtils.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/server/src/controllers/controllerUtils.ts b/server/src/controllers/controllerUtils.ts index 592c58fc3..b1ccc4c84 100755 --- a/server/src/controllers/controllerUtils.ts +++ b/server/src/controllers/controllerUtils.ts @@ -112,6 +112,13 @@ const requireUserEmail = (userEmail?: string): string => { return userEmail; }; +export const requireUserRoles = (userRoles?: string[]): string[] => { + if (!userRoles || userRoles.length === 0) { + throw new AppError({ message: "User roles are required", status: 400 }); + } + return userRoles; +} + export { fetchMonitorCertificate, requireString, From b4c929963f75b4946a26f6602786591d41977d63 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 4 Feb 2026 17:49:45 +0000 Subject: [PATCH 3/5] add permission check to invite service --- server/src/controllers/inviteController.ts | 7 +++++-- server/src/service/business/inviteService.ts | 20 ++++++++++++++++++-- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/server/src/controllers/inviteController.ts b/server/src/controllers/inviteController.ts index 664d84d98..990b81651 100755 --- a/server/src/controllers/inviteController.ts +++ b/server/src/controllers/inviteController.ts @@ -1,5 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { inviteBodyValidation, inviteVerificationBodyValidation } from "@/validation/joi.js"; +import { requireTeamId, requireUserRoles } from "@/controllers/controllerUtils.js"; const SERVICE_NAME = "inviteController"; class InviteController { @@ -14,12 +15,14 @@ class InviteController { } getInviteToken = async (req: Request, res: Response, next: NextFunction) => { + console.log(req.body); try { + const teamId = requireTeamId(req.user?.teamId); + const userRoles = requireUserRoles(req.user?.role); const invite = req.body; - const teamId = req?.user?.teamId; invite.teamId = teamId; await inviteBodyValidation.validateAsync(invite); - const inviteToken = await this.inviteService.getInviteToken({ invite, teamId }); + const inviteToken = await this.inviteService.getInviteToken({ invite, teamId, userRoles }); return res.status(200).json({ success: true, msg: "Invite token generated successfully", diff --git a/server/src/service/business/inviteService.ts b/server/src/service/business/inviteService.ts index eee0f993c..ba6039b34 100644 --- a/server/src/service/business/inviteService.ts +++ b/server/src/service/business/inviteService.ts @@ -1,4 +1,5 @@ -import type { Invite } from "@/types/index.js"; +import type { Invite, UserRole } from "@/types/index.js"; +import { canManageRole } from "@/types/user.js"; import type { IInvitesRepository } from "@/repositories/index.js"; import { AppError } from "@/utils/AppError.js"; @@ -29,8 +30,23 @@ class InviteService { return InviteService.SERVICE_NAME; } - getInviteToken = async ({ invite, teamId }: { invite: Partial; teamId: string }) => { + getInviteToken = async ({ invite, teamId, userRoles }: { invite: Partial; teamId: string; userRoles: UserRole[] }) => { invite.teamId = teamId; + + const inviteRoles = invite.role ?? []; + + for (const targetRole of inviteRoles) { + const canManage = userRoles.some((actorRole) => canManageRole(actorRole, targetRole)); + if (!canManage) { + throw new AppError({ + message: "You do not have permission to create this invite", + service: SERVICE_NAME, + method: "getInviteToken", + status: 403, + }); + } + } + const inviteToken = await this.invitesRepository.create(invite); return inviteToken; }; From 97dd3ec9cb6cfde697fa3599b7ab78613c2f0ca2 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 4 Feb 2026 17:57:00 +0000 Subject: [PATCH 4/5] add permissin check to sendInviteEmail --- server/src/controllers/inviteController.ts | 6 +++++- server/src/service/business/inviteService.ts | 16 +++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/server/src/controllers/inviteController.ts b/server/src/controllers/inviteController.ts index 990b81651..e6d5d7ccb 100755 --- a/server/src/controllers/inviteController.ts +++ b/server/src/controllers/inviteController.ts @@ -35,13 +35,17 @@ class InviteController { sendInviteEmail = async (req: Request, res: Response, next: NextFunction) => { try { + const teamId = requireTeamId(req?.user?.teamId); + const userRoles = requireUserRoles(req?.user?.role); + const inviteRequest = req.body; - inviteRequest.teamId = req?.user?.teamId; + inviteRequest.teamId = teamId; await inviteBodyValidation.validateAsync(inviteRequest); const inviteToken = await this.inviteService.sendInviteEmail({ invite: inviteRequest, firstName: req?.user?.firstName, + userRoles, }); return res.status(200).json({ success: true, diff --git a/server/src/service/business/inviteService.ts b/server/src/service/business/inviteService.ts index ba6039b34..e4069d485 100644 --- a/server/src/service/business/inviteService.ts +++ b/server/src/service/business/inviteService.ts @@ -51,7 +51,21 @@ class InviteService { return inviteToken; }; - sendInviteEmail = async ({ invite, firstName }: { invite: Partial; firstName: any }) => { + sendInviteEmail = async ({ invite, firstName, userRoles }: { invite: Partial; firstName: any; userRoles: UserRole[] }) => { + const inviteRoles = invite.role ?? []; + + for (const targetRole of inviteRoles) { + const canManage = userRoles.some((actorRole) => canManageRole(actorRole, targetRole)); + if (!canManage) { + throw new AppError({ + message: "You do not have permission to create this invite", + service: SERVICE_NAME, + method: "getInviteToken", + status: 403, + }); + } + } + const inviteToken = await this.invitesRepository.create(invite); const { clientHost } = this.settingsService.getSettings(); From 189f309631bd44d9fd6c1f3d8507d9d8df78f345 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 4 Feb 2026 18:10:52 +0000 Subject: [PATCH 5/5] format --- server/src/controllers/controllerUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/controllers/controllerUtils.ts b/server/src/controllers/controllerUtils.ts index b1ccc4c84..99c60807e 100755 --- a/server/src/controllers/controllerUtils.ts +++ b/server/src/controllers/controllerUtils.ts @@ -117,7 +117,7 @@ export const requireUserRoles = (userRoles?: string[]): string[] => { throw new AppError({ message: "User roles are required", status: 400 }); } return userRoles; -} +}; export { fetchMonitorCertificate,