/** * Composable para leer información de usuario de Authentik * Los headers son inyectados por Authentik Proxy Outpost * * Documentación de headers disponibles: * - x-authentik-username: Username del usuario * - x-authentik-email: Email del usuario * - x-authentik-name: Nombre completo del usuario * - x-authentik-uid: UID único del usuario * - x-authentik-groups: Grupos separados por | * - x-authentik-meta-app: Slug de la aplicación en Authentik * - x-authentik-meta-outpost: Nombre del outpost * - Nota: Los roles RBAC son internos de Authentik y no se exponen via headers */ interface AuthentikUser { username: string email: string | undefined name: string | undefined groups: string[] uid: string | undefined avatar: string // Metadata de la aplicación y outpost appSlug?: string outpostName?: string } interface AuthStatusResponse { authenticated: boolean user?: { username: string name?: string } } export const useAuthentik = () => { // Leer headers en el servidor y almacenarlos en state const authentikUser = useState('authentikUser', () => { // Solo en el servidor, leer los headers if (import.meta.server) { const headers = useRequestHeaders() const username = headers['x-authentik-username'] const email = headers['x-authentik-email'] const name = headers['x-authentik-name'] const groups = headers['x-authentik-groups'] const uid = headers['x-authentik-uid'] const appSlug = headers['x-authentik-meta-app'] const outpostName = headers['x-authentik-meta-outpost'] // Si no hay username, el usuario no está autenticado if (!username) { return null } return { username, email, name, groups: groups ? groups.split('|').filter(g => g.trim()) : [], uid, appSlug, outpostName, // Generar avatar URL usando UI Avatars avatar: `https://ui-avatars.com/api/?name=${encodeURIComponent(name || username)}&background=random&size=128` } } return null }) const user = computed(() => authentikUser.value) const isAuthenticated = computed(() => !!user.value) const logout = () => { // Logout completo: invalida la sesión de Authentik completamente // Esto cierra sesión en todas las aplicaciones const authentikUrl = useRuntimeConfig().public.authentikUrl || 'https://authentik.nucleoriofrio.com' navigateTo(`${authentikUrl}/flows/-/default/invalidation/`, { external: true }) } const goToProfile = () => { // URL de perfil de Authentik const authentikUrl = useRuntimeConfig().public.authentikUrl || 'https://authentik.nucleoriofrio.com' navigateTo(`${authentikUrl}/if/user/`, { external: true, open: { target: '_blank' } }) } const checkSessionStatus = async () => { const toast = useToast() // Verificar si está offline primero if (!navigator.onLine) { toast.add({ title: 'Modo Offline', description: 'No se puede validar sesión sin conexión', color: 'neutral', icon: 'i-heroicons-wifi' }) return } // Mostrar toast de "verificando..." toast.add({ title: 'Verificando sesión...', description: 'Consultando estado en Authentik', color: 'info', icon: 'i-heroicons-arrow-path' }) try { // Consultar el endpoint de API que verifica contra Authentik const response = await $fetch('/api/auth/status') if (response.authenticated && response.user) { // Sesión activa en Authentik toast.add({ title: 'Sesión Activa', description: `Conectado como: ${response.user.name || response.user.username}`, color: 'success', icon: 'i-heroicons-check-circle' }) } else { // Sin sesión en Authentik toast.add({ title: 'Sin Sesión', description: 'No hay sesión activa en Authentik', color: 'warning', icon: 'i-heroicons-exclamation-triangle', actions: [{ label: 'Iniciar Sesión', onClick: () => { // Recargar la página forzará a Authentik a redirigir al login window.location.reload() } }] }) } } catch (error: unknown) { // Verificar si está offline ahora (pudo desconectarse durante la petición) if (!navigator.onLine) { toast.add({ title: 'Modo Offline', description: 'No se puede validar sesión sin conexión', color: 'neutral', icon: 'i-heroicons-wifi' }) return } // Si el error es por redirect de Authentik (CORS/fetch error), significa que no hay sesión // Authentik redirige a login cuando no hay sesión válida, causando error CORS en fetch const errorMessage = (error as Error)?.message || String(error) const isCorsOrRedirectError = errorMessage.includes('Failed to fetch') || errorMessage.includes('CORS') || (error as any)?.statusCode === 302 if (isCorsOrRedirectError) { // Interpretar como sesión expirada/inválida toast.add({ title: 'Sin Sesión', description: 'No hay sesión activa en Authentik', color: 'warning', icon: 'i-heroicons-exclamation-triangle', actions: [{ label: 'Iniciar Sesión', onClick: () => { // Recargar la página forzará a Authentik a redirigir al login window.location.reload() } }] }) } else { // Error real de red o servidor toast.add({ title: 'Error', description: 'No se pudo verificar el estado de la sesión', color: 'error', icon: 'i-heroicons-x-circle' }) } console.error('Error checking session status:', error) } } /** * Verifica si el usuario pertenece a un grupo específico (frontend) * Lee los grupos desde el estado local (headers de Authentik) */ const hasGroup = (groupName: string): boolean => { if (!user.value) return false return user.value.groups.includes(groupName) } /** * Verifica si el usuario pertenece a un grupo específico (backend) * Consulta al servidor para validar contra Authentik */ const checkGroupBackend = async (groupName: string): Promise => { try { const response = await $fetch<{ hasGroup: boolean }>(`/api/auth/check-group`, { method: 'POST', body: { groupName } }) return response.hasGroup } catch (error) { console.error('Error checking group membership:', error) return false } } return { user, isAuthenticated, logout, goToProfile, checkSessionStatus, hasGroup, checkGroupBackend } }