Files
RepoDructor/composables/useAuth.ts
josedario87 eedff715d4
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 24s
fix: implementar manejo correcto de modo offline
Problema crítico anterior:
Cuando se perdía conexión, el sistema marcaba como "no autenticado"
y DESREGISTRABA el Service Worker, destruyendo todo el contenido
cacheado y volviendo la PWA inútil offline.

Solución implementada:

1. useAuth.ts:
   - Agregar estado isOffline con listeners de navigator.onLine
   - Detectar offline ANTES de marcar como no autenticado
   - En checkAuth: si offline, mantener último estado conocido
   - En markUnauthenticated: NO ejecutar si estamos offline
   - Nuevo authStatus: 'offline' cuando sin conexión
   - NO desregistrar SW cuando estamos offline

2. AuthIndicator.client.vue:
   - Importar isOffline del composable
   - Agregar icono WifiOff para estado offline
   - Agregar textos: "Offline" / "Sin conexión. Puedes usar contenido guardado"
   - Estilos naranja para estado offline
   - En watchers: NO llamar markUnauthenticated si offline
   - handleClick: ignorar clicks en modo offline

Ahora offline funciona correctamente:
 Mantiene último estado de autenticación conocido
 NO desregistra Service Worker
 Contenido cacheado permanece accesible
 Indicador visual claro (naranja) de modo offline
 La PWA es totalmente funcional sin conexión
2025-10-17 04:21:31 -06:00

265 lines
9.1 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)
const isOffline = ref(false) // Estado de conexión
// Estado de información del usuario
const userInfo = ref(null)
// Listener para cambios de autenticación desde otras tabs/ventanas
let visibilityChangeListener: (() => void) | null = null
let focusListener: (() => void) | null = null
export const useAuth = () => {
// Detectar cambios en el estado de conexión
if (import.meta.client) {
const updateOnlineStatus = () => {
const wasOffline = isOffline.value
isOffline.value = !navigator.onLine
if (wasOffline && !isOffline.value) {
console.log('[Auth] Connection restored - back online')
} else if (!wasOffline && isOffline.value) {
console.log('[Auth] Connection lost - now offline')
}
}
// Inicializar estado
updateOnlineStatus()
// Escuchar eventos de conexión
window.addEventListener('online', updateOnlineStatus)
window.addEventListener('offline', updateOnlineStatus)
}
const checkAuth = async (force = false) => {
// Si estamos offline, no hacer requests
if (isOffline.value && !force) {
console.log('[Auth] Skipping check - offline')
return
}
// Si ya sabemos que NO estamos autenticados, no hacer más requests
// (evita loops innecesarios)
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 {
// Usar endpoint dedicado que lee headers de Authentik desde el servidor
// Este endpoint tiene headers no-cache y verifica la sesión en tiempo real
const response = await $fetch<{ authenticated: boolean; user: any | null; timestamp: string }>('/api/auth/status', {
// No usar redirect: 'error' aquí porque queremos la respuesta del servidor
// El endpoint responde con authenticated: false si no hay sesión
})
const wasAuthenticated = isAuthenticated.value
isAuthenticated.value = response.authenticated
authChecked.value = true
lastCheckTime.value = now
// Log cambios de estado
if (wasAuthenticated !== isAuthenticated.value) {
console.log('[Auth] Status changed:', isAuthenticated.value ? 'authenticated' : 'unauthenticated')
if (response.user) {
console.log('[Auth] User:', response.user.username)
}
}
} catch (error: any) {
console.warn('[Auth] Check failed:', error.message || error)
// Verificar si estamos offline AHORA (pudo cambiar durante el request)
if (!navigator.onLine) {
console.log('[Auth] Offline detected - maintaining current auth state')
isOffline.value = true
// NO cambiar isAuthenticated cuando estamos offline
// Mantener el último estado conocido
} else {
// Online pero el fetch falló = problema de autenticación
const wasAuthenticated = isAuthenticated.value
console.log('[Auth] Online but fetch failed - marking as unauthenticated')
isAuthenticated.value = false
authChecked.value = true
if (wasAuthenticated) {
console.log('[Auth] Status changed: authenticated → unauthenticated')
}
}
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 fetchUserInfo = async () => {
if (!isAuthenticated.value) {
console.log('[Auth] Cannot fetch user info - not authenticated')
return
}
try {
const response = await $fetch<{ authenticated: boolean; user: any | null }>('/api/auth/status')
if (response.authenticated && response.user) {
userInfo.value = response.user
console.log('[Auth] User info loaded:', response.user)
} else {
userInfo.value = null
}
} catch (error) {
console.warn('[Auth] Failed to fetch user info:', error)
userInfo.value = null
}
}
const logout = async () => {
console.log('[Auth] Logging out...')
// Marcar como no autenticado y desregistrar SW
await markUnauthenticated()
// Limpiar información del usuario
userInfo.value = null
// Navegar directamente al servidor de Authentik para logout
// Authentik tiene un flujo de invalidación por defecto que cierra la sesión
console.log('[Auth] Navigating to Authentik logout...')
window.location.href = 'https://authentik.nucleoriofrio.com/if/flow/default-invalidation-flow/'
}
const markUnauthenticated = async () => {
// Helper para marcar como no autenticado (útil cuando detectamos 401/403)
// IMPORTANTE: NO llamar esto si estamos offline, solo si hay un error real de auth
// Si estamos offline, NO marcar como no autenticado
if (isOffline.value || !navigator.onLine) {
console.log('[Auth] Skipping markUnauthenticated - offline mode')
return
}
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(() => {
// Si estamos offline, mostrar estado offline
if (isOffline.value) return 'offline'
if (!authChecked.value) return 'unknown'
return isAuthenticated.value ? 'authenticated' : 'unauthenticated'
})
return {
isAuthenticated,
authChecked,
isCheckingAuth,
isOffline,
authStatus,
userInfo,
checkAuth,
triggerAuth,
fetchUserInfo,
logout,
markUnauthenticated,
setupVisibilityListener,
cleanupListeners
}
}