Skip to content

Feat/rbac page scoped permissions#61

Merged
ilramdhan merged 12 commits into
mutugading:mainfrom
ilramdhan:feat/rbac-page-scoped-permissions
Jul 1, 2026
Merged

Feat/rbac page scoped permissions#61
ilramdhan merged 12 commits into
mutugading:mainfrom
ilramdhan:feat/rbac-page-scoped-permissions

Conversation

@ilramdhan

Copy link
Copy Markdown
Member

Description

Type of Change

  • 🐛 Bug fix
  • ✨ New feature
  • 🎨 UI/UX improvement
  • ♻️ Refactor
  • 📚 Documentation
  • 🔧 Chore (deps, config)

Module/Component Affected

  • Dashboard
  • Finance
  • HR / IT / CI / EXSIM
  • Components (common/)
  • Components (ui/)
  • Navigation
  • API Routes

Changes Made

Related Issues

Fixes #
Related to #

Screenshots

Before

After

Testing Performed

Manual Testing

  • Desktop (1440px+)
  • Tablet (768px)
  • Mobile (375px)
  • Light mode
  • Dark mode

Browser Testing

  • Chrome
  • Firefox
  • Safari
  • Edge

Build Verification

  • npm run lint passes
  • npx tsc --noEmit passes
  • npm run build succeeds

Accessibility

  • Keyboard navigation works
  • Screen reader compatible
  • Proper ARIA labels
  • Color contrast adequate

Performance

  • No unnecessary re-renders
  • Images optimized
  • Heavy components lazy loaded

Pre-merge Checklist

  • I have read and followed RULES.md
  • I have read and followed CONTRIBUTING.md
  • Loading states implemented (if data fetching)
  • Error handling present
  • Component props typed properly
  • Uses semantic color classes
  • Responsive design tested
  • Dark mode compatible
  • Screenshots included (for UI changes)

Reviewer Notes

ilramdhan and others added 12 commits July 1, 2026 09:19
…d menu_id (Tasks 8, 11)

Task 8: ListPermissionsParams gains menuId; BFF list route forwards menu_id.
Task 11: description now required (zod min 1) with helper copy; new searchable
MenuCombobox (Popover+Command, no raw UUID, 'Global / none' option) sets menu_id
on create + update. tsc clean; eslint 0 errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
groupPermissionsByMenu pure converter (5 vitest cases: grouping, global-last,
first-seen order, whitespace-menuId, title fallback). PermissionPicker renders
permissions grouped by page with per-page select-all (indeterminate), per-row
name+action+description, and read-only 'from role' inherited rows for reuse in
the direct user-permission dialog.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s (Task 10)

Replaces the service->module->entity tree with the shared PermissionPicker fed by
the full permission catalog (usePermissions pageSize 500). Permissions now group by
owning page with per-page select-all and per-row descriptions; diff-based save
(assign/remove) unchanged. Directly addresses the 'grouped by page' pain point.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds 'Manage Permissions' user action opening a page-grouped picker (reuses
PermissionPicker). Direct grants are toggleable; role-inherited permissions show
checked + read-only 'from role'. New BFF routes users/[id]/permissions(+/remove),
useAssignUserPermissions/useRemoveUserPermissions hooks (backend already supported
this). Resolves role-sprawl: grant one permission without a throwaway role.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New /administrator/permissions/catalog route: all permissions grouped by owning
page (via groupPermissionsByMenu) with name, code, action, description, and role
count — answers 'what permissions does this page have and what do they do' without
reading code. Searchable across page/name/code/description. Linked from the
permission management page header ('View catalog').

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
src/lib/rbac/permissions.ts: all 279 permission codes grouped by owning page
(generated from the RBAC audit / spec Appendix A), so components import constants
instead of scattered string literals. Refactors product-requests-page-client to
use PERMISSIONS.ProductRequests.requestCreate as the reference pattern; remaining
call sites can adopt incrementally.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ListPermissions and ListMenus cap page_size at 100 (buf.validate lte:100), so the
pickers' pageSize:500 and MenuCombobox's 200 were rejected -> empty responses
('No permissions available', menu combobox only 'Global/none'). Add useAllPermissions()
backed by GetPermissionsByService (no pagination, already returns menu_id) and use it
in the role dialog, user-permission dialog, and catalog; cap MenuCombobox at 100.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…og grouping

MenuCombobox passed sortBy 'sortOrder' but ListMenus only allows snake_case
'sort_order' (proto in-list) -> request rejected -> only 'Global/none' showed.
Fixed to 'sort_order'. Also rebuilt MenuPermissionDialog (menu visibility gating)
to use useAllPermissions + shared PermissionPicker: was capped at pageSize 100
(truncated to first 100 of 279 perms) and ungrouped; now full + grouped by page.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
roles[].permissions from the backend is empty, so the previous readOnlyIds computation
never marked anything. Now compute role-inherited = allPermissionCodes (effective) minus
direct-grant codes, mapped to catalog IDs. Role-inherited permissions show checked +
read-only 'from role', so an admin cannot redundantly grant a permission the user already
has via a role; direct selection is the true direct grants only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds page-scoped RBAC permission metadata (menuId/menuTitle) and introduces shared UI tooling to browse and assign permissions by owning page, including new direct user-permission assignment flows and a permission catalog view.

Changes:

  • Extend IAM permission types/requests to carry menuId and menuTitle, and add menuId filtering support.
  • Introduce shared permission grouping + selection components (grouping helper, picker UI, and menu combobox) and reuse them across role/menu/user assignment dialogs.
  • Add direct user-permission assignment hooks + API routes, plus an admin “Permission Catalog” page and a centralized PERMISSIONS constants map.

Reviewed changes

Copilot reviewed 24 out of 25 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/types/iam/user.ts Re-export generated request/response types/parsers for direct user-permission assignment.
src/types/iam/role.ts Add menuId to list params to support page-scoped permission filtering.
src/types/generated/iam/v1/role.ts Generated updates adding menuId/menuTitle fields and request support.
src/lib/rbac/permissions.ts New generated permission-code constant map grouped by page.
src/lib/rbac/group-permissions.ts New utility to group permissions by owning menu/page (global bucket last).
src/hooks/iam/use-users.ts Add mutations for assigning/removing direct permissions for users.
src/hooks/iam/use-permissions.ts Add useAllPermissions() to flatten full permission catalog from by-service endpoint.
src/components/settings/users/user-table.tsx Add optional “Manage Permissions” row action.
src/components/settings/users/user-permission-dialog.tsx New dialog to manage direct permissions on a user, with role-inherited read-only selections.
src/components/settings/users/index.ts Export UserPermissionDialog.
src/components/settings/roles/role-permissions-dialog.tsx Refactor to use PermissionPicker + full permissions catalog.
src/components/settings/rbac/permission-picker.tsx New shared grouped permission selector UI (collapsible groups + select-all).
src/components/settings/rbac/menu-combobox.tsx New menu/page selector for scoping a permission to a menu.
src/components/settings/permissions/permission-form-dialog.tsx Make description required and add page/menu association via MenuCombobox.
src/components/iam/menus/menu-permission-dialog.tsx Refactor to use PermissionPicker and useAllPermissions().
src/app/api/v1/iam/users/[userId]/permissions/route.ts New API route to assign direct permissions to a user.
src/app/api/v1/iam/users/[userId]/permissions/remove/route.ts New API route to remove direct permissions from a user.
src/app/api/v1/iam/permissions/route.ts Add query parsing for menuId filter.
src/app/(dashboard)/finance/product-requests/product-requests-page-client.tsx Switch hardcoded permission string to PERMISSIONS constant.
src/app/(dashboard)/administrator/users/users-page-client.tsx Wire up user permission dialog and handler.
src/app/(dashboard)/administrator/permissions/permissions-page-client.tsx Add link to the new permissions catalog page.
src/app/(dashboard)/administrator/permissions/catalog/permission-catalog-client.tsx New searchable catalog browsing permissions grouped by page.
src/app/(dashboard)/administrator/permissions/catalog/page.tsx New page wrapper for the catalog.
src/app/(dashboard)/administrator/permissions/catalog/loading.tsx New loading skeleton for the catalog route.
src/tests/rbac/group-permissions.test.ts Add unit tests for permission grouping behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +112 to +115
<span className="flex items-center gap-2 text-xs text-muted-foreground">
{selectedCount}/{toggleableIds.length}
{open ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
</span>
Comment on lines +25 to +26
// GLOBAL_OPTION represents a permission that is not scoped to any single page.
const GLOBAL_VALUE = "__global__"
Comment on lines +49 to 53
if (open) {
// eslint-disable-next-line react-hooks/set-state-in-effect -- intentional: sync selection state when dialog opens
setSelected(new Set(currentPermIds))
}
}, [open, JSON.stringify(currentPermIds)]) // eslint-disable-line react-hooks/exhaustive-deps
Comment on lines +36 to +40
Dashboard: {
dashboardView: "ci.module.dashboard.view",
},
// Dashboard (EXSIM_DASHBOARD)
Dashboard1: {
@ilramdhan ilramdhan merged commit de4e40f into mutugading:main Jul 1, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants