Converted 11 remaining production files:
- services/*.ts (toolsService, taskService, git-utils, storageService,
adminService, gitService)
- lib/auth/permissions.ts
- lib/components/layout/sidebarNavigation.ts
- lib/components/reports/reportTypeProfiles.ts
- components/git/useGitManager.ts
- routes/datasets/review/useReviewSession.ts
- routes/datasets/review/review-workspace-helpers.ts
- routes/settings/settings-utils.ts
All production files have proper GRACE contracts:
- C2/C3 with @BRIEF, @PRE, @POST, @SIDE_EFFECT, @RELATION
- Generic <T = unknown> for API calls
- Typed interfaces for state/options
Phase 3: 126 .svelte components → <script lang="ts">
Phase 4: 53 test .js files → .ts
Final verification:
- npm run build ✅
- npm run test ✅ (66 passed, 6 pre-existing failures unchanged)
Total: 0 .js files remain in production code
126 lines
4.2 KiB
TypeScript
126 lines
4.2 KiB
TypeScript
// #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.
|
|
|
|
interface UserRole {
|
|
name?: string;
|
|
permissions?: (string | PermissionObject)[];
|
|
}
|
|
|
|
interface PermissionObject {
|
|
resource?: string;
|
|
action?: string;
|
|
}
|
|
|
|
interface User {
|
|
roles?: UserRole[];
|
|
}
|
|
|
|
interface NormalizedPermission {
|
|
resource: string | null;
|
|
action: string;
|
|
}
|
|
|
|
const KNOWN_ACTIONS = new Set(["READ", "WRITE", "EXECUTE", "DELETE"]);
|
|
|
|
function normalizeAction(action: string, fallback = "READ"): string {
|
|
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: string, defaultAction = "READ"): NormalizedPermission {
|
|
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: User | null | undefined): boolean {
|
|
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: User | null | undefined, requirement: string, action = "READ"): boolean {
|
|
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
|