Files
RepoDructor/components/AuthIndicator.client.vue
josedario87 cda4722a27
All checks were successful
build-and-deploy / build (push) Successful in 23s
build-and-deploy / deploy (push) Successful in 3s
fix: desregistrar Service Worker al perder autenticación
- 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
2025-10-12 03:12:03 -06:00

320 lines
7.4 KiB
Vue

<template>
<button
class="auth-indicator"
:class="[authStatus, { checking: isCheckingAuth }]"
@click="handleClick"
:title="tooltipText"
:disabled="isCheckingAuth"
>
<component
:is="iconComponent"
:size="20"
class="auth-icon"
/>
<span class="auth-status-text">{{ statusText }}</span>
</button>
</template>
<script setup>
import { computed, onMounted, onUnmounted, watch } from 'vue'
import { UserCheck, UserX, Loader, ShieldAlert } from 'lucide-vue-next'
import { useAuth } from '~/composables/useAuth'
import { useMusicStore } from '~/stores/music'
const {
isAuthenticated,
authChecked,
isCheckingAuth,
authStatus,
checkAuth,
triggerAuth,
markUnauthenticated,
setupVisibilityListener,
cleanupListeners
} = useAuth()
const musicStore = useMusicStore()
// Check auth on mount and setup listeners
onMounted(async () => {
// Asumimos autenticado inicialmente (si la página cargó, pasamos Authentik)
// NO hacemos checkAuth inicial para evitar conflictos con Service Worker y Authentik
console.log('[AuthIndicator] Mounted - assuming authenticated (page loaded successfully)')
setupVisibilityListener()
// POLLING DESHABILITADO: Causa conflictos con Authentik cuando no estamos autenticados
// En su lugar, confiamos en la detección reactiva de errores del musicStore
console.log('[AuthIndicator] Polling disabled - relying on reactive error detection')
/* CÓDIGO DESHABILITADO
const interval = setInterval(() => {
if (!document.hidden && isAuthenticated.value) {
checkAuth(false)
}
}, 60000)
onUnmounted(() => {
clearInterval(interval)
})
*/
})
// Cleanup listeners on unmount
onUnmounted(() => {
cleanupListeners()
})
// Watch for failed API requests that might indicate auth loss
watch(() => musicStore.error, (error, oldError) => {
// Solo actuar si es un nuevo error (no el mismo)
if (error && error !== oldError) {
console.log('[AuthIndicator] Music store error detected:', error)
// Detectar errores de autenticación (múltiples formas)
const isAuthError =
error.includes('401') ||
error.includes('403') ||
error.includes('Unauthorized') ||
error.includes('Forbidden') ||
error.includes('Failed to fetch') || // Authentik redirect genera esto
error.includes('no response') // Nuxt $fetch sin respuesta
if (isAuthError) {
// Auth error detected - mark as unauthenticated immediately
console.log('[AuthIndicator] Auth error detected, updating status')
markUnauthenticated()
}
}
}, { immediate: false })
// Watch loading state - cuando termina de cargar, verificar si hubo errores de auth
watch(() => musicStore.loading, (loading, wasLoading) => {
if (wasLoading && !loading && musicStore.error) {
// Terminó de cargar con error, verificar si es de auth
const error = musicStore.error
const isAuthError =
error.includes('401') ||
error.includes('403') ||
error.includes('Unauthorized') ||
error.includes('Forbidden') ||
error.includes('Failed to fetch') ||
error.includes('no response')
if (isAuthError) {
markUnauthenticated()
}
}
})
const iconComponent = computed(() => {
if (isCheckingAuth.value) return Loader
if (!authChecked.value) return Loader
switch (authStatus.value) {
case 'authenticated':
return UserCheck
case 'unauthenticated':
return UserX
default:
return ShieldAlert
}
})
const statusText = computed(() => {
if (isCheckingAuth.value) return 'Verificando...'
switch (authStatus.value) {
case 'authenticated':
return 'Conectado'
case 'unauthenticated':
return 'Reautenticar'
default:
return 'Verificando...'
}
})
const tooltipText = computed(() => {
if (isCheckingAuth.value) return 'Verificando autenticación...'
switch (authStatus.value) {
case 'authenticated':
return 'Estás autenticado. Click para verificar estado.'
case 'unauthenticated':
return 'Sesión expirada. Click para iniciar sesión de nuevo.'
default:
return 'Verificando estado de autenticación...'
}
})
const handleClick = () => {
if (isCheckingAuth.value) return
if (authStatus.value === 'unauthenticated') {
// Forzar reload de la página para que Authentik intercepte
triggerAuth()
} else if (authStatus.value === 'authenticated') {
// Re-check auth status (forzar, ignorar cache)
checkAuth(true)
}
}
</script>
<style scoped>
.auth-indicator {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 14px;
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
color: var(--text-secondary);
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
white-space: nowrap;
position: relative;
overflow: hidden;
}
.auth-indicator::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 2px;
background: var(--status-color);
opacity: 0;
transition: opacity 0.3s ease;
}
.auth-indicator:hover::before {
opacity: 1;
}
.auth-indicator:not(:disabled):hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.2);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.auth-indicator:disabled {
cursor: not-allowed;
opacity: 0.6;
}
.auth-icon {
flex-shrink: 0;
color: var(--status-color);
transition: all 0.3s ease;
}
.auth-indicator.checking .auth-icon {
animation: spin 1s linear infinite;
}
.auth-status-text {
color: var(--text-primary);
font-weight: 500;
letter-spacing: 0.3px;
}
/* Status colors */
.auth-indicator.authenticated {
--status-color: #10b981;
border-color: rgba(16, 185, 129, 0.3);
}
.auth-indicator.authenticated:hover {
border-color: rgba(16, 185, 129, 0.5);
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.2);
}
.auth-indicator.unauthenticated {
--status-color: #ef4444;
border-color: rgba(239, 68, 68, 0.3);
}
.auth-indicator.unauthenticated:hover {
border-color: rgba(239, 68, 68, 0.5);
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.2);
}
.auth-indicator.unknown {
--status-color: #f59e0b;
border-color: rgba(245, 158, 11, 0.3);
}
.auth-indicator.checking {
--status-color: #3b82f6;
}
/* Animation for loading state */
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* Responsive design */
@media (max-width: 768px) {
.auth-status-text {
display: none;
}
.auth-indicator {
padding: 8px;
min-width: 40px;
justify-content: center;
}
}
/* Pulse effect for unauthenticated state */
.auth-indicator.unauthenticated {
animation: pulse-red 2s ease-in-out infinite;
}
@keyframes pulse-red {
0%, 100% {
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4);
}
50% {
box-shadow: 0 0 0 6px rgba(239, 68, 68, 0);
}
}
/* Accessibility */
@media (prefers-reduced-motion: reduce) {
.auth-indicator {
transition: none;
animation: none;
}
.auth-indicator.checking .auth-icon {
animation: none;
}
}
/* High contrast mode */
@media (prefers-contrast: high) {
.auth-indicator {
border-width: 2px;
}
.auth-indicator.authenticated {
background: rgba(16, 185, 129, 0.2);
}
.auth-indicator.unauthenticated {
background: rgba(239, 68, 68, 0.2);
}
}
</style>