Frontend Global Role Enum Centralization Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Centralize frontend role checking on one global enum aligned with backend role values and enforce strict role parsing at permission-check boundaries.

Architecture: Introduce a shared role module as the single role source of truth, then migrate existing dashboard/app/organization permission checks to consume this module. Replace soft-normalization-based checks with strict parse helpers so invalid role formats are non-privileged by default.

Tech Stack: React 19, TypeScript, Vite, Ant Design, Bun workspace tests (via Make wrapper)


File Structure Map

  • Create: frontend/src/shared/auth/roles.ts
    Single source of truth for role enum + strict helpers (isRole, toRoleOrNull, assertRole).

  • Modify: frontend/src/pages/dashboard/dashboard-role.ts
    Remove duplicate enum ownership and re-export / derive schema from shared global role enum.

  • Modify: frontend/src/App.tsx
    Replace role checks/imports to use shared global role enum.

  • Modify: frontend/src/hooks/dashboard/useSuperAdminDashboard.ts
    Replace role import for query key role constant.

  • Modify: frontend/src/hooks/dashboard/useAdminDashboard.ts
    Replace role import for query key role constant.

  • Modify: frontend/src/hooks/dashboard/useOwnerDashboard.ts
    Replace role import for query key role constant.

  • Modify: frontend/src/hooks/dashboard/useStaffDashboard.ts
    Replace role import for query key role constant.

  • Modify: frontend/src/hooks/dashboard/useTechnicalDashboard.ts
    Replace role import for query key role constant.

  • Modify: frontend/src/pages/organizations/components/OrganizationDevicesTab.tsx
    Replace role comparison to shared enum.

  • Modify: frontend/src/pages/organizations/organizationUserCapabilities.ts
    Remove soft normalize path and convert to strict role parsing/comparison.

  • Modify/Test: frontend/src/pages/organizations/components/OrganizationDevicesTab.spec.tsx
    Update role fixtures and assertions for strict backend-format behavior.

  • Test (if present / touched): frontend/src/pages/organizations/organizationUserCapabilities*.spec.ts
    Add/update strict parsing permission tests for valid vs invalid role values.


Task 1: Add Shared Global Role Module (TDD)

Files:

  • Create: frontend/src/shared/auth/roles.ts

  • Test: frontend/src/pages/organizations/components/OrganizationDevicesTab.spec.tsx (temporary helper usage assertion if no direct unit test file exists)

  • Step 1: Write failing strict-helper test case in existing spec (or utility spec if available)

it('treats non-backend role format as invalid', () => {
  const invalidRole = 'Super Admin';
  // role helper should reject this format
  expect(toRoleOrNull(invalidRole)).toBeNull();
});
  • Step 2: Run test to verify it fails

Run: make frontend:test FILE=src/pages/organizations/components/OrganizationDevicesTab.spec.tsx
Expected: FAIL with toRoleOrNull is not defined or import/module missing.

  • Step 3: Implement shared role module minimally
export enum RoleEnum {
  SUPER_ADMIN = 'SUPER_ADMIN',
  ADMIN = 'ADMIN',
  OWNER = 'OWNER',
  STAFF = 'STAFF',
  TECHNICAL = 'TECHNICAL',
}

const roleValues = new Set<string>(Object.values(RoleEnum));

export function isRole(value: unknown): value is RoleEnum {
  return typeof value === 'string' && roleValues.has(value);
}

export function toRoleOrNull(value: unknown): RoleEnum | null {
  return isRole(value) ? value : null;
}

export function assertRole(value: unknown): RoleEnum {
  if (!isRole(value)) {
    throw new Error(`Invalid role: ${String(value)}`);
  }
  return value;
}
  • Step 4: Run test to verify it passes

Run: make frontend:test FILE=src/pages/organizations/components/OrganizationDevicesTab.spec.tsx
Expected: PASS for strict-helper assertion.

  • Step 5: Commit
git add frontend/src/shared/auth/roles.ts frontend/src/pages/organizations/components/OrganizationDevicesTab.spec.tsx
git commit -m "feat(frontend): add shared strict role enum helpers"

Task 2: Replace Dashboard/App/DevicesTab Role Imports With Shared Enum

