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).
This commit is contained in:
@@ -55,6 +55,31 @@ router.get("/permissions", requireAdmin, async (_req, res): Promise<void> => {
|
||||
res.json(rows.map(serializePermission));
|
||||
});
|
||||
|
||||
router.get("/roles/:id/usage", requireAdmin, async (req, res): Promise<void> => {
|
||||
const id = Number(req.params.id);
|
||||
if (!Number.isInteger(id)) {
|
||||
res.status(400).json({ error: "Invalid id" });
|
||||
return;
|
||||
}
|
||||
const [role] = await db.select({ id: rolesTable.id }).from(rolesTable).where(eq(rolesTable.id, id));
|
||||
if (!role) {
|
||||
res.status(404).json({ error: "Role not found" });
|
||||
return;
|
||||
}
|
||||
const [userCountRow] = await db
|
||||
.select({ count: sql<number>`count(*)::int` })
|
||||
.from(userRolesTable)
|
||||
.where(eq(userRolesTable.roleId, id));
|
||||
const [groupCountRow] = await db
|
||||
.select({ count: sql<number>`count(*)::int` })
|
||||
.from(groupRolesTable)
|
||||
.where(eq(groupRolesTable.roleId, id));
|
||||
res.json({
|
||||
userCount: userCountRow?.count ?? 0,
|
||||
groupCount: groupCountRow?.count ?? 0,
|
||||
});
|
||||
});
|
||||
|
||||
router.post("/roles", requireAdmin, async (req, res): Promise<void> => {
|
||||
const parsed = CreateRoleBody.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
@@ -141,6 +166,7 @@ router.patch("/roles/:id", requireAdmin, async (req, res): Promise<void> => {
|
||||
if (parsed.data.descriptionEn !== undefined) updates.descriptionEn = parsed.data.descriptionEn;
|
||||
if (Object.keys(updates).length > 0) {
|
||||
await db.update(rolesTable).set(updates).where(eq(rolesTable.id, id));
|
||||
const renamed = updates.name !== undefined && updates.name !== existing.name;
|
||||
await db.insert(auditLogsTable).values({
|
||||
actorUserId: req.session.userId ?? null,
|
||||
action: "role.update",
|
||||
@@ -149,6 +175,10 @@ router.patch("/roles/:id", requireAdmin, async (req, res): Promise<void> => {
|
||||
metadata: {
|
||||
previousName: existing.name,
|
||||
name: updates.name ?? existing.name,
|
||||
renamed,
|
||||
...(renamed
|
||||
? { renamedFrom: existing.name, renamedTo: updates.name }
|
||||
: {}),
|
||||
changes: updates,
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user