import type { H3Event } from 'h3' // Cache for org-role access mapping let orgRoleAccessCache: Map> | null = null let cacheTimestamp: number = 0 const CACHE_DURATION = 5 * 60 * 1000 // 5 minutes interface OrgRecord { id: number AD_Role_OrgAccess?: Array<{ AD_Role_ID?: { id: number } IsActive?: boolean }> } /** * Fetches and caches organization-role access mapping from iDempiere * Returns a Map where: * - Key: Organization ID * - Value: Set of Role IDs that have access to this organization * * Special case: Organization ID 0 (Name: "*") means "All Organizations" * * Loaded with the SERVICE token (config.api.idempieretoken) when configured, the * caller's token only as a fallback. A limited merchant role sees just its own org * (prod logs: "Loaded 2 organizations" vs 17 for staff) — built from that token the * map lacked org 0 (the staff '*' roles) and every other merchant org, and was then * served from this cache to staff for 5 minutes: merchants got no toast on a * hand-over, staff none on a merchant reply. The map is tenant-wide reference data, * not something to scope per caller. */ export async function getOrgRoleAccessMap(event: H3Event, authToken?: string | null, skipTokenRefresh?: boolean): Promise>> { const now = Date.now() // Return cached data if still valid if (orgRoleAccessCache && (now - cacheTimestamp) < CACHE_DURATION) { return orgRoleAccessCache } const fetchOrgAccess = async (token: string) => { const res: any = await event.context.fetch( 'models/ad_org?$expand=AD_Role_OrgAccess', 'GET', token, null ) if (!res?.records) { console.error('[OrgRoleAccessCache] Failed to fetch org-role access data') return null } // Build the mapping const accessMap = new Map>() for (const org of res.records as OrgRecord[]) { const orgId = org.id const roleSet = new Set() if (org.AD_Role_OrgAccess && Array.isArray(org.AD_Role_OrgAccess)) { for (const access of org.AD_Role_OrgAccess) { if (access.IsActive !== false && access.AD_Role_ID?.id) { roleSet.add(access.AD_Role_ID.id) } } } accessMap.set(orgId, roleSet) } return accessMap } try { const serviceToken: string = (useRuntimeConfig() as any).api?.idempieretoken || '' let token = serviceToken || authToken || await getTokenHelper(event) let accessMap: Map> | null = null try { accessMap = await fetchOrgAccess(token) } catch (fetchError: any) { // On 401, try refreshing the token and retry once // Skip refresh if called from SSE context (headers already sent) if (!skipTokenRefresh && (fetchError?.statusCode === 401 || fetchError?.status === 401)) { try { const refreshedToken = await refreshTokenHelper(event) accessMap = await fetchOrgAccess(refreshedToken) } catch (refreshError: any) { console.error('[OrgRoleAccessCache] Token refresh also failed:', refreshError.message) } } else if (fetchError?.statusCode === 401 || fetchError?.status === 401) { console.error('[OrgRoleAccessCache] Token expired (refresh skipped for SSE context)') } else { throw fetchError } } if (!accessMap) { return orgRoleAccessCache || new Map() } // Cache the result orgRoleAccessCache = accessMap cacheTimestamp = now console.log(`[OrgRoleAccessCache] Loaded ${accessMap.size} organizations with role mappings (${serviceToken ? 'service' : 'caller'} token)`) return accessMap } catch (error: any) { console.error('[OrgRoleAccessCache] Error fetching org-role access:', error.message) // Return existing cache or empty map on error return orgRoleAccessCache || new Map() } } /** * Checks if a given role has access to a specific organization * Handles the special case where org ID 0 (Name: "*") means "All Organizations" */ export function hasRoleAccessToOrg( orgRoleAccessMap: Map>, roleId: number, orgId: number ): boolean { // Check if role has access to org ID 0 ("*" - All Organizations) const allOrgsRoles = orgRoleAccessMap.get(0) if (allOrgsRoles && allOrgsRoles.has(roleId)) { return true // Role has access to all organizations } // Check if role has access to the specific organization const orgRoles = orgRoleAccessMap.get(orgId) return orgRoles ? orgRoles.has(roleId) : false } /** * Invalidates the cache, forcing a refresh on next access */ export function invalidateOrgRoleAccessCache() { orgRoleAccessCache = null cacheTimestamp = 0 console.log('[OrgRoleAccessCache] Cache invalidated') }