Files
ss-tools/frontend/src/lib/auth/permissions.js
busya 67867f8220 refactor(semantics): migrate legacy @TIER to @COMPLEXITY annotations
- Replaced @TIER: TRIVIAL with @COMPLEXITY: 1
- Replaced @TIER: STANDARD with @COMPLEXITY: 3
- Replaced @TIER: CRITICAL with @COMPLEXITY: 5
- Manually elevated specific critical/complex components to levels 2 and 4
- Ignored legacy, specs, and node_modules directories
- Updated generated semantic map
2026-03-16 10:06:44 +03:00

103 lines
3.6 KiB
JavaScript

// [DEF:frontend.src.lib.auth.permissions:Module]
// @COMPLEXITY: 3
// @SEMANTICS: auth, permissions, rbac, roles
// @PURPOSE: Shared frontend RBAC utilities for route guards and menu visibility.
// @LAYER: Domain
// @RELATION: USED_BY -> frontend.src.components.auth.ProtectedRoute
// @RELATION: USED_BY -> frontend.src.lib.components.layout.Sidebar
// @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;
}
// [DEF:normalizePermissionRequirement: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 };
}
// [/DEF:normalizePermissionRequirement:Function]
// [DEF:isAdminUser: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",
);
}
// [/DEF:isAdminUser:Function]
// [DEF:hasPermission: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;
}
// [/DEF:hasPermission:Function]
// [/DEF:frontend.src.lib.auth.permissions:Module]