vamos
This commit is contained in:
@@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}`
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user