indicador de conexion y logging
All checks were successful
build-and-deploy / build (push) Successful in 23s
build-and-deploy / deploy (push) Successful in 3s

This commit is contained in:
2025-10-11 19:36:19 -06:00
parent c56f06034e
commit 1f087eb6f3
4 changed files with 345 additions and 2 deletions

View File

@@ -0,0 +1,251 @@
<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, 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 } = useAuth()
const musicStore = useMusicStore()
// Check auth on mount
onMounted(async () => {
await checkAuth()
})
// Watch for failed API requests that might indicate auth loss
watch(() => musicStore.error, (error) => {
if (error && (error.includes('401') || error.includes('403') || error.includes('Unauthorized'))) {
// Auth might have expired, recheck
checkAuth()
}
})
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 'Sin conexión'
default:
return 'Desconocido'
}
})
const tooltipText = computed(() => {
if (isCheckingAuth.value) return 'Verificando autenticación...'
switch (authStatus.value) {
case 'authenticated':
return 'Estás autenticado. Puedes descargar música.'
case 'unauthenticated':
return 'No autenticado. Click para iniciar sesión.'
default:
return 'Estado de autenticación desconocido'
}
})
const handleClick = () => {
if (isCheckingAuth.value) return
if (authStatus.value === 'unauthenticated') {
// Redirect to trigger Authentik
triggerAuth()
} else if (authStatus.value === 'authenticated') {
// Re-check auth status
checkAuth()
}
}
</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>

View File

@@ -12,6 +12,7 @@
</div>
<div class="header-controls">
<AuthIndicator />
<PlaybackControls
@shuffle-changed="$emit('shuffle-changed', $event)"
@repeat-changed="$emit('repeat-changed', $event)"
@@ -34,6 +35,7 @@
<script setup>
import { computed } from 'vue'
import { Music } from 'lucide-vue-next'
import AuthIndicator from './AuthIndicator.client.vue'
import ThemeToggle from './ThemeToggle.client.vue'
import PlaybackControls from './PlaybackControls.client.vue'

81
composables/useAuth.ts Normal file
View File

@@ -0,0 +1,81 @@
import { ref, computed } from 'vue'
// Estado global compartido para auth
const authChecked = ref(false)
const isAuthenticated = ref(false)
const isCheckingAuth = ref(false)
const lastCheckTime = ref(0)
export const useAuth = () => {
const checkAuth = async (force = false) => {
// 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
signal: AbortSignal.timeout(5000) // 5s timeout
})
// Si responde 200, estamos autenticados
// Si responde 401/403, no estamos autenticados
isAuthenticated.value = response.ok
authChecked.value = true
lastCheckTime.value = now
console.log('[Auth] Status:', isAuthenticated.value ? 'authenticated' : 'unauthenticated')
} catch (error) {
console.warn('[Auth] Failed to check authentication status:', error)
// En caso de error de red, mantenemos el último estado conocido si existe
// Solo marcamos como no autenticado si es el primer chequeo
if (!authChecked.value) {
isAuthenticated.value = false
}
authChecked.value = true
lastCheckTime.value = now
} finally {
isCheckingAuth.value = false
}
}
const triggerAuth = () => {
console.log('[Auth] Redirecting to trigger Authentik...')
// Redirige a la página principal para que Authentik intercepte
// Si ya estamos en /, forzamos reload
if (window.location.pathname === '/') {
window.location.reload()
} else {
window.location.href = '/'
}
}
const markUnauthenticated = () => {
// Helper para marcar como no autenticado (útil cuando detectamos 401/403)
isAuthenticated.value = false
authChecked.value = true
lastCheckTime.value = Date.now()
}
const authStatus = computed(() => {
if (!authChecked.value) return 'unknown'
return isAuthenticated.value ? 'authenticated' : 'unauthenticated'
})
return {
isAuthenticated,
authChecked,
isCheckingAuth,
authStatus,
checkAuth,
triggerAuth,
markUnauthenticated
}
}

View File

@@ -47,7 +47,16 @@ export const useMusicStore = defineStore('music', {
const response = await $fetch<{ tracks: Track[] }>('/api/music')
this.tracks = response.tracks || []
} catch (e: any) {
this.error = e?.message || 'Failed to load tracks'
const errorMsg = e?.message || 'Failed to load tracks'
this.error = errorMsg
// Check if it's an auth error
if (e?.statusCode === 401 || e?.statusCode === 403 ||
errorMsg.includes('401') || errorMsg.includes('403') ||
errorMsg.includes('Unauthorized')) {
console.warn('[Music Store] Authentication error detected')
// The useAuth composable will be notified via watch in components
}
} finally {
this.loading = false
}