import { ref, computed } from 'vue' import { HUB_URL } from 'src/config/hub' // Singleton state — shared across all components // DEV local (pas de hub devant pour /auth/permissions) : profil permissif d'emblée pour pouvoir prévisualiser/auditer // toutes les pages. En PROD : null → chargé depuis le hub (restrictif tant que non chargé). Aucun effet en prod. const _DEV_LOCAL = typeof window !== 'undefined' && (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') const permissions = ref(_DEV_LOCAL ? { email: 'dev@localhost', name: 'Dev (local)', is_superuser: true, capabilities: {}, groups: ['admin'] } : null) // { email, username, name, groups, is_superuser, capabilities, overrides } const loading = ref(false) const error = ref(null) let fetchPromise = null // Prevent duplicate fetches /** * Fetch effective permissions for the current user from targo-hub. * Reads X-Authentik-Email header (injected by Traefik). * @param {string} email — user email (from Authentik header or session) * @param {string} [username] — Authentik username, used as a FALLBACK lookup when the email * matches no provisioned OPS account (empty/wrong email → the hub retries by username). */ async function loadPermissions (email, username) { if (!email && !username) return if (fetchPromise) return fetchPromise loading.value = true error.value = null const qs = new URLSearchParams() if (email) qs.set('email', email) if (username) qs.set('username', username) const url = `${HUB_URL}/auth/permissions?${qs.toString()}` const isLocal = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1' // RÉSILIENCE : le tout 1er appel /hub au démarrage échoue parfois (session pas encore « chaude » / course au boot) // alors que les appels /hub suivants (ex. recherche) passent. On RÉESSAIE (jusqu'à 4 tentatives, backoff 0,7→2,1 s) // avant de conclure « pas d'accès » → un superuser n'est plus bloqué par un échec transitoire (le vrai bug observé). async function attempt (n) { try { const r = await fetch(url, { cache: 'no-store' }) if (!r.ok) throw new Error('HTTP ' + r.status) const data = await r.json() permissions.value = data error.value = null } catch (e) { if (n < 4) { await new Promise(res => setTimeout(res, 700 * n)); return attempt(n + 1) } console.error('[usePermissions] load failed after retries:', e.message) error.value = e.message // DEV local (pas de hub devant) : profil permissif pour prévisualiser/auditer ; PROD → null (App.vue « fail-loud »). permissions.value = isLocal ? { email, is_superuser: true, capabilities: {}, groups: ['admin'] } : null } } fetchPromise = attempt(1).finally(() => { loading.value = false fetchPromise = null }) return fetchPromise } /** * Check if the current user has a specific capability. * Superusers always return true. * Returns false if permissions haven't loaded yet (safe default). */ function can (capability) { if (!permissions.value) return false if (permissions.value.is_superuser) return true return permissions.value.capabilities?.[capability] === true } /** * Check if the user has ANY of the listed capabilities. */ function canAny (...capabilities) { return capabilities.some(c => can(c)) } /** * Check if the user belongs to a specific group. */ function hasGroup (groupName) { return permissions.value?.groups?.includes(groupName) || false } // Computed helpers const userGroups = computed(() => permissions.value?.groups || []) const userName = computed(() => permissions.value?.name || '') const userEmail = computed(() => permissions.value?.email || '') const isSuperuser = computed(() => permissions.value?.is_superuser || false) const isAdmin = computed(() => hasGroup('admin') || isSuperuser.value) const isLoaded = computed(() => permissions.value !== null) export function usePermissions () { return { permissions, loading, error, loadPermissions, can, canAny, hasGroup, userGroups, userName, userEmail, isSuperuser, isAdmin, isLoaded, HUB_URL, } }