Soul is now protecting tables using permissions for roles.
A couple of issues have demonstrated that this is working effectively.
But, while that is valid, we use roles to decide which actions to offer to the user, either by using the role-string or the id of the role.
This logic is spread across VAL. It would be easier to audit/verify role permissions if these were in one place.
We have a rolesThatCanEditPassword const in UserShow. It contains an array of role names that can do the operation described in the constant name.
We should follow this practice across all of VAL, centralising them in a file called clientPermissions. Note: we should use const values for role-value, not strings.
Note: this requires further analysis, to identify/list places where permissions are implemented in the UI.
Analysis Complete ✅
Permission logic is decentralized. While useCanAccess centralizes permission data (from backend), components bypass it for direct role checks.
Scattered Permission Checks Found
1. Role String Arrays
UserShow.tsx:523 - rolesThatCanEditPassword
DispatchShow.tsx:136 - rolesThatCanCreateReceiptNote
DispatchList.tsx:93 - rolesThatCanCreateReceiptNote
DestructionShow.tsx:116 - DestructionCetificateAndDestoryCanAccessBy
2. Hardcoded Role IDs (magic numbers)
permissions.ts:62-80 - Direct role_id === 3/2/1 checks
authProvider/index.ts:79-83 - Maps role_id → role strings
UserShow.tsx:420-428 - Display logic with hardcoded IDs
UserForm.tsx:279-284 - MenuItem values use string IDs
3. Direct getUser() Calls
- Used 13 times across codebase
- Pattern:
getUser() → check user.userRole against strings
Implementation Plan
Phase 1: Create Constants & Permission Definitions
File: src/providers/authProvider/clientPermissions.ts
// Role constants (source of truth)
export const ROLE_ID = {
NONE: 1,
RCO_USER: 2,
RCO_POWER_USER: 3
} as const
export const ROLE_NAME = {
NONE: 'default',
RCO_USER: 'rco-user',
RCO_POWER_USER: 'rco-power-user'
} as const
// Operation-specific permission functions
export const canEditPassword = (userRole: string): boolean => {
return [ROLE_NAME.RCO_USER, ROLE_NAME.RCO_POWER_USER].includes(userRole)
}
export const canCreateReceiptNote = (userRole: string): boolean => {
return [ROLE_NAME.RCO_USER, ROLE_NAME.RCO_POWER_USER].includes(userRole)
}
export const canAccessDestructionCertificate = (userRole: string): boolean => {
return [ROLE_NAME.RCO_USER, ROLE_NAME.RCO_POWER_USER].includes(userRole)
}
// Reference data permissions by role ID
export const getReferenceDataPermissions = (roleId: number): Permission => {
switch (roleId) {
case ROLE_ID.RCO_POWER_USER:
return { read: true, write: true, delete: false }
case ROLE_ID.RCO_USER:
return { read: true, write: false, delete: false }
case ROLE_ID.NONE:
default:
return { read: false, write: false, delete: false }
}
}
// Display name helper
export const getRoleDisplayName = (roleId: number): string => {
switch (roleId) {
case ROLE_ID.RCO_POWER_USER:
return 'RCO Power User'
case ROLE_ID.RCO_USER:
return 'RCO User'
case ROLE_ID.NONE:
return 'Default Role'
default:
return 'User have no role'
}
}
Phase 2: Refactor Existing Files
2.1 Update permissions.ts (lines 62-80)
Replace hardcoded role_id checks with getReferenceDataPermissions(permissions[0]?.role_id)
2.2 Update authProvider/index.ts (lines 79-83)
Replace mapping logic with constants:
const userValue =
userRoleId.data.data[0]?.role_id === ROLE_ID.RCO_USER
? ROLE_NAME.RCO_USER
: userRoleId.data.data[0]?.role_id === ROLE_ID.RCO_POWER_USER
? ROLE_NAME.RCO_POWER_USER
: ROLE_NAME.NONE
2.3 Update Component Files
Replace local arrays with centralized functions:
Phase 3: Update Types
Add to src/types.d.ts:
type RoleId = 1 | 2 | 3
type RoleName = 'default' | 'rco-user' | 'rco-power-user'
Phase 4: Testing
- Verify all permission checks still work
- Test with each role: none, rco-user, rco-power-user, superuser
- Check:
- Password editing permissions
- Dispatch receipt note permissions
- Destruction certificate access
- Reference data permissions
- User role display names
Phase 5: Documentation
Update CLAUDE.md to document:
- Location of centralized permissions:
src/providers/authProvider/clientPermissions.ts
- Pattern for adding new permission checks
- Available role constants
Benefits
- Single source of truth for role constants
- Easy to audit - all permission logic in one file
- Type-safe - constants prevent typos
- Maintainable - change role logic in one place
- Testable - permission functions can be unit tested
Files to Change
- Create:
src/providers/authProvider/clientPermissions.ts
- Update:
src/providers/authProvider/permissions.ts
- Update:
src/providers/authProvider/index.ts
- Update:
src/resources/users/UserShow.tsx
- Update:
src/resources/users/UserForm.tsx
- Update:
src/resources/dispatch/DispatchShow.tsx
- Update:
src/resources/dispatch/DispatchList.tsx
- Update:
src/resources/destruction/DestructionShow.tsx
- Update:
src/types.d.ts
- Update:
CLAUDE.md
Soul is now protecting tables using permissions for roles.
A couple of issues have demonstrated that this is working effectively.
But, while that is valid, we use roles to decide which actions to offer to the user, either by using the role-string or the id of the role.
This logic is spread across VAL. It would be easier to audit/verify role permissions if these were in one place.
We have a
rolesThatCanEditPasswordconst inUserShow. It contains an array of role names that can do the operation described in the constant name.We should follow this practice across all of VAL, centralising them in a file called
clientPermissions. Note: we should useconstvalues for role-value, not strings.Note: this requires further analysis, to identify/list places where permissions are implemented in the UI.
Analysis Complete ✅
Permission logic is decentralized. While
useCanAccesscentralizes permission data (from backend), components bypass it for direct role checks.Scattered Permission Checks Found
1. Role String Arrays
UserShow.tsx:523-rolesThatCanEditPasswordDispatchShow.tsx:136-rolesThatCanCreateReceiptNoteDispatchList.tsx:93-rolesThatCanCreateReceiptNoteDestructionShow.tsx:116-DestructionCetificateAndDestoryCanAccessBy2. Hardcoded Role IDs (magic numbers)
permissions.ts:62-80- Directrole_id === 3/2/1checksauthProvider/index.ts:79-83- Maps role_id → role stringsUserShow.tsx:420-428- Display logic with hardcoded IDsUserForm.tsx:279-284- MenuItem values use string IDs3. Direct
getUser()CallsgetUser()→ checkuser.userRoleagainst stringsImplementation Plan
Phase 1: Create Constants & Permission Definitions
File:
src/providers/authProvider/clientPermissions.tsPhase 2: Refactor Existing Files
2.1 Update
permissions.ts(lines 62-80)Replace hardcoded role_id checks with
getReferenceDataPermissions(permissions[0]?.role_id)2.2 Update
authProvider/index.ts(lines 79-83)Replace mapping logic with constants:
2.3 Update Component Files
Replace local arrays with centralized functions:
UserShow.tsx:523-533
UserShow.tsx:420-428
DispatchShow.tsx:136-139 & DispatchList.tsx:93-96
DestructionShow.tsx:116-123
UserForm.tsx:279-284
Phase 3: Update Types
Add to
src/types.d.ts:Phase 4: Testing
Phase 5: Documentation
Update
CLAUDE.mdto document:src/providers/authProvider/clientPermissions.tsBenefits
Files to Change
src/providers/authProvider/clientPermissions.tssrc/providers/authProvider/permissions.tssrc/providers/authProvider/index.tssrc/resources/users/UserShow.tsxsrc/resources/users/UserForm.tsxsrc/resources/dispatch/DispatchShow.tsxsrc/resources/dispatch/DispatchList.tsxsrc/resources/destruction/DestructionShow.tsxsrc/types.d.tsCLAUDE.md