Show users/groups impact before removing role permissions (Task #99)
Adds a per-permission impact preview to the role editor so admins can see
who would lose each permission before saving — and a confirmation step
when the change would actually revoke access from at least one user.
API
- New endpoint POST /api/roles/:id/permissions/impact-preview
- Body: { permissionIds: number[] } (the proposed kept set)
- Response: { removed: [{ permissionId, permissionName, userCount,
groupCount, groups: [{id,name}] }], totalAffectedUsers }
- A user is counted as "affected" only when no other role they hold
(direct or via groups) still grants the removed permission.
- Implemented in artifacts/api-server/src/routes/roles.ts using two
drizzle selectDistinct queries (direct + via groups) merged in JS.
Switched away from a sql`${arr}::int[]` approach that failed because
drizzle expands JS arrays as a parameter list, not as a single array
parameter. Now uses inArray().
OpenAPI / client
- Added schemas RolePermissionsImpactBody, RolePermissionImpactGroup,
RolePermissionImpactItem, RolePermissionsImpact in
lib/api-spec/openapi.yaml.
- Regenerated lib/api-client-react/src/generated/* (new
usePreviewRolePermissionsImpact hook).
UI
- artifacts/tx-os/src/pages/admin.tsx RolesPanel:
- Debounced (350ms) on-demand fetch only when at least one permission
has been unchecked relative to the saved state.
- Inline amber summary panel listing each removed permission with
user/group counts, plus a total-affected-users line.
- Confirmation modal before save when totalAffectedUsers > 0.
- Added EN/AR translations (impactTitle, impactPerPermission,
impactViaGroups, impactTotal, confirmRemoval*) using i18next
_one/_other plural keys to match existing pluralized strings.
Tests
- New artifacts/api-server/tests/role-permissions-impact.test.mjs
(6 tests, all green): empty removals, group-derived impact,
"covered by another role" exclusion, 404 on unknown role, 400 on
invalid body, 403 for non-admin.
- Existing roles-crud tests still pass.
- E2E flow verified end to end via the testing tool: amber summary
appears, confirmation dialog gates the destructive save, and the
resulting permission set persists.
Code-review follow-ups (applied in this same task)
- Tightened the OpenAPI 404 description for the new endpoint to clarify
that unknown permission IDs in the body are tolerated (only a missing
role yields 404). Regenerated the API client.
- Added a request-sequencing guard in the role editor so a stale
in-flight impact-preview response cannot overwrite a newer selection.
- When the impact preview fails while removals exist, the inline panel
now shows an explicit error message and the Save button is disabled
until the user resolves it (e.g. by changing the selection so the
preview can re-run successfully). EN/AR translations added under
admin.roles.impactError.
Notes
- The pre-existing test workflow failure (ECONNREFUSED on 127.0.0.1:8080
before the API server is ready) is unrelated and already tracked.
- replit.md not updated — change is endpoint-level and does not alter
documented architecture.
- A modification to artifacts/tx-os/public/opengraph.jpg may show up in
the diff; it is a build artifact regenerated by the vite dev server
on restart and is not part of this change.
Replit-Task-Id: 30b7a2f7-7b60-429a-89d4-79280a81803f
This commit is contained in:
@@ -67,6 +67,8 @@ import type {
|
||||
ResetPasswordBody,
|
||||
Role,
|
||||
RoleDeletionConflict,
|
||||
RolePermissionsImpact,
|
||||
RolePermissionsImpactBody,
|
||||
RoleUsage,
|
||||
SendMessageBody,
|
||||
Service,
|
||||
@@ -5219,6 +5221,104 @@ export function useGetRoleUsage<
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a candidate set of permission IDs for the role, returns the list of
|
||||
permissions that would be removed and, for each one, how many users would
|
||||
lose the permission (because they hold this role directly or via a group
|
||||
and have no other role granting the permission), plus the groups that
|
||||
currently grant this role.
|
||||
|
||||
* @summary Preview the impact of changing a role's permission set (admin)
|
||||
*/
|
||||
export const getPreviewRolePermissionsImpactUrl = (id: number) => {
|
||||
return `/api/roles/${id}/permissions/impact-preview`;
|
||||
};
|
||||
|
||||
export const previewRolePermissionsImpact = async (
|
||||
id: number,
|
||||
rolePermissionsImpactBody: RolePermissionsImpactBody,
|
||||
options?: RequestInit,
|
||||
): Promise<RolePermissionsImpact> => {
|
||||
return customFetch<RolePermissionsImpact>(
|
||||
getPreviewRolePermissionsImpactUrl(id),
|
||||
{
|
||||
...options,
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", ...options?.headers },
|
||||
body: JSON.stringify(rolePermissionsImpactBody),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const getPreviewRolePermissionsImpactMutationOptions = <
|
||||
TError = ErrorType<ErrorResponse>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof previewRolePermissionsImpact>>,
|
||||
TError,
|
||||
{ id: number; data: BodyType<RolePermissionsImpactBody> },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof previewRolePermissionsImpact>>,
|
||||
TError,
|
||||
{ id: number; data: BodyType<RolePermissionsImpactBody> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ["previewRolePermissionsImpact"];
|
||||
const { mutation: mutationOptions, request: requestOptions } = options
|
||||
? options.mutation &&
|
||||
"mutationKey" in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey }, request: undefined };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof previewRolePermissionsImpact>>,
|
||||
{ id: number; data: BodyType<RolePermissionsImpactBody> }
|
||||
> = (props) => {
|
||||
const { id, data } = props ?? {};
|
||||
|
||||
return previewRolePermissionsImpact(id, data, requestOptions);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type PreviewRolePermissionsImpactMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof previewRolePermissionsImpact>>
|
||||
>;
|
||||
export type PreviewRolePermissionsImpactMutationBody =
|
||||
BodyType<RolePermissionsImpactBody>;
|
||||
export type PreviewRolePermissionsImpactMutationError =
|
||||
ErrorType<ErrorResponse>;
|
||||
|
||||
/**
|
||||
* @summary Preview the impact of changing a role's permission set (admin)
|
||||
*/
|
||||
export const usePreviewRolePermissionsImpact = <
|
||||
TError = ErrorType<ErrorResponse>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof previewRolePermissionsImpact>>,
|
||||
TError,
|
||||
{ id: number; data: BodyType<RolePermissionsImpactBody> },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof previewRolePermissionsImpact>>,
|
||||
TError,
|
||||
{ id: number; data: BodyType<RolePermissionsImpactBody> },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getPreviewRolePermissionsImpactMutationOptions(options));
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary List permissions assigned to a role (admin)
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user