Skip to content

Store client-side role-permissions in one place. #1118

@IanMayo

Description

@IanMayo

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:

  • UserShow.tsx:523-533

    // Before: const rolesThatCanEditPassword = ['rco-user', 'rco-power-user']
    // After: import { canEditPassword } from '../../providers/authProvider/clientPermissions'
    {userDetails && canEditPassword(userDetails.userRole) ? ... : null}
  • UserShow.tsx:420-428

    import { getRoleDisplayName } from '../../providers/authProvider/clientPermissions'
    {loading ? <LoadingIndicator /> : getRoleDisplayName(parseInt(userRoleId))}
  • DispatchShow.tsx:136-139 & DispatchList.tsx:93-96

    import { canCreateReceiptNote } from '../../providers/authProvider/clientPermissions'
    {user && canCreateReceiptNote(user.userRole) ? ... : null}
  • DestructionShow.tsx:116-123

    import { canAccessDestructionCertificate } from '../../providers/authProvider/clientPermissions'
    {user && canAccessDestructionCertificate(user.userRole) ? ... : null}
  • UserForm.tsx:279-284

    import { ROLE_ID } from '../../providers/authProvider/clientPermissions'
    <MenuItem value={ROLE_ID.NONE}>...</MenuItem>
    <MenuItem value={ROLE_ID.RCO_USER}>RCO User</MenuItem>
    <MenuItem value={ROLE_ID.RCO_POWER_USER}>RCO Power User</MenuItem>

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

  1. Verify all permission checks still work
  2. Test with each role: none, rco-user, rco-power-user, superuser
  3. 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

  1. Create: src/providers/authProvider/clientPermissions.ts
  2. Update: src/providers/authProvider/permissions.ts
  3. Update: src/providers/authProvider/index.ts
  4. Update: src/resources/users/UserShow.tsx
  5. Update: src/resources/users/UserForm.tsx
  6. Update: src/resources/dispatch/DispatchShow.tsx
  7. Update: src/resources/dispatch/DispatchList.tsx
  8. Update: src/resources/destruction/DestructionShow.tsx
  9. Update: src/types.d.ts
  10. Update: CLAUDE.md

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions