Task #98: Warn admins before renaming a role used in code references

- Added GET /api/roles/:id/usage endpoint returning {userCount, groupCount},
  guarded by requireAdmin and validating the role id.
- Updated PATCH /api/roles/:id audit metadata to record renamed=true,
  renamedFrom, and renamedTo when the role name changes (in addition to
  existing name fields), satisfying the traceability requirement.
- Added RoleUsage schema + /roles/{id}/usage operation to openapi.yaml,
  regenerated lib/api-zod via orval. Codegen script in lib/api-spec was
  updated intentionally to preserve `export * from './manual'` in
  api-zod's index, which orval was overwriting.
- RolesPanel edit dialog (admin.tsx) now tracks the original role name
  on open and shows an amber warning banner with from/to text plus a
  usage line (users + groups counts) whenever the trimmed name input
  differs from the original. Warning is suppressed for system or
  protected roles since their name input is disabled.
- Added en/ar locale strings: renameWarningTitle, renameWarningBody,
  renameUsage.
- Reverted accidental opengraph.jpg change picked up earlier in the
  session — it is unrelated to this task.

Verification: e2e test created a non-system role, assigned the admin
user to it, opened the edit dialog, observed the warning + usage line,
saved the rename, and confirmed both the role name persisted and the
audit log captured renamed/renamedFrom/renamedTo.

Note: The pnpm `test` workflow shows pre-existing failures in
executive-meetings.ts that are unrelated to this task (verified via
git stash before changes).

Replit-Task-Id: c1ee048d-50c6-4439-a430-ee0b7ec09959
This commit is contained in:
riyadhafraa
2026-04-29 10:57:44 +00:00
parent 0d58a856d6
commit 59c7338d63
9 changed files with 222 additions and 1 deletions
@@ -215,6 +215,11 @@ export interface UpdateRoleBody {
descriptionEn?: string | null;
}
export interface RoleUsage {
userCount: number;
groupCount: number;
}
export interface RoleDeletionConflict {
error: string;
userCount: number;
+88
View File
@@ -67,6 +67,7 @@ import type {
ResetPasswordBody,
Role,
RoleDeletionConflict,
RoleUsage,
SendMessageBody,
Service,
ServiceCategory,
@@ -5131,6 +5132,93 @@ export const useDeleteRole = <
return useMutation(getDeleteRoleMutationOptions(options));
};
/**
* @summary Count users and groups currently assigned to a role (admin)
*/
export const getGetRoleUsageUrl = (id: number) => {
return `/api/roles/${id}/usage`;
};
export const getRoleUsage = async (
id: number,
options?: RequestInit,
): Promise<RoleUsage> => {
return customFetch<RoleUsage>(getGetRoleUsageUrl(id), {
...options,
method: "GET",
});
};
export const getGetRoleUsageQueryKey = (id: number) => {
return [`/api/roles/${id}/usage`] as const;
};
export const getGetRoleUsageQueryOptions = <
TData = Awaited<ReturnType<typeof getRoleUsage>>,
TError = ErrorType<ErrorResponse>,
>(
id: number,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getRoleUsage>>,
TError,
TData
>;
request?: SecondParameter<typeof customFetch>;
},
) => {
const { query: queryOptions, request: requestOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetRoleUsageQueryKey(id);
const queryFn: QueryFunction<Awaited<ReturnType<typeof getRoleUsage>>> = ({
signal,
}) => getRoleUsage(id, { signal, ...requestOptions });
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getRoleUsage>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetRoleUsageQueryResult = NonNullable<
Awaited<ReturnType<typeof getRoleUsage>>
>;
export type GetRoleUsageQueryError = ErrorType<ErrorResponse>;
/**
* @summary Count users and groups currently assigned to a role (admin)
*/
export function useGetRoleUsage<
TData = Awaited<ReturnType<typeof getRoleUsage>>,
TError = ErrorType<ErrorResponse>,
>(
id: number,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getRoleUsage>>,
TError,
TData
>;
request?: SecondParameter<typeof customFetch>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetRoleUsageQueryOptions(id, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary List permissions assigned to a role (admin)
*/
+36
View File
@@ -1425,6 +1425,31 @@ paths:
schema:
$ref: "#/components/schemas/RoleDeletionConflict"
/roles/{id}/usage:
get:
operationId: getRoleUsage
tags: [roles]
summary: Count users and groups currently assigned to a role (admin)
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
"200":
description: Counts of users and groups using the role
content:
application/json:
schema:
$ref: "#/components/schemas/RoleUsage"
"404":
description: Role not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/roles/{id}/permissions:
get:
operationId: getRolePermissions
@@ -2282,6 +2307,17 @@ components:
descriptionEn:
type: ["string", "null"]
RoleUsage:
type: object
properties:
userCount:
type: integer
groupCount:
type: integer
required:
- userCount
- groupCount
RoleDeletionConflict:
type: object
properties:
+1 -1
View File
@@ -3,7 +3,7 @@
"version": "0.0.0",
"private": true,
"scripts": {
"codegen": "orval --config ./orval.config.ts && echo \"export * from './generated/api';\" > ../api-zod/src/index.ts && pnpm -w run typecheck:libs"
"codegen": "orval --config ./orval.config.ts && printf \"export * from './generated/api';\\nexport * from './manual';\\n\" > ../api-zod/src/index.ts && pnpm -w run typecheck:libs"
},
"devDependencies": {
"orval": "^8.5.2"
+12
View File
@@ -1896,6 +1896,18 @@ export const DeleteRoleParams = zod.object({
id: zod.coerce.number(),
});
/**
* @summary Count users and groups currently assigned to a role (admin)
*/
export const GetRoleUsageParams = zod.object({
id: zod.coerce.number(),
});
export const GetRoleUsageResponse = zod.object({
userCount: zod.number(),
groupCount: zod.number(),
});
/**
* @summary List permissions assigned to a role (admin)
*/