fix: implementar manejo correcto de modo offline
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 24s

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
This commit is contained in:
2025-10-17 04:21:31 -06:00
parent 40945dc634
commit eedff715d4
2 changed files with 89 additions and 11 deletions

View File

@@ -60,7 +60,7 @@
<script setup> <script setup>
import { ref, computed, onMounted, onUnmounted, watch } from 'vue' import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import { UserCheck, UserX, Loader, ShieldAlert, ChevronDown, RefreshCw, LogOut } from 'lucide-vue-next' import { UserCheck, UserX, Loader, ShieldAlert, ChevronDown, RefreshCw, LogOut, WifiOff } from 'lucide-vue-next'
import { useAuth } from '~/composables/useAuth' import { useAuth } from '~/composables/useAuth'
import { useMusicStore } from '~/stores/music' import { useMusicStore } from '~/stores/music'
@@ -68,6 +68,7 @@ const {
isAuthenticated, isAuthenticated,
authChecked, authChecked,
isCheckingAuth, isCheckingAuth,
isOffline,
authStatus, authStatus,
userInfo, userInfo,
checkAuth, checkAuth,
@@ -124,6 +125,12 @@ watch(() => musicStore.error, (error, oldError) => {
if (error && error !== oldError) { if (error && error !== oldError) {
console.log('[AuthIndicator] Music store error detected:', error) console.log('[AuthIndicator] Music store error detected:', error)
// Si estamos offline, NO marcar como no autenticado
if (isOffline.value) {
console.log('[AuthIndicator] Ignoring error - offline mode')
return
}
// Detectar errores de autenticación (múltiples formas) // Detectar errores de autenticación (múltiples formas)
const isAuthError = const isAuthError =
error.includes('401') || error.includes('401') ||
@@ -144,6 +151,12 @@ watch(() => musicStore.error, (error, oldError) => {
// Watch loading state - cuando termina de cargar, verificar si hubo errores de auth // Watch loading state - cuando termina de cargar, verificar si hubo errores de auth
watch(() => musicStore.loading, (loading, wasLoading) => { watch(() => musicStore.loading, (loading, wasLoading) => {
if (wasLoading && !loading && musicStore.error) { if (wasLoading && !loading && musicStore.error) {
// Si estamos offline, NO marcar como no autenticado
if (isOffline.value) {
console.log('[AuthIndicator] Ignoring loading error - offline mode')
return
}
// Terminó de cargar con error, verificar si es de auth // Terminó de cargar con error, verificar si es de auth
const error = musicStore.error const error = musicStore.error
const isAuthError = const isAuthError =
@@ -169,6 +182,8 @@ const iconComponent = computed(() => {
return UserCheck return UserCheck
case 'unauthenticated': case 'unauthenticated':
return UserX return UserX
case 'offline':
return WifiOff
default: default:
return ShieldAlert return ShieldAlert
} }
@@ -182,6 +197,8 @@ const statusText = computed(() => {
return 'Conectado' return 'Conectado'
case 'unauthenticated': case 'unauthenticated':
return 'Reautenticar' return 'Reautenticar'
case 'offline':
return 'Offline'
default: default:
return 'Verificando...' return 'Verificando...'
} }
@@ -195,6 +212,8 @@ const tooltipText = computed(() => {
return 'Estás autenticado. Click para verificar estado.' return 'Estás autenticado. Click para verificar estado.'
case 'unauthenticated': case 'unauthenticated':
return 'Sesión expirada. Click para iniciar sesión de nuevo.' return 'Sesión expirada. Click para iniciar sesión de nuevo.'
case 'offline':
return 'Sin conexión. Puedes usar contenido guardado offline.'
default: default:
return 'Verificando estado de autenticación...' return 'Verificando estado de autenticación...'
} }
@@ -228,7 +247,11 @@ const closeDropdown = () => {
const handleClick = () => { const handleClick = () => {
if (isCheckingAuth.value) return if (isCheckingAuth.value) return
if (authStatus.value === 'unauthenticated') { if (authStatus.value === 'offline') {
// En modo offline, no hacer nada (ya no se puede autenticar)
console.log('[AuthIndicator] Click ignored - offline mode')
return
} else if (authStatus.value === 'unauthenticated') {
// Forzar reload de la página para que Authentik intercepte // Forzar reload de la página para que Authentik intercepte
triggerAuth() triggerAuth()
} else if (authStatus.value === 'authenticated') { } else if (authStatus.value === 'authenticated') {
@@ -367,6 +390,16 @@ onUnmounted(() => {
--status-color: #3b82f6; --status-color: #3b82f6;
} }
.auth-indicator.offline {
--status-color: #f59e0b;
border-color: rgba(245, 158, 11, 0.3);
}
.auth-indicator.offline:hover {
border-color: rgba(245, 158, 11, 0.5);
box-shadow: 0 4px 12px rgba(245, 158, 11, 0.2);
}
/* Animation for loading state */ /* Animation for loading state */
@keyframes spin { @keyframes spin {
from { from {

View File

@@ -5,6 +5,7 @@ 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 isAuthenticated = ref(true) // Asumimos autenticado al inicio (si la página cargó, es porque pasamos Authentik)
const isCheckingAuth = ref(false) const isCheckingAuth = ref(false)
const lastCheckTime = ref(0) const lastCheckTime = ref(0)
const isOffline = ref(false) // Estado de conexión
// Estado de información del usuario // Estado de información del usuario
const userInfo = ref(null) const userInfo = ref(null)
@@ -14,7 +15,34 @@ let visibilityChangeListener: (() => void) | null = null
let focusListener: (() => void) | null = null let focusListener: (() => void) | null = null
export const useAuth = () => { 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) => { 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 // Si ya sabemos que NO estamos autenticados, no hacer más requests
// (evita loops innecesarios) // (evita loops innecesarios)
if (!isAuthenticated.value && !force) { if (!isAuthenticated.value && !force) {
@@ -54,16 +82,22 @@ export const useAuth = () => {
} catch (error: any) { } catch (error: any) {
console.warn('[Auth] Check failed:', error.message || error) console.warn('[Auth] Check failed:', error.message || error)
const wasAuthenticated = isAuthenticated.value // 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
// Si el fetch falla completamente (CORS, redirect, etc), asumir no autenticado if (wasAuthenticated) {
// Esto pasa cuando Authentik redirige antes de que el endpoint pueda responder console.log('[Auth] Status changed: authenticated → unauthenticated')
console.log('[Auth] Fetch failed - marking as unauthenticated') }
isAuthenticated.value = false
authChecked.value = true
if (wasAuthenticated) {
console.log('[Auth] Status changed: authenticated → unauthenticated')
} }
lastCheckTime.value = now lastCheckTime.value = now
@@ -129,6 +163,14 @@ export const useAuth = () => {
const markUnauthenticated = async () => { const markUnauthenticated = async () => {
// Helper para marcar como no autenticado (útil cuando detectamos 401/403) // 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') console.log('[Auth] Marking as unauthenticated')
isAuthenticated.value = false isAuthenticated.value = false
authChecked.value = true authChecked.value = true
@@ -198,6 +240,8 @@ export const useAuth = () => {
} }
const authStatus = computed(() => { const authStatus = computed(() => {
// Si estamos offline, mostrar estado offline
if (isOffline.value) return 'offline'
if (!authChecked.value) return 'unknown' if (!authChecked.value) return 'unknown'
return isAuthenticated.value ? 'authenticated' : 'unauthenticated' return isAuthenticated.value ? 'authenticated' : 'unauthenticated'
}) })
@@ -206,6 +250,7 @@ export const useAuth = () => {
isAuthenticated, isAuthenticated,
authChecked, authChecked,
isCheckingAuth, isCheckingAuth,
isOffline,
authStatus, authStatus,
userInfo, userInfo,
checkAuth, checkAuth,