Symptôme (overlay fail-loud confirmé) : « OPS a reçu louis@targo.ca mais pas de permissions », alors que le compte EST superuser (is_superuser=True, admin+sysadmin, vérifié live) et que le hub renvoie 200 (6/6, ~0,14 s). La recherche (même base /hub, appelée plus tard) marche → seul le 1er appel /hub au démarrage (checkSession) échoue = course de boot / session pas encore « chaude ». Fix : loadPermissions réessaie jusqu'à 4× (backoff 0,7→2,1 s, cache:no-store) avant de conclure « pas d'accès ». Un échec transitoire ne verrouille plus l'app derrière un menu vide / overlay. (Les droits sont dans Authentik : groupes admin/sysadmin + is_superuser, inchangés — ce n'était ni le jeton ni les droits.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
112 lines
4.1 KiB
JavaScript
112 lines
4.1 KiB
JavaScript
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,
|
|
}
|
|
}
|