vamos
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-12 00:59:50 -06:00
parent 1f087eb6f3
commit 98fb972a4e
4 changed files with 173 additions and 34 deletions

View File

@@ -16,24 +16,70 @@
</template> </template>
<script setup> <script setup>
import { computed, onMounted, watch } from 'vue' import { computed, onMounted, onUnmounted, watch } from 'vue'
import { UserCheck, UserX, Loader, ShieldAlert } from 'lucide-vue-next' import { UserCheck, UserX, Loader, ShieldAlert } from 'lucide-vue-next'
import { useAuth } from '~/composables/useAuth' import { useAuth } from '~/composables/useAuth'
import { useMusicStore } from '~/stores/music' import { useMusicStore } from '~/stores/music'
const { isAuthenticated, authChecked, isCheckingAuth, authStatus, checkAuth, triggerAuth } = useAuth() const {
isAuthenticated,
authChecked,
isCheckingAuth,
authStatus,
checkAuth,
triggerAuth,
markUnauthenticated,
setupVisibilityListener,
cleanupListeners
} = useAuth()
const musicStore = useMusicStore() const musicStore = useMusicStore()
// Check auth on mount // Check auth on mount and setup listeners
onMounted(async () => { onMounted(async () => {
await checkAuth() await checkAuth(true)
setupVisibilityListener()
// Chequea auth cada 30 segundos mientras la pestaña está activa
const interval = setInterval(() => {
if (!document.hidden) {
checkAuth()
}
}, 30000)
onUnmounted(() => {
clearInterval(interval)
})
})
// Cleanup listeners on unmount
onUnmounted(() => {
cleanupListeners()
}) })
// Watch for failed API requests that might indicate auth loss // Watch for failed API requests that might indicate auth loss
watch(() => musicStore.error, (error) => { watch(() => musicStore.error, (error, oldError) => {
if (error && (error.includes('401') || error.includes('403') || error.includes('Unauthorized'))) { // Solo actuar si es un nuevo error (no el mismo)
// Auth might have expired, recheck if (error && error !== oldError) {
checkAuth() console.log('[AuthIndicator] Music store error detected:', error)
if (error.includes('401') || error.includes('403') ||
error.includes('Unauthorized') || error.includes('Forbidden')) {
// 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
if (error.includes('401') || error.includes('403') ||
error.includes('Unauthorized') || error.includes('Forbidden')) {
markUnauthenticated()
}
} }
}) })

View File

@@ -1,4 +1,4 @@
import { ref, computed } from 'vue' import { ref, computed, onUnmounted } from 'vue'
// Estado global compartido para auth // Estado global compartido para auth
const authChecked = ref(false) const authChecked = ref(false)
@@ -6,12 +6,16 @@ const isAuthenticated = ref(false)
const isCheckingAuth = ref(false) const isCheckingAuth = ref(false)
const lastCheckTime = ref(0) 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 = () => { export const useAuth = () => {
const checkAuth = async (force = false) => { const checkAuth = async (force = false) => {
// Evitar chequeos duplicados (cache de 30 segundos) // Evitar chequeos duplicados (cache de 10 segundos, reducido)
const now = Date.now() const now = Date.now()
if (!force && isCheckingAuth.value) return if (!force && isCheckingAuth.value) return
if (!force && authChecked.value && now - lastCheckTime.value < 30000) { if (!force && authChecked.value && now - lastCheckTime.value < 10000) {
return return
} }
@@ -22,23 +26,23 @@ export const useAuth = () => {
const response = await fetch('/api/music', { const response = await fetch('/api/music', {
method: 'HEAD', // Solo headers, no body method: 'HEAD', // Solo headers, no body
credentials: 'include', // Include cookies for auth credentials: 'include', // Include cookies for auth
cache: 'no-store', // No cachear la respuesta
signal: AbortSignal.timeout(5000) // 5s timeout signal: AbortSignal.timeout(5000) // 5s timeout
}) })
// Si responde 200, estamos autenticados const wasAuthenticated = isAuthenticated.value
// Si responde 401/403, no estamos autenticados
isAuthenticated.value = response.ok isAuthenticated.value = response.ok
authChecked.value = true authChecked.value = true
lastCheckTime.value = now lastCheckTime.value = now
console.log('[Auth] Status:', isAuthenticated.value ? 'authenticated' : 'unauthenticated') // Log cambios de estado
if (wasAuthenticated !== isAuthenticated.value) {
console.log('[Auth] Status changed:', isAuthenticated.value ? 'authenticated' : 'unauthenticated')
}
} catch (error) { } catch (error) {
console.warn('[Auth] Failed to check authentication status:', error) console.warn('[Auth] Failed to check authentication status:', error)
// En caso de error de red, mantenemos el último estado conocido si existe // En caso de error de red, marcamos como no autenticado
// Solo marcamos como no autenticado si es el primer chequeo
if (!authChecked.value) {
isAuthenticated.value = false isAuthenticated.value = false
}
authChecked.value = true authChecked.value = true
lastCheckTime.value = now lastCheckTime.value = now
} finally { } finally {
@@ -46,24 +50,77 @@ export const useAuth = () => {
} }
} }
const triggerAuth = () => { const triggerAuth = async () => {
console.log('[Auth] Redirecting to trigger Authentik...') console.log('[Auth] Triggering authentication flow...')
// Redirige a la página principal para que Authentik intercepte
// Si ya estamos en /, forzamos reload // Primero, hacemos una request al endpoint protegido para forzar
if (window.location.pathname === '/') { // que Authentik nos redirija a su página de login
window.location.reload() try {
} else { const response = await fetch('/api/music', {
window.location.href = '/' method: 'GET',
credentials: 'include',
redirect: 'manual' // No seguir redirects automáticamente
})
if (!response.ok && (response.status === 401 || response.status === 403)) {
// Si obtenemos 401/403, forzamos un reload completo de la página
// para que Traefik + Authentik intercepten y redirijan al login
console.log('[Auth] Unauthorized - forcing full page reload to trigger auth')
window.location.href = window.location.href
} else if (response.ok) {
// Si la respuesta es OK, ya estamos autenticados
console.log('[Auth] Already authenticated')
await checkAuth(true)
}
} catch (error) {
console.error('[Auth] Error triggering auth:', error)
// En caso de error, forzamos reload de todas formas
window.location.href = window.location.href
} }
} }
const markUnauthenticated = () => { const markUnauthenticated = () => {
// Helper para marcar como no autenticado (útil cuando detectamos 401/403) // Helper para marcar como no autenticado (útil cuando detectamos 401/403)
console.log('[Auth] Marking as unauthenticated')
isAuthenticated.value = false isAuthenticated.value = false
authChecked.value = true authChecked.value = true
lastCheckTime.value = Date.now() lastCheckTime.value = Date.now()
} }
const setupVisibilityListener = () => {
// 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) {
console.log('[Auth] Tab visible again, checking auth...')
checkAuth(true)
}
}
document.addEventListener('visibilitychange', visibilityChangeListener)
}
// También chequea cuando la ventana recibe foco
if (typeof window !== 'undefined' && !focusListener) {
focusListener = () => {
console.log('[Auth] Window focused, checking auth...')
checkAuth(true)
}
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(() => { const authStatus = computed(() => {
if (!authChecked.value) return 'unknown' if (!authChecked.value) return 'unknown'
return isAuthenticated.value ? 'authenticated' : 'unauthenticated' return isAuthenticated.value ? 'authenticated' : 'unauthenticated'
@@ -76,6 +133,8 @@ export const useAuth = () => {
authStatus, authStatus,
checkAuth, checkAuth,
triggerAuth, triggerAuth,
markUnauthenticated markUnauthenticated,
setupVisibilityListener,
cleanupListeners
} }
} }

View File

@@ -166,9 +166,20 @@ const playTrack = async (track, index) => {
// Fetch and preload entire song into memory // Fetch and preload entire song into memory
const encodedName = encodeURIComponent(track.name) const encodedName = encodeURIComponent(track.name)
const response = await fetch(`/api/music/${encodedName}`) const response = await fetch(`/api/music/${encodedName}`)
// Check for authentication errors
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`) const errorMsg = `HTTP ${response.status}`
// If auth error, propagate to music store for auth indicator to detect
if (response.status === 401 || response.status === 403) {
console.warn('[PlayTrack] Authentication error:', response.status)
musicStore.error = `${errorMsg}: Unauthorized - Please log in`
} }
throw new Error(errorMsg)
}
const blob = await response.blob() const blob = await response.blob()
const audioUrl = URL.createObjectURL(blob) const audioUrl = URL.createObjectURL(blob)
@@ -191,6 +202,12 @@ const playTrack = async (track, index) => {
failedTracks.value.add(track.name) failedTracks.value.add(track.name)
loadingTrack.value = null loadingTrack.value = null
// Don't try fallback streaming if it's an auth error (it will fail too)
if (error.message && (error.message.includes('401') || error.message.includes('403'))) {
console.warn('[PlayTrack] Skipping fallback due to auth error')
return
}
// Try fallback to streaming // Try fallback to streaming
const encodedName = encodeURIComponent(track.name) const encodedName = encodeURIComponent(track.name)
audioPlayer.value.src = `/api/music/${encodedName}` audioPlayer.value.src = `/api/music/${encodedName}`

View File

@@ -129,11 +129,28 @@ export const useMusicStore = defineStore('music', {
try { try {
const encodedName = encodeURIComponent(name) const encodedName = encodeURIComponent(name)
const response = await fetch(`/api/music/${encodedName}`) const response = await fetch(`/api/music/${encodedName}`)
if (!response.ok) throw new Error(`HTTP ${response.status}`)
if (!response.ok) {
const errorMsg = `HTTP ${response.status}`
// Detect authentication errors
if (response.status === 401 || response.status === 403) {
console.warn('[Music Store] Cache failed - authentication error:', response.status)
this.error = `${errorMsg}: Unauthorized - Please log in`
}
throw new Error(errorMsg)
}
const blob = await response.blob() const blob = await response.blob()
await this.saveTrackBlob(name, blob, duration) await this.saveTrackBlob(name, blob, duration)
return true return true
} catch (e) { } catch (e: any) {
console.error('[Music Store] Cache failed:', e)
// Propagate auth errors
if (e?.message?.includes('401') || e?.message?.includes('403')) {
this.error = e.message
}
return false return false
} }
} }