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
+44
View File
@@ -45,6 +45,8 @@ import {
getListPermissionsQueryKey,
useGetRolePermissions,
getGetRolePermissionsQueryKey,
useGetRoleUsage,
getGetRoleUsageQueryKey,
useReplaceRolePermissions,
useListAuditLogs,
getListAuditLogsQueryKey,
@@ -3226,6 +3228,7 @@ function RolesPanel() {
const [editingId, setEditingId] = useState<number | null>(null);
const [editingIsSystem, setEditingIsSystem] = useState(false);
const [editForm, setEditForm] = useState({ name: "", descriptionAr: "", descriptionEn: "" });
const [originalEditName, setOriginalEditName] = useState("");
const [editPermissionIds, setEditPermissionIds] = useState<Set<number>>(new Set());
const [initialPermissionIds, setInitialPermissionIds] = useState<Set<number>>(new Set());
const [deleteTarget, setDeleteTarget] = useState<{ id: number; name: string } | null>(null);
@@ -3241,6 +3244,12 @@ function RolesPanel() {
},
},
);
const { data: editingRoleUsage } = useGetRoleUsage(editingPermissionsId, {
query: {
queryKey: getGetRoleUsageQueryKey(editingPermissionsId),
enabled: editingId != null && !editingIsSystem,
},
});
useEffect(() => {
if (editingId == null) return;
@@ -3309,14 +3318,23 @@ function RolesPanel() {
descriptionAr: role.descriptionAr ?? "",
descriptionEn: role.descriptionEn ?? "",
});
setOriginalEditName(role.name);
};
const closeEditDialog = () => {
setEditingId(null);
setEditPermissionIds(new Set());
setInitialPermissionIds(new Set());
setOriginalEditName("");
};
const editNameTrimmed = editForm.name.trim();
const isRenaming =
!editingIsSystem &&
originalEditName.length > 0 &&
editNameTrimmed.length > 0 &&
editNameTrimmed !== originalEditName;
const permissionsChanged = (() => {
if (editPermissionIds.size !== initialPermissionIds.size) return true;
for (const id of editPermissionIds) {
@@ -3585,6 +3603,32 @@ function RolesPanel() {
<p className="text-xs text-muted-foreground">
{editingIsSystem ? t("admin.roles.editSystemNameHint") : t("admin.roles.nameHint")}
</p>
{isRenaming && (
<div
className="mt-2 rounded-xl border border-amber-300 bg-amber-50 p-3 text-xs text-amber-900 space-y-1"
data-testid="role-rename-warning"
role="alert"
>
<p className="font-semibold">
{t("admin.roles.renameWarningTitle", {
from: originalEditName,
to: editNameTrimmed,
})}
</p>
<p>{t("admin.roles.renameWarningBody")}</p>
{editingRoleUsage && (
<p
className="text-amber-800"
data-testid="role-rename-usage"
>
{t("admin.roles.renameUsage", {
users: editingRoleUsage.userCount,
groups: editingRoleUsage.groupCount,
})}
</p>
)}
</div>
)}
</div>
<div className="space-y-1">
<Label>{t("admin.roles.descriptionAr")}</Label>