Files:

  • Modify: frontend/src/pages/dashboard/dashboard-role.ts

  • Modify: frontend/src/App.tsx

  • Modify: frontend/src/hooks/dashboard/useSuperAdminDashboard.ts

  • Modify: frontend/src/hooks/dashboard/useAdminDashboard.ts

  • Modify: frontend/src/hooks/dashboard/useOwnerDashboard.ts

  • Modify: frontend/src/hooks/dashboard/useStaffDashboard.ts

  • Modify: frontend/src/hooks/dashboard/useTechnicalDashboard.ts

  • Modify: frontend/src/pages/organizations/components/OrganizationDevicesTab.tsx

  • Test: frontend/src/pages/organizations/components/OrganizationDevicesTab.spec.tsx

  • Step 1: Write failing assertion for shared enum usage path in affected behavior

it('allows create device only for SUPER_ADMIN role constant', () => {
  const actorRole = RoleEnum.SUPER_ADMIN;
  expect(actorRole).toBe('SUPER_ADMIN');
});
  • Step 2: Run test to verify pre-migration failure (import/type mismatch)

Run: make frontend:test FILE=src/pages/organizations/components/OrganizationDevicesTab.spec.tsx
Expected: FAIL if test still relies on old enum imports.

  • Step 3: Migrate imports and enum references
// dashboard-role.ts
import { RoleEnum } from '@/shared/auth/roles';
import { z } from 'zod';

export const dashboardRoleSchema = z.nativeEnum(RoleEnum);
export type DashboardRole = z.infer<typeof dashboardRoleSchema>;
export { RoleEnum as DashboardRoleEnum };
// Example in App.tsx and dashboard hooks / devices tab
import { RoleEnum } from '@/shared/auth/roles';

roles={[RoleEnum.SUPER_ADMIN]}

const canCreateDevice = capabilities.canAccessTab && actorRole === RoleEnum.SUPER_ADMIN;

queryKey: ['dashboard', RoleEnum.ADMIN, 'organization'];
  • Step 4: Run targeted test and typecheck-equivalent frontend test command

Run: make frontend:test FILE=src/pages/organizations/components/OrganizationDevicesTab.spec.tsx
Expected: PASS with no enum import errors.

  • Step 5: Commit
git add frontend/src/pages/dashboard/dashboard-role.ts frontend/src/App.tsx frontend/src/hooks/dashboard/useSuperAdminDashboard.ts frontend/src/hooks/dashboard/useAdminDashboard.ts frontend/src/hooks/dashboard/useOwnerDashboard.ts frontend/src/hooks/dashboard/useStaffDashboard.ts frontend/src/hooks/dashboard/useTechnicalDashboard.ts frontend/src/pages/organizations/components/OrganizationDevicesTab.tsx frontend/src/pages/organizations/components/OrganizationDevicesTab.spec.tsx
git commit -m "refactor(frontend): migrate dashboard and device role checks to shared enum"

Task 3: Enforce Strict Role Parsing in Organization Capabilities

Files:

  • Modify: frontend/src/pages/organizations/organizationUserCapabilities.ts

  • Test: frontend/src/pages/organizations/components/OrganizationDevicesTab.spec.tsx

  • Test (if present / create minimal): frontend/src/pages/organizations/organizationUserCapabilities.spec.ts

  • Step 1: Write failing tests for strict invalid-role behavior

it('does not grant access for display-format role', () => {
  const caps = getOrganizationUserCapabilities({
    actorRole: 'Super Admin',
    actorOrganizationId: 'org-a',
    selectedOrganizationId: 'org-a',
    targetRoleNames: [],
  });

  expect(caps.canAccessTab).toBe(false);
  expect(caps.canCreateUser).toBe(false);
});
it('grants expected access for backend-format role', () => {
  const caps = getOrganizationUserCapabilities({
    actorRole: 'SUPER_ADMIN',
    actorOrganizationId: 'org-a',
    selectedOrganizationId: 'org-b',
    targetRoleNames: ['OWNER'],
  });

  expect(caps.canAccessTab).toBe(true);
  expect(caps.canCreateUser).toBe(true);
});
  • Step 2: Run tests to verify failure before implementation

Run: make frontend:test FILE=src/pages/organizations/components/OrganizationDevicesTab.spec.tsx
Expected: FAIL because current normalization path treats display format as valid.

  • Step 3: Replace normalization logic with strict role parsing
import { RoleEnum, toRoleOrNull } from '@/shared/auth/roles';

const TAB_ACCESS_ROLES = new Set<RoleEnum>([
  RoleEnum.SUPER_ADMIN,
  RoleEnum.ADMIN,
  RoleEnum.OWNER,
]);

const MANAGE_USER_ROLES = new Set<RoleEnum>([
  RoleEnum.SUPER_ADMIN,
  RoleEnum.ADMIN,
  RoleEnum.OWNER,
]);

const EDIT_USER_ROLES = new Set<RoleEnum>([
  RoleEnum.SUPER_ADMIN,
  RoleEnum.ADMIN,
  RoleEnum.OWNER,
]);

const roleRank: Record<RoleEnum, number> = {
  [RoleEnum.TECHNICAL]: 1,
  [RoleEnum.STAFF]: 2,
  [RoleEnum.OWNER]: 3,
  [RoleEnum.ADMIN]: 4,
  [RoleEnum.SUPER_ADMIN]: 5,
};

export function getOrganizationUserCapabilities(input: OrganizationUserCapabilityInput) {
  const actorRole = toRoleOrNull(input.actorRole);
  if (!actorRole) {
    return {
      canAccessTab: false,
      canCreateUser: false,
      canDeleteUser: false,
      canEditUser: false,
      canToggleStatus: false,
    };
  }

  const inScope =
    actorRole === RoleEnum.SUPER_ADMIN ||
    input.actorOrganizationId === input.selectedOrganizationId;

  const targetHighestRank = Math.max(
    0,
    ...(input.targetRoleNames ?? [])
      .map((role) => toRoleOrNull(role))
      .map((role) => (role ? roleRank[role] : 0)),
  );

  const actorRank = roleRank[actorRole];
  const canManageTarget =
    targetHighestRank < actorRank || actorRole === RoleEnum.SUPER_ADMIN;

  return {
    canAccessTab: inScope && TAB_ACCESS_ROLES.has(actorRole),
    canCreateUser: inScope && MANAGE_USER_ROLES.has(actorRole),
    canDeleteUser: inScope && MANAGE_USER_ROLES.has(actorRole) && (canManageTarget || !targetHighestRank),
    canEditUser: inScope && EDIT_USER_ROLES.has(actorRole) && (canManageTarget || !targetHighestRank),
    canToggleStatus: inScope && EDIT_USER_ROLES.has(actorRole) && (canManageTarget || !targetHighestRank),
  };
}
  • Step 4: Run tests to verify strict behavior passes

Run: make frontend:test FILE=src/pages/organizations/components/OrganizationDevicesTab.spec.tsx
Expected: PASS with invalid-display-format role denied and valid backend-format role accepted.

  • Step 5: Commit
git add frontend/src/pages/organizations/organizationUserCapabilities.ts frontend/src/pages/organizations/components/OrganizationDevicesTab.spec.tsx
# Include capabilities spec file if created/modified
git commit -m "refactor(frontend): enforce strict backend role parsing in capabilities"

Task 4: Regression Verification and Cleanup of Old Role Usage

Files:

  • Modify (if needed): any frontend file still importing old localized role enum directly

  • Test: all tests touched in previous tasks

  • Step 1: Search for leftover old role enum/string-literal patterns

Run:

grep -R "DashboardRoleEnum\|super admin\|Super Admin\|super_admin" frontend/src

Expected: no permission-check logic relies on display/normalized literals; any remaining matches are intentional display text only.

  • Step 2: If leftover permission checks exist, migrate them to shared enum
import { RoleEnum } from '@/shared/auth/roles';

if (actorRole === RoleEnum.ADMIN) {
  // permission path
}
  • Step 3: Re-run targeted tests after cleanup

Run: make frontend:test FILE=src/pages/organizations/components/OrganizationDevicesTab.spec.tsx
Expected: PASS.

  • Step 4: Final commit for cleanup
git add frontend/src
git commit -m "chore(frontend): remove residual non-global role checks"

Spec Coverage Check

  • Shared role module + strict helpers: covered by Task 1.
  • Dashboard/app/devices role migration to one enum source: covered by Task 2.
  • Remove soft normalization and enforce strict parsing in capabilities: covered by Task 3.
  • Verify no lingering non-global role checks in affected paths: covered by Task 4.

No uncovered spec requirement remains.

Placeholder Scan

  • No TODO/TBD placeholders.
  • Each code-changing step includes concrete code.
  • Each test step includes explicit command and expected result.

Type Consistency Check

  • Single role type used throughout: RoleEnum from frontend/src/shared/auth/roles.ts.
  • Strict parser helper used consistently: toRoleOrNull(value).
  • Capability ranking and set membership use RoleEnum keys/values only.