mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
fix(backoffice): scope unit-admin role matching to the selected org (EDRFREIGHT-223)
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<string>,
|
||||
): 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<string>;
|
||||
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 (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{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 (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user