From 9c16579f547af659ce7ce4b6718aef05aa31c10d Mon Sep 17 00:00:00 2001 From: Nathnael Date: Tue, 28 Jul 2026 12:36:01 +0000 Subject: [PATCH] fix(backoffice): scope unit-admin role matching to the selected org (EDRFREIGHT-223) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getAdminRoleInfo picked a user's unit_admin role by role key alone — a unit-admin grant carries only a unitId, no organizationId, so a user who is unit-admin in one org and (say) org-admin in another had the wrong unit attached to their row wherever both orgs share the viewer's admin list. That wrong unitId then flowed into the remove action, which deletes by exact {userId, roleId, unitId} match — a legitimate 0-match, surfaced as "user_role_not_found", removal silently failing. Fetches the selected org's unit ids (same pattern as the position-type form's org→units scoping) and requires the unit-admin grant's unitId to be one of them before treating it as this org's grant. Co-Authored-By: Claude Sonnet 5 --- .../org-admins/OrgAdminsColumnDefn.tsx | 19 +++++++++++------ .../components/org-admins/OrgAdminsPage.tsx | 21 ++++++++++++++++++- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/apps/edr-freight-web/backoffice/src/super-admin/components/org-admins/OrgAdminsColumnDefn.tsx b/apps/edr-freight-web/backoffice/src/super-admin/components/org-admins/OrgAdminsColumnDefn.tsx index 97c3bfbc6..8bbe7c587 100644 --- a/apps/edr-freight-web/backoffice/src/super-admin/components/org-admins/OrgAdminsColumnDefn.tsx +++ b/apps/edr-freight-web/backoffice/src/super-admin/components/org-admins/OrgAdminsColumnDefn.tsx @@ -32,13 +32,15 @@ export interface AdminRoleInfo { /** * all-admins/:id returns users who are org admins of the org OR unit admins of * one of its units; userRoles carries every role of the user, so match the org - * explicitly for the org-admin grant. + * explicitly for the org-admin grant. The unit-admin grant carries no + * organizationId of its own, only a unitId, so orgUnitIds (every unit that + * belongs to the selected org) is required to tell a same-org unit-admin + * grant apart from a same-user unit-admin grant in a different org. */ -// ponytail: unit relation isn't loaded, so a unit_admin grant from another org -// can't be told apart — acceptable, the server only returns admins of this org. export function getAdminRoleInfo( admin: OrgAdminUser, selectedOrgId: string, + orgUnitIds: Set, ): AdminRoleInfo { const roles = admin.userRoles ?? []; const isOrgAdmin = roles.some( @@ -47,7 +49,10 @@ export function getAdminRoleInfo( r.organizationId === selectedOrgId, ); const unitRole = roles.find( - (r) => r.role?.key === UNIT_ADMIN_ROLE_KEY && r.unitId, + (r) => + r.role?.key === UNIT_ADMIN_ROLE_KEY && + !!r.unitId && + orgUnitIds.has(r.unitId), ); return { isOrgAdmin, @@ -58,6 +63,7 @@ export function getAdminRoleInfo( interface ColumnCallbacks { selectedOrgId: string; + orgUnitIds: Set; localizedName: (name?: { am?: string; en?: string }) => string; onEdit: (admin: OrgAdminUser) => void; onResend: (admin: OrgAdminUser) => void; @@ -67,6 +73,7 @@ interface ColumnCallbacks { export function getOrgAdminsColumnDefn({ selectedOrgId, + orgUnitIds, localizedName, onEdit, onResend, @@ -118,7 +125,7 @@ export function getOrgAdminsColumnDefn({ id: "role", header: () => t("orgAdmins.columns.role"), cell: ({ row }) => { - const info = getAdminRoleInfo(row.original, selectedOrgId); + const info = getAdminRoleInfo(row.original, selectedOrgId, orgUnitIds); return (
{info.isOrgAdmin && ( @@ -179,7 +186,7 @@ export function getOrgAdminsColumnDefn({ enableHiding: false, cell: ({ row }) => { const admin = row.original; - const roleInfo = getAdminRoleInfo(admin, selectedOrgId); + const roleInfo = getAdminRoleInfo(admin, selectedOrgId, orgUnitIds); return ( diff --git a/apps/edr-freight-web/backoffice/src/super-admin/components/org-admins/OrgAdminsPage.tsx b/apps/edr-freight-web/backoffice/src/super-admin/components/org-admins/OrgAdminsPage.tsx index 53ac19841..adebd98a5 100644 --- a/apps/edr-freight-web/backoffice/src/super-admin/components/org-admins/OrgAdminsPage.tsx +++ b/apps/edr-freight-web/backoffice/src/super-admin/components/org-admins/OrgAdminsPage.tsx @@ -31,6 +31,8 @@ import { Badge } from "@/shared/common/ui/badge"; import { AdvancedTable } from "@/shared/common/ui/table/AdvancedTable"; import { useLocalizedName } from "@/shared/common/localizedName"; import { OrganizationDto } from "@/shared/dto/organization/organizationDto"; +import { useUnit } from "@/user-management/hooks/useUnit"; +import { UnitDto } from "@/user-management/dto/unit/unitDto"; import { OrgAdminUser, useOrgAdmins, @@ -89,6 +91,22 @@ export default function OrgAdminsPage() { skip: pageIndex * pageSize, }); + const { getList: getUnitList } = useUnit(); + // A unit-admin grant only carries a unitId, no organizationId — this is the + // set that tells "unit_admin of this org" apart from "unit_admin of some + // other org the same user also administers" (see getAdminRoleInfo). + const { data: orgUnitsResponse } = getUnitList(selectedOrg?.id ?? "", { + take: 3000, + skip: 0, + }); + const orgUnitIds = useMemo( + () => + new Set( + (orgUnitsResponse?.data?.items ?? []).map((unit: UnitDto) => unit.id), + ), + [orgUnitsResponse], + ); + useEffect(() => { setPageIndex(0); }, [selectedOrg?.id, pageSize]); @@ -178,6 +196,7 @@ export default function OrgAdminsPage() { () => getOrgAdminsColumnDefn({ selectedOrgId: selectedOrg?.id ?? "", + orgUnitIds, localizedName: localizedName as (name?: { am?: string; en?: string; @@ -191,7 +210,7 @@ export default function OrgAdminsPage() { onRemove: (admin, roleInfo) => setRemoveTarget({ admin, roleInfo }), }), // eslint-disable-next-line react-hooks/exhaustive-deps - [selectedOrg?.id], + [selectedOrg?.id, orgUnitIds], ); return (