'use server'; import { createAccessControl } from 'better-auth/plugins/access'; import { getAllRolesWithPermissions, getPermissionList } from './authorization/role'; // Caching layers to prevent repeated initialization & logging in dev (Strict Mode double invokes) // ----------------------------------------------------------------- // Statement cache (already implemented below) plus new AC + Roles caches let cachedAccessControl: ReturnType | null = null; let accessControlInitPromise: Promise> | null = null; let statementLoggedOnce = false; let cachedRolesMap: Record | null = null; let rolesInitPromise: Promise> | null = null; // Define TypeScript interfaces for better type safety interface Permission { page: string; access: string | string[]; } interface Role { name: string; permissions: Permission[] | null; } // Remove top-level await to avoid side-effects during module evaluation in React render paths. // Lazy-load & cache permission statement instead. type Statement = Awaited>; let cachedStatement: Statement | null = null; let loadingPromise: Promise | null = null; async function loadStatement(): Promise { if (cachedStatement) return cachedStatement; if (!loadingPromise) { loadingPromise = getPermissionList().then((res) => { cachedStatement = res; return res; }).finally(() => { loadingPromise = null; }); } return loadingPromise; } export async function getAccessControl() { // Fast-path return if already initialized if (cachedAccessControl) return cachedAccessControl; if (accessControlInitPromise) return accessControlInitPromise; accessControlInitPromise = (async () => { const statement = await loadStatement(); // if (process.env.NODE_ENV !== 'production' && !statementLoggedOnce) { // console.log('Access Control Statement (logged once):', statement); // statementLoggedOnce = true; // ensure single log in dev despite re-renders / hot reload // } const ac = createAccessControl(statement); cachedAccessControl = ac; return ac; })(); try { return await accessControlInitPromise; } finally { accessControlInitPromise = null; // allow future force re-init if ever needed } } // Initialize roles function to be called when needed export async function initializeRoles(forceReload = false) { if (!forceReload && cachedRolesMap) return cachedRolesMap; if (!forceReload && rolesInitPromise) return rolesInitPromise; rolesInitPromise = (async () => { const roleList = (await getAllRolesWithPermissions()) as Role[]; const ac = await getAccessControl(); const rolesMap: Record = {}; for (const role of roleList) { const statements: Record = {}; if (role.permissions && Array.isArray(role.permissions)) { for (const perm of role.permissions) { const resource = perm.page; const accessList = Array.isArray(perm.access) ? perm.access : [perm.access]; if (!statements[resource]) statements[resource] = []; for (const a of accessList) { if (!statements[resource].includes(a)) statements[resource].push(a); } } } rolesMap[role.name] = ac.newRole(statements); } // if (process.env.NODE_ENV !== 'production') { // // Only log once per initialization cycle // if (forceReload || !cachedRolesMap) { // console.dir(rolesMap, { depth: null }); // } // } cachedRolesMap = rolesMap; return rolesMap; })(); try { return await rolesInitPromise; } finally { rolesInitPromise = null; // clear to allow future reload if forceReload passed } } // New: return plain role -> statements mapping (serializable for client) export async function getRoleStatements() { const roleList = await getAllRolesWithPermissions(); const rolesStatements: Record> = {}; for (const role of roleList as any[]) { const statements: Record = {}; if (role.permissions && Array.isArray(role.permissions)) { for (const perm of role.permissions) { const resource = perm.page; const accessList = Array.isArray(perm.access) ? perm.access : [perm.access]; if (!statements[resource]) statements[resource] = []; for (const a of accessList) { if (!statements[resource].includes(a)) statements[resource].push(a); } } } rolesStatements[role.name] = statements; } return rolesStatements; } // Helper (optional) to force a refresh (e.g., after permission model change) export async function refreshAccessControlAndRoles() { cachedStatement = null; cachedAccessControl = null; cachedRolesMap = null; await getAccessControl(); await initializeRoles(true); }