Add user info header to authentication dropdown
All checks were successful
build-and-deploy / build (push) Successful in 23s
build-and-deploy / deploy (push) Successful in 2s

- Created /api/auth/userinfo endpoint to fetch user data from Authentik headers
- Added userInfo state and fetchUserInfo() function to useAuth composable
- Implemented compact user info header in dropdown with avatar, name, and email
- Avatar shows user initials with gradient background
- Styled with glassmorphism design matching app aesthetic
This commit is contained in:
2025-10-12 03:26:38 -06:00
parent efaf33a4f4
commit 182bfa74c9
3 changed files with 124 additions and 1 deletions

View File

@@ -30,6 +30,19 @@
:style="dropdownStyle" :style="dropdownStyle"
@click.stop @click.stop
> >
<!-- User Info Header -->
<div v-if="userInfo" class="user-info-header">
<div class="user-avatar">
{{ userInfo.initials }}
</div>
<div class="user-details">
<div class="user-name">{{ userInfo.name }}</div>
<div class="user-email">{{ userInfo.email }}</div>
</div>
</div>
<div class="dropdown-divider"></div>
<button class="dropdown-item" @click="handleVerifyAuth"> <button class="dropdown-item" @click="handleVerifyAuth">
<RefreshCw :size="16" /> <RefreshCw :size="16" />
<span>Verificar estado</span> <span>Verificar estado</span>
@@ -56,8 +69,10 @@ const {
authChecked, authChecked,
isCheckingAuth, isCheckingAuth,
authStatus, authStatus,
userInfo,
checkAuth, checkAuth,
triggerAuth, triggerAuth,
fetchUserInfo,
logout, logout,
markUnauthenticated, markUnauthenticated,
setupVisibilityListener, setupVisibilityListener,
@@ -76,6 +91,11 @@ onMounted(async () => {
console.log('[AuthIndicator] Mounted - assuming authenticated (page loaded successfully)') console.log('[AuthIndicator] Mounted - assuming authenticated (page loaded successfully)')
setupVisibilityListener() setupVisibilityListener()
// Cargar información del usuario si estamos autenticados
if (isAuthenticated.value) {
await fetchUserInfo()
}
// POLLING DESHABILITADO: Causa conflictos con Authentik cuando no estamos autenticados // POLLING DESHABILITADO: Causa conflictos con Authentik cuando no estamos autenticados
// En su lugar, confiamos en la detección reactiva de errores del musicStore // En su lugar, confiamos en la detección reactiva de errores del musicStore
console.log('[AuthIndicator] Polling disabled - relying on reactive error detection') console.log('[AuthIndicator] Polling disabled - relying on reactive error detection')
@@ -433,7 +453,7 @@ onUnmounted(() => {
.auth-dropdown { .auth-dropdown {
position: fixed; position: fixed;
z-index: 10000; z-index: 10000;
min-width: 200px; min-width: 220px;
border-radius: 12px; border-radius: 12px;
background: var(--bg-secondary); background: var(--bg-secondary);
backdrop-filter: blur(50px); backdrop-filter: blur(50px);
@@ -445,6 +465,57 @@ onUnmounted(() => {
overflow: hidden; overflow: hidden;
} }
/* User Info Header */
.user-info-header {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 14px;
margin-bottom: 4px;
}
.user-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
display: flex;
align-items: center;
justify-content: center;
font-size: 0.875rem;
font-weight: 600;
color: white;
flex-shrink: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.user-details {
flex: 1;
min-width: 0;
overflow: hidden;
}
.user-name {
font-size: 0.9rem;
font-weight: 600;
color: var(--text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.3;
}
.user-email {
font-size: 0.75rem;
color: var(--text-secondary);
opacity: 0.7;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.3;
margin-top: 2px;
}
.dropdown-item { .dropdown-item {
width: 100%; width: 100%;
display: flex; display: flex;

View File

@@ -6,6 +6,9 @@ const isAuthenticated = ref(true) // Asumimos autenticado al inicio (si la pági
const isCheckingAuth = ref(false) const isCheckingAuth = ref(false)
const lastCheckTime = ref(0) const lastCheckTime = ref(0)
// Estado de información del usuario
const userInfo = ref(null)
// Listener para cambios de autenticación desde otras tabs/ventanas // Listener para cambios de autenticación desde otras tabs/ventanas
let visibilityChangeListener: (() => void) | null = null let visibilityChangeListener: (() => void) | null = null
let focusListener: (() => void) | null = null let focusListener: (() => void) | null = null
@@ -106,12 +109,31 @@ export const useAuth = () => {
window.location.href = window.location.origin + '/' window.location.href = window.location.origin + '/'
} }
const fetchUserInfo = async () => {
if (!isAuthenticated.value) {
console.log('[Auth] Cannot fetch user info - not authenticated')
return
}
try {
const info = await $fetch('/api/auth/userinfo')
userInfo.value = info
console.log('[Auth] User info loaded:', info)
} catch (error) {
console.warn('[Auth] Failed to fetch user info:', error)
userInfo.value = null
}
}
const logout = async () => { const logout = async () => {
console.log('[Auth] Logging out...') console.log('[Auth] Logging out...')
// Marcar como no autenticado y desregistrar SW // Marcar como no autenticado y desregistrar SW
await markUnauthenticated() await markUnauthenticated()
// Limpiar información del usuario
userInfo.value = null
// Navegar directamente al servidor de Authentik para logout // Navegar directamente al servidor de Authentik para logout
// Authentik tiene un flujo de invalidación por defecto que cierra la sesión // Authentik tiene un flujo de invalidación por defecto que cierra la sesión
console.log('[Auth] Navigating to Authentik logout...') console.log('[Auth] Navigating to Authentik logout...')
@@ -198,8 +220,10 @@ export const useAuth = () => {
authChecked, authChecked,
isCheckingAuth, isCheckingAuth,
authStatus, authStatus,
userInfo,
checkAuth, checkAuth,
triggerAuth, triggerAuth,
fetchUserInfo,
logout, logout,
markUnauthenticated, markUnauthenticated,
setupVisibilityListener, setupVisibilityListener,

View File

@@ -0,0 +1,28 @@
export default defineEventHandler((event) => {
// Authentik forward-auth pasa información del usuario en headers
const username = getHeader(event, 'x-authentik-username') || 'Usuario'
const email = getHeader(event, 'x-authentik-email') || ''
const name = getHeader(event, 'x-authentik-name') || ''
const uid = getHeader(event, 'x-authentik-uid') || ''
// Retornar información del usuario
return {
username,
email,
name: name || username, // Usar username si no hay name
uid,
initials: getInitials(name || username)
}
})
// Helper para obtener iniciales
function getInitials(name: string): string {
if (!name) return '?'
const parts = name.trim().split(/\s+/)
if (parts.length === 1) {
return parts[0].substring(0, 2).toUpperCase()
}
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase()
}