Files
RepoDructor/composables/useAuth.ts
josedario87 fda5b2497a
All checks were successful
build-and-deploy / build (push) Successful in 24s
build-and-deploy / deploy (push) Successful in 3s
fix: mejorar detección de pérdida de autenticación con Authentik
- Detectar errores "Failed to fetch" y "no response" como pérdida de auth
- Mejor logging para debugging de cambios de estado
- Manejar correctamente redirects 302 de Authentik en fetch
2025-10-12 03:06:08 -06:00

162 lines
5.8 KiB
TypeScript

import { ref, computed } from 'vue'
// Estado global compartido para auth
const authChecked = ref(true) // Iniciamos como chequeado
const isAuthenticated = ref(true) // Asumimos autenticado al inicio (si la página cargó, es porque pasamos Authentik)
const isCheckingAuth = ref(false)
const lastCheckTime = ref(0)
// Listener para cambios de autenticación desde otras tabs/ventanas
let visibilityChangeListener: (() => void) | null = null
let focusListener: (() => void) | null = null
export const useAuth = () => {
const checkAuth = async (force = false) => {
// Si ya sabemos que NO estamos autenticados, no hacer más requests
// (evita loops de CORS con Authentik redirects)
if (!isAuthenticated.value && !force) {
console.log('[Auth] Skipping check - already known to be unauthenticated')
return
}
// Evitar chequeos duplicados (cache de 30 segundos)
const now = Date.now()
if (!force && isCheckingAuth.value) return
if (!force && authChecked.value && now - lastCheckTime.value < 30000) {
return
}
isCheckingAuth.value = true
try {
// Intenta hacer fetch a un endpoint protegido
const response = await fetch('/api/music', {
method: 'HEAD', // Solo headers, no body
credentials: 'include', // Include cookies for auth
cache: 'no-store', // No cachear la respuesta
redirect: 'error', // No seguir redirects (error si Authentik intenta redirigir)
signal: AbortSignal.timeout(5000) // 5s timeout
})
const wasAuthenticated = isAuthenticated.value
isAuthenticated.value = response.ok
authChecked.value = true
lastCheckTime.value = now
// Log cambios de estado
if (wasAuthenticated !== isAuthenticated.value) {
console.log('[Auth] Status changed:', isAuthenticated.value ? 'authenticated' : 'unauthenticated')
}
} catch (error: any) {
// Con redirect: 'error', cualquier redirect (302) genera "Failed to fetch"
// Authentik redirige a login cuando no estás autenticado = 302
console.warn('[Auth] Check failed:', error.message)
const wasAuthenticated = isAuthenticated.value
// "Failed to fetch" con redirect: 'error' configurado = sesión expirada
// Authentik intenta redirigir (302) pero fetch lo rechaza y genera error
if (error.message && error.message.includes('Failed to fetch')) {
console.log('[Auth] Failed to fetch detected - marking as unauthenticated')
isAuthenticated.value = false
authChecked.value = true
if (wasAuthenticated) {
console.log('[Auth] Status changed: authenticated → unauthenticated (Authentik redirect detected)')
} else {
console.log('[Auth] Confirmed unauthenticated state')
}
} else if (error.name === 'TypeError' && error.message.includes('fetch')) {
// Otro tipo de TypeError fetch = también probablemente redirect
console.log('[Auth] TypeError fetch detected - marking as unauthenticated')
isAuthenticated.value = false
authChecked.value = true
if (wasAuthenticated) {
console.log('[Auth] Status changed: authenticated → unauthenticated (fetch error)')
} else {
console.log('[Auth] Confirmed unauthenticated state')
}
} else {
// Otro tipo de error (timeout, etc) - mantener estado actual
console.log('[Auth] Network error (not auth related), maintaining current state:', error.name)
}
lastCheckTime.value = now
} finally {
isCheckingAuth.value = false
}
}
const triggerAuth = () => {
console.log('[Auth] Triggering authentication flow - forcing page reload...')
// Simplemente recargamos la página completamente
// Si no estamos autenticados, Authentik interceptará y redirigirá al login
// Si ya estamos autenticados, la página se recarga normalmente
window.location.reload()
}
const markUnauthenticated = () => {
// Helper para marcar como no autenticado (útil cuando detectamos 401/403)
console.log('[Auth] Marking as unauthenticated')
isAuthenticated.value = false
authChecked.value = true
lastCheckTime.value = Date.now()
}
const setupVisibilityListener = () => {
// Re-chequea auth cuando la pestaña vuelve a ser visible
// (útil si el usuario se autentica en otra pestaña)
if (typeof document !== 'undefined' && !visibilityChangeListener) {
visibilityChangeListener = () => {
if (!document.hidden && isAuthenticated.value) {
// Solo chequear si creemos estar autenticados
// (evita spam de requests fallidas)
console.log('[Auth] Tab visible again, checking auth...')
checkAuth(false)
}
}
document.addEventListener('visibilitychange', visibilityChangeListener)
}
// También chequea cuando la ventana recibe foco
if (typeof window !== 'undefined' && !focusListener) {
focusListener = () => {
if (isAuthenticated.value) {
// Solo chequear si creemos estar autenticados
console.log('[Auth] Window focused, checking auth...')
checkAuth(false)
}
}
window.addEventListener('focus', focusListener)
}
}
const cleanupListeners = () => {
if (visibilityChangeListener) {
document.removeEventListener('visibilitychange', visibilityChangeListener)
visibilityChangeListener = null
}
if (focusListener) {
window.removeEventListener('focus', focusListener)
focusListener = null
}
}
const authStatus = computed(() => {
if (!authChecked.value) return 'unknown'
return isAuthenticated.value ? 'authenticated' : 'unauthenticated'
})
return {
isAuthenticated,
authChecked,
isCheckingAuth,
authStatus,
checkAuth,
triggerAuth,
markUnauthenticated,
setupVisibilityListener,
cleanupListeners
}
}