fix: allow protected hub self access management
This commit is contained in:
@@ -163,6 +163,20 @@ const publicPoolSections: Array<{ id: AdminSection; label: string; icon: React.R
|
||||
{ id: "misc", label: "Разное", icon: <SlidersHorizontal size={16} /> },
|
||||
];
|
||||
|
||||
const protectedLauncherUserIds = new Set(["user_root"]);
|
||||
|
||||
function isProtectedLauncherUser(userId: string): boolean {
|
||||
return protectedLauncherUserIds.has(userId);
|
||||
}
|
||||
|
||||
function canSelfManageProtectedUser(me: MeResponse, userId: string): boolean {
|
||||
return isProtectedLauncherUser(userId) && me.user.id === userId;
|
||||
}
|
||||
|
||||
function isProtectedFromCurrentActor(me: MeResponse, userId: string): boolean {
|
||||
return isProtectedLauncherUser(userId) && !canSelfManageProtectedUser(me, userId);
|
||||
}
|
||||
|
||||
export function AdminOverlay({
|
||||
data,
|
||||
me,
|
||||
@@ -499,6 +513,7 @@ export function AdminOverlay({
|
||||
<GroupsSection
|
||||
data={data}
|
||||
clientId={scopedClientId}
|
||||
me={me}
|
||||
onCreateGroup={onCreateGroup}
|
||||
onUpdateGroup={onUpdateGroup}
|
||||
onDeleteGroup={onDeleteGroup}
|
||||
@@ -517,6 +532,7 @@ export function AdminOverlay({
|
||||
{activeSection === "access" ? (
|
||||
<AccessSection
|
||||
data={data}
|
||||
me={me}
|
||||
matrix={accessMatrix}
|
||||
selectedCell={selectedAccessCell}
|
||||
onSelectCell={(cell) => setSelectedCell({ userId: cell.userId, serviceId: cell.serviceId })}
|
||||
@@ -1174,12 +1190,14 @@ function PlatformUsersSection({
|
||||
function GroupsSection({
|
||||
data,
|
||||
clientId,
|
||||
me,
|
||||
onCreateGroup,
|
||||
onUpdateGroup,
|
||||
onDeleteGroup,
|
||||
}: {
|
||||
data: LauncherData;
|
||||
clientId: string;
|
||||
me: MeResponse;
|
||||
onCreateGroup: (clientId: string) => void;
|
||||
onUpdateGroup: (groupId: string, patch: Partial<ClientGroup>) => void;
|
||||
onDeleteGroup: (groupId: string) => void;
|
||||
@@ -1247,6 +1265,7 @@ function GroupsSection({
|
||||
{editingGroup ? (
|
||||
<GroupEditorModal
|
||||
group={editingGroup}
|
||||
me={me}
|
||||
users={data.memberships
|
||||
.filter((membership) => membership.clientId === clientId)
|
||||
.map((membership) => getUser(data, membership.userId))}
|
||||
@@ -2674,12 +2693,14 @@ function UserEditorModal({
|
||||
|
||||
function GroupEditorModal({
|
||||
group,
|
||||
me,
|
||||
users,
|
||||
onClose,
|
||||
onSave,
|
||||
onDelete,
|
||||
}: {
|
||||
group: ClientGroup;
|
||||
me: MeResponse;
|
||||
users: LauncherUser[];
|
||||
onClose: () => void;
|
||||
onSave: (patch: Partial<ClientGroup>) => void;
|
||||
@@ -2718,17 +2739,23 @@ function GroupEditorModal({
|
||||
<div className="service-content-field service-content-field--wide">
|
||||
<span>Участники</span>
|
||||
<div className="admin-token-grid">
|
||||
{users.map((user) => (
|
||||
<button
|
||||
key={user.id}
|
||||
className="admin-token"
|
||||
data-active={draft.memberIds.includes(user.id)}
|
||||
type="button"
|
||||
onClick={() => toggleUser(user.id)}
|
||||
>
|
||||
{user.name}
|
||||
</button>
|
||||
))}
|
||||
{users.map((user) => {
|
||||
const lockedProtectedUser = isProtectedFromCurrentActor(me, user.id);
|
||||
|
||||
return (
|
||||
<button
|
||||
key={user.id}
|
||||
className="admin-token"
|
||||
data-active={draft.memberIds.includes(user.id)}
|
||||
type="button"
|
||||
disabled={lockedProtectedUser}
|
||||
title={lockedProtectedUser ? "Защищённого пользователя может менять в группах только он сам" : undefined}
|
||||
onClick={() => toggleUser(user.id)}
|
||||
>
|
||||
{user.name}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2926,6 +2953,7 @@ function mediaKindFromUrl(value: string): MediaKind | null {
|
||||
|
||||
function AccessSection({
|
||||
data,
|
||||
me,
|
||||
matrix,
|
||||
selectedCell,
|
||||
onSelectCell,
|
||||
@@ -2942,6 +2970,7 @@ function AccessSection({
|
||||
onSetServiceModuleEntitlement,
|
||||
}: {
|
||||
data: LauncherData;
|
||||
me: MeResponse;
|
||||
matrix: ReturnType<typeof buildAccessMatrix>;
|
||||
selectedCell: AccessMatrixCell | null;
|
||||
onSelectCell: (cell: AccessMatrixCell) => void;
|
||||
@@ -3011,7 +3040,9 @@ function AccessSection({
|
||||
const membership = data.memberships.find((item) => item.clientId === matrix.client.id && item.userId === user.id);
|
||||
if (!membership) return null;
|
||||
|
||||
const protectedUser = user.id === "user_root";
|
||||
const protectedUser = isProtectedLauncherUser(user.id);
|
||||
const protectedFromActor = isProtectedFromCurrentActor(me, user.id);
|
||||
const protectedSelfManage = canSelfManageProtectedUser(me, user.id);
|
||||
const inviterMeta = getMembershipInviterMeta(data, membership);
|
||||
const pendingKey = `${matrix.client.id}:${user.id}:${primaryTaskManagerWorkspace?.slug ?? "primary"}`;
|
||||
const pendingTaskerAssignment = Boolean(pendingTaskManagerMemberships[pendingKey]);
|
||||
@@ -3038,7 +3069,7 @@ function AccessSection({
|
||||
<div className="access-grid-cell" role="cell">
|
||||
<MainRoleControl
|
||||
value={membership.role}
|
||||
protectedUser={protectedUser}
|
||||
protectedUser={protectedFromActor}
|
||||
onChange={(role) => onUpdateMembership(membership.id, { role })}
|
||||
/>
|
||||
</div>
|
||||
@@ -3056,13 +3087,14 @@ function AccessSection({
|
||||
pendingValue={pendingAccessAssignments[accessCellKey(user.id, service.id)]}
|
||||
busy={!usePublicTaskerAccess && isTaskManagerService && pendingTaskerAssignment}
|
||||
publicSelfService={usePublicTaskerAccess}
|
||||
readOnly={protectedFromActor}
|
||||
onSelectCell={onSelectCell}
|
||||
onSetAccess={(value) => {
|
||||
const nextValue = value;
|
||||
onSetUserServiceAccess({ userId: user.id, serviceId: service.id, value: nextValue });
|
||||
|
||||
if (usePublicTaskerAccess) return;
|
||||
if (!isTaskManagerService || !primaryTaskManagerWorkspace || protectedUser || forcedTaskManagerAdmin) return;
|
||||
if (!isTaskManagerService || !primaryTaskManagerWorkspace || protectedFromActor || (forcedTaskManagerAdmin && !protectedSelfManage)) return;
|
||||
|
||||
onSetTaskManagerWorkspaceMemberRole({
|
||||
clientId: matrix.client.id,
|
||||
@@ -3086,6 +3118,7 @@ function AccessSection({
|
||||
{detailsCell && detailsService ? (
|
||||
<OperationalCoreAccessModal
|
||||
data={data}
|
||||
me={me}
|
||||
client={matrix.client}
|
||||
user={getUser(data, detailsCell.userId)}
|
||||
service={detailsService}
|
||||
@@ -3110,6 +3143,7 @@ function AccessSection({
|
||||
|
||||
function PublicAccessUsersPanel({
|
||||
data,
|
||||
me,
|
||||
matrix,
|
||||
selectedCell,
|
||||
onSelectCell,
|
||||
@@ -3121,6 +3155,7 @@ function PublicAccessUsersPanel({
|
||||
onSetServiceModuleEntitlement,
|
||||
}: {
|
||||
data: LauncherData;
|
||||
me: MeResponse;
|
||||
matrix: ReturnType<typeof buildAccessMatrix>;
|
||||
selectedCell: AccessMatrixCell | null;
|
||||
onSelectCell: (cell: AccessMatrixCell) => void;
|
||||
@@ -3161,7 +3196,8 @@ function PublicAccessUsersPanel({
|
||||
const membership = data.memberships.find((item) => item.clientId === matrix.client.id && item.userId === user.id);
|
||||
if (!membership) return null;
|
||||
|
||||
const protectedUser = user.id === "user_root";
|
||||
const protectedUser = isProtectedLauncherUser(user.id);
|
||||
const protectedFromActor = isProtectedFromCurrentActor(me, user.id);
|
||||
const inviterMeta = getMembershipInviterMeta(data, membership);
|
||||
const operationalCoreCell = operationalCoreService
|
||||
? matrix.cells.find((cell) => cell.userId === user.id && cell.serviceId === operationalCoreService.id) ?? null
|
||||
@@ -3196,7 +3232,7 @@ function PublicAccessUsersPanel({
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
{protectedUser ? (
|
||||
{protectedFromActor ? (
|
||||
<AdminStaticPill>{membershipRoleLabel(membership.role)}</AdminStaticPill>
|
||||
) : (
|
||||
<NodeDcSelect
|
||||
@@ -3253,6 +3289,7 @@ function PublicAccessUsersPanel({
|
||||
{detailsCell && detailsService ? (
|
||||
<OperationalCoreAccessModal
|
||||
data={data}
|
||||
me={me}
|
||||
client={matrix.client}
|
||||
user={getUser(data, detailsCell.userId)}
|
||||
service={detailsService}
|
||||
@@ -3360,6 +3397,7 @@ function MainRoleControl({
|
||||
|
||||
function OperationalCoreAccessModal({
|
||||
data,
|
||||
me,
|
||||
client,
|
||||
user,
|
||||
service,
|
||||
@@ -3378,6 +3416,7 @@ function OperationalCoreAccessModal({
|
||||
onSetServiceModuleEntitlement,
|
||||
}: {
|
||||
data: LauncherData;
|
||||
me: MeResponse;
|
||||
client: Client;
|
||||
user: LauncherUser;
|
||||
service: Service;
|
||||
@@ -3396,7 +3435,8 @@ function OperationalCoreAccessModal({
|
||||
onSetServiceModuleEntitlement: (command: SetServiceModuleEntitlementCommand) => void;
|
||||
}) {
|
||||
const membership = data.memberships.find((item) => item.clientId === client.id && item.userId === user.id);
|
||||
const protectedUser = user.id === "user_root" || membership?.role === "client_owner";
|
||||
const protectedSelfManage = canSelfManageProtectedUser(me, user.id);
|
||||
const protectedUser = isProtectedFromCurrentActor(me, user.id) || (membership?.role === "client_owner" && !protectedSelfManage);
|
||||
const basePendingValue = pendingAccessAssignments[accessCellKey(user.id, service.id)];
|
||||
const baseAssignmentValue = basePendingValue ?? accessAssignmentValue(cell);
|
||||
const baseSelectOptions = publicSelfService ? publicOperationalCoreAccessOptions : accessAssignmentOptions;
|
||||
@@ -3628,6 +3668,7 @@ function AccessCellControl({
|
||||
pendingValue,
|
||||
busy = false,
|
||||
publicSelfService = false,
|
||||
readOnly = false,
|
||||
onSelectCell,
|
||||
onSetAccess,
|
||||
onOpenDetails,
|
||||
@@ -3637,6 +3678,7 @@ function AccessCellControl({
|
||||
pendingValue?: AccessAssignmentValue;
|
||||
busy?: boolean;
|
||||
publicSelfService?: boolean;
|
||||
readOnly?: boolean;
|
||||
onSelectCell: (cell: AccessMatrixCell) => void;
|
||||
onSetAccess: (value: AccessAssignmentValue) => void;
|
||||
onOpenDetails?: () => void;
|
||||
@@ -3663,6 +3705,7 @@ function AccessCellControl({
|
||||
cell.effectiveAccess.allowed && "access-cell--allowed",
|
||||
!cell.effectiveAccess.allowed && "access-cell--denied",
|
||||
cell.effectiveAccess.source === "exception" && !publicSelfService && "access-cell--exception",
|
||||
readOnly && "access-cell--readonly",
|
||||
isPending && "access-cell--pending",
|
||||
active && "access-cell--active"
|
||||
);
|
||||
@@ -3685,6 +3728,15 @@ function AccessCellControl({
|
||||
);
|
||||
}
|
||||
|
||||
if (readOnly) {
|
||||
return (
|
||||
<span className={cellClassName}>
|
||||
<strong>{displayTitle}</strong>
|
||||
<span>{displaySource}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<NodeDcSelect
|
||||
value={selectValue}
|
||||
|
||||
Reference in New Issue
Block a user