Accounts
The accounts module is Coreola’s admin surface for who can do what. It manages three resources:
- Users — the people who can sign in
- Roles — named bundles of abilities
- Abilities — the atomic permission definitions
These three are the editable side of Coreola’s RBAC system. The read side (gating routes and components) is covered in Permissions.
- Source:
src/features/application/accounts/ - API:
src/api/users/,src/api/roles/,src/api/abilities/ - Routes:
/application/accounts/users,/application/accounts/roles,/application/accounts/abilities
Layout
The accounts area is a section with three sub-pages stitched together by Accounts.tsx (a VerticalTabs + Outlet wrapper). Each tab opens one of the resources.
The route requires at least one of: account.read, role.read, ability.read. The redirect within Accounts picks the first tab the user can see based on the abilities they hold.
Users
Route
/application/accounts/users/:userId?
Model hooks
useUsers— list-level concerns: fetching, table state, opening the create dialog, deletion.useUserDetails— the per-user form:- User CRUD form (name, email, role assignment)
- Suspended toggle (
suspendedfield onUser) - Avatar editor integration via
useAvatarEditor - Delete dialog
- Server-error mapping via
useFieldsFromError
The list lives in the left tab pane; the right pane shows the selected user’s details when :userId is present.
User shape
From src/api/users/users.types.ts:
id,email,first_name,last_name,avatarsuspended— boolean flag, soft-disable without deletionroles[]— role ids assigned to the userabilities— flattened abilities matrix (derived from roles, included for convenience)
Suspended vs deleted
A suspended user keeps their record (audit history, references on assessments) but cannot sign in. Deletion is irreversible. Operators should reach for suspend by default.
Roles
Route
/application/accounts/roles/:roleId?
Model hooks
useRoles— list of roles, create-new flow, delete.useRoleDetails— the per-role form:- Name + description fields.
- Abilities matrix rendered as
abilityRows— one row per ability, one checkbox per action (read,create,update,delete). onToggleAbility(abilityKey, action)toggles a single cell.
Role shape
RoleType {
id, name, description,
abilities: Record<string, RoleAbilityType>
// e.g. abilities = {
// "assessment": { read: true, create: true, update: false, delete: false },
// "customer": { read: true, create: false, update: false, delete: false },
// }
}A role is essentially a name + a matrix. The matrix is what buildAbilityFromMatrix turns into a CASL Ability at sign-in.
Abilities
Route
/application/accounts/abilities/:abilityId?
Model hooks
useAbilities— list and CRUD.useAbilityDetails— the per-ability form:key— the resource key (e.g.,assessment,customer)name— display labeldescription— what this ability covers- Action booleans (
read,create,update,delete) — the definition of which actions are valid for this resource.
Ability shape
AbilityType {
id, key, name, description,
read, create, update, delete // booleans — which actions exist for this resource
}An ability is a definition of “this resource exists and these actions are meaningful for it”. Whether a specific user can do those actions is determined by the role that the user holds — see the relationship below.
How users, roles, and abilities relate
Ability defines "what actions exist for resource X"
↑
│ referenced by
│
Role grants "for ability X, this role can read/create/update/delete"
↑
│ assigned to
│
User has "these roles"Concretely:
- An ability says: “the
assessmentresource has actionsread,create,update,delete”. - A role says: “for
assessment, this role canreadandcreate, but notupdateordelete”. - A user has roles. The user’s effective permissions are the union of the role matrices.
At sign-in, the backend resolves the user’s roles into a flattened matrix on user.abilities so the frontend does not have to do the join.
Editing the matrix — UX details
Both useRoleDetails and useAbilityDetails use abilityRows / abilityActionRow patterns to render the matrix as a checkbox grid:
read create update delete
assessment ☑ ☑ ☐ ☐
customer ☑ ☐ ☐ ☐
user ☐ ☐ ☐ ☐The grid is editable inline. Each toggle dispatches a single mutation against the role/ability — no “Save” button required. This gives operators fast iteration without ceremony, and the audit log captures each toggle as its own activity.
A common alternative is a draft-and-save pattern. Coreola chose inline because admin operations on roles are deliberate and low-frequency — the operator wants the toggle to be the action, not a step toward an action.
Permissions for the permissions UI
Yes, it’s recursive. The route gates are:
/application/accounts/users— requiresaccount.read/application/accounts/roles— requiresrole.read/application/accounts/abilities— requiresability.read
So the ability to manage abilities is itself an ability. Bootstrapping: the seeded admin user (json-server/data/api/users.json) has all three.
API
Three slices:
usersApi
GET/POST/PUT/DELETE /users— standard CRUD plus pagination, sort, filter.
rolesApi
GET/POST/PUT/DELETE /roles— CRUD over the role + matrix.
abilitiesApi
GET/POST/PUT/DELETE /abilities— CRUD over the ability definitions.
All three follow the same RTK Query patterns as the rest of the app. Custom Mapped hooks normalize names and the matrix shape for the UI.
Migration paths
If your product uses a different RBAC model:
Flat permissions (no roles)
Skip the roles resource. Assign abilities directly on the user. Keep usersApi and abilitiesApi, drop rolesApi.
Hierarchical roles (parent/child)
Add a parent_id field to RoleType and resolve permissions transitively at sign-in (the backend does this — frontend keeps reading the flat matrix on the user).
External IDP (Okta, Auth0, Keycloak)
Replace the sign-in mutation with an OIDC handshake. The user’s role assignment becomes a claim on the token; the matrix can still be computed server-side and shipped on user.abilities. The accounts UI then becomes read-only or disappears, depending on whether you let admins edit the IDP from inside the product.
Anti-patterns
- Hardcoding role names in components. Always check abilities, never
user.roles.includes('admin'). Roles are data; abilities are the API. - Building a “permissions” tab elsewhere. The accounts module is the single source of UI truth.
- Bypassing CASL for “convenience” checks. Use
useAbility().can(action, subject)everywhere — even for cosmetic UI changes.
Next steps
- Permissions — how the resulting matrix gates the UI
- Auth — where the matrix arrives in the user slice
- Tables — the underlying table for the user/role/ability lists