- Desregistrar SW automáticamente cuando se detecta pérdida de auth - Desregistrar SW antes de triggerAuth para evitar conflictos con Authentik - Deshabilitar polling y listeners de visibility (causan errores de CORS) - Confiar en detección reactiva de errores del musicStore - Usar window.location.href en lugar de reload() para forzar navegación
196 lines
7.3 KiB
TypeScript
196 lines
7.3 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 = async () => {
|
|
console.log('[Auth] Triggering authentication flow...')
|
|
|
|
// Desregistrar el Service Worker temporalmente para evitar conflictos con Authentik
|
|
if ('serviceWorker' in navigator) {
|
|
try {
|
|
const registrations = await navigator.serviceWorker.getRegistrations()
|
|
console.log('[Auth] Unregistering Service Workers before auth...')
|
|
await Promise.all(registrations.map(reg => reg.unregister()))
|
|
} catch (error) {
|
|
console.warn('[Auth] Could not unregister SW:', error)
|
|
}
|
|
}
|
|
|
|
// Forzar navegación completa a la raíz
|
|
// Authentik interceptará y redirigirá al login si no estamos autenticados
|
|
console.log('[Auth] Forcing hard navigation to trigger Authentik...')
|
|
window.location.href = window.location.origin + '/'
|
|
}
|
|
|
|
const markUnauthenticated = async () => {
|
|
// 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()
|
|
|
|
// Desregistrar Service Worker para evitar conflictos con Authentik
|
|
if ('serviceWorker' in navigator) {
|
|
try {
|
|
const registrations = await navigator.serviceWorker.getRegistrations()
|
|
if (registrations.length > 0) {
|
|
console.log('[Auth] Unregistering Service Worker due to auth loss...')
|
|
await Promise.all(registrations.map(reg => reg.unregister()))
|
|
console.log('[Auth] Service Worker unregistered - page will need reload for re-auth')
|
|
}
|
|
} catch (error) {
|
|
console.warn('[Auth] Could not unregister SW:', error)
|
|
}
|
|
}
|
|
}
|
|
|
|
const setupVisibilityListener = () => {
|
|
// DESHABILITADO: Los listeners causan problemas con Authentik
|
|
// Cuando no estamos autenticados, los listeners intentan hacer fetch
|
|
// y Authentik redirige, causando errores de CORS
|
|
// En su lugar, confiamos en que el usuario haga click en "Reautenticar"
|
|
console.log('[Auth] Visibility listeners disabled to avoid conflicts with Authentik')
|
|
return
|
|
|
|
/* CÓDIGO DESHABILITADO
|
|
// 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
|
|
}
|
|
}
|