// #region PermissionsModule [C:3] [TYPE Module] [SEMANTICS auth, rbac, permission, role, guard] // @BRIEF Shared frontend RBAC utilities for route guards, permission checks, and menu visibility based on user roles. // @RELATION DEPENDS_ON -> [AuthStore] // #region Permissions:Module [TYPE Function] // @SEMANTICS: auth, permissions, rbac, roles // @PURPOSE: Shared frontend RBAC utilities for route guards and menu visibility. // @LAYER Domain // @RELATION DEPENDS_ON -> [EXT:frontend:authStore] // @INVARIANT: Admin role always bypasses explicit permission checks. const KNOWN_ACTIONS = new Set(["READ", "WRITE", "EXECUTE", "DELETE"]); function normalizeAction(action, fallback = "READ") { const normalized = String(action || "").trim().toUpperCase(); if (!normalized) return fallback; return normalized; } // #region normalizePermissionRequirement:Function [TYPE Function] // @PURPOSE: Convert permission requirement string to canonical resource/action tuple. // @PRE: Permission can be "resource" or "resource:ACTION" where resource itself may contain ":". // @POST: Returns normalized object with action in uppercase. export function normalizePermissionRequirement(permission, defaultAction = "READ") { const fallbackAction = normalizeAction(defaultAction, "READ"); const rawPermission = String(permission || "").trim(); if (!rawPermission) { return { resource: null, action: fallbackAction }; } const parts = rawPermission.split(":"); if (parts.length > 1) { const tail = normalizeAction(parts[parts.length - 1], fallbackAction); if (KNOWN_ACTIONS.has(tail)) { const resource = parts.slice(0, -1).join(":"); return { resource, action: tail }; } } return { resource: rawPermission, action: fallbackAction }; } // #endregion normalizePermissionRequirement:Function // #region isAdminUser:Function [TYPE Function] // @PURPOSE: Determine whether user has Admin role. // @PRE: user can be null or partially populated. // @POST: Returns true when at least one role name equals "Admin" (case-insensitive). export function isAdminUser(user) { const roles = Array.isArray(user?.roles) ? user.roles : []; return roles.some( (role) => String(role?.name || "").trim().toLowerCase() === "admin", ); } // #endregion isAdminUser:Function // #region hasPermission:Function [TYPE Function] // @PURPOSE: Check if user has a required resource/action permission. // @PRE: user contains roles with permissions from /api/auth/me payload. // @POST: Returns true when requirement is empty, user is admin, or matching permission exists. export function hasPermission(user, requirement, action = "READ") { if (!requirement) return true; if (!user) return false; if (isAdminUser(user)) return true; const { resource, action: requiredAction } = normalizePermissionRequirement( requirement, action, ); if (!resource) return true; const roles = Array.isArray(user.roles) ? user.roles : []; for (const role of roles) { const permissions = Array.isArray(role?.permissions) ? role.permissions : []; for (const permission of permissions) { if (typeof permission === "string") { const normalized = normalizePermissionRequirement( permission, requiredAction, ); if ( normalized.resource === resource && normalized.action === requiredAction ) { return true; } continue; } const permissionResource = String(permission?.resource || "").trim(); const permissionAction = normalizeAction(permission?.action, ""); if ( permissionResource === resource && permissionAction === requiredAction ) { return true; } } } return false; } // #endregion hasPermission:Function // #endregion Permissions:Module // #endregion PermissionsModule