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.tsTest:
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.tsModify:
frontend/src/App.tsxModify:
frontend/src/hooks/dashboard/useSuperAdminDashboard.tsModify:
frontend/src/hooks/dashboard/useAdminDashboard.tsModify:
frontend/src/hooks/dashboard/useOwnerDashboard.tsModify:
frontend/src/hooks/dashboard/useStaffDashboard.tsModify:
frontend/src/hooks/dashboard/useTechnicalDashboard.tsModify:
frontend/src/pages/organizations/components/OrganizationDevicesTab.tsxTest:
frontend/src/pages/organizations/components/OrganizationDevicesTab.spec.tsxStep 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.tsTest:
frontend/src/pages/organizations/components/OrganizationDevicesTab.spec.tsxTest (if present / create minimal):
frontend/src/pages/organizations/organizationUserCapabilities.spec.tsStep 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:
RoleEnumfromfrontend/src/shared/auth/roles.ts. - Strict parser helper used consistently:
toRoleOrNull(value). - Capability ranking and set membership use
RoleEnumkeys/values only.