Add Authentik integration UI components

- Create useAuthentik composable to read headers
- Add UserAvatar component with avatar and user info
- Add StatusBadges for auth/connection status
- Add ActionButtons for logout and profile
- Add UserMetadata component with full user details
- Integrate all components in main page
- Use Nuxt UI components throughout
This commit is contained in:
2025-10-12 22:53:44 -06:00
parent fc46ae7a53
commit 7de670d824
6 changed files with 274 additions and 3 deletions

View File

@@ -1,6 +1,53 @@
<template> <template>
<div> <UApp>
<NuxtRouteAnnouncer /> <NuxtRouteAnnouncer />
<NuxtWelcome />
</div> <UContainer class="py-8">
<div class="space-y-6">
<!-- Header -->
<div class="text-center mb-8">
<h1 class="text-4xl font-bold mb-2">Plantilla Nuxt + Authentik</h1>
<p class="text-gray-600 dark:text-gray-400">
Ejemplo de integración con Authentik Proxy Outpost
</p>
</div>
<!-- Componentes de autenticación -->
<div v-if="isAuthenticated" class="grid gap-6 lg:grid-cols-2">
<!-- Columna izquierda -->
<div class="space-y-6">
<!-- Avatar y datos básicos -->
<AuthUserAvatar />
<!-- Badges de estado -->
<AuthStatusBadges />
<!-- Botones de acción -->
<AuthActionButtons />
</div>
<!-- Columna derecha -->
<div class="space-y-6">
<!-- Metadatos completos -->
<AuthUserMetadata />
</div>
</div>
<!-- Mensaje si no está autenticado -->
<UCard v-else class="text-center">
<div class="py-8">
<UIcon name="i-heroicons-shield-exclamation" class="w-16 h-16 mx-auto mb-4 text-gray-400" />
<h2 class="text-2xl font-semibold mb-2">No autenticado</h2>
<p class="text-gray-600 dark:text-gray-400">
Authentik Proxy Outpost debería redirigirte automáticamente.
</p>
</div>
</UCard>
</div>
</UContainer>
</UApp>
</template> </template>
<script setup lang="ts">
const { isAuthenticated } = useAuthentik()
</script>

View File

@@ -0,0 +1,34 @@
<template>
<UCard class="w-full">
<div class="flex flex-wrap gap-3">
<!-- Botón de perfil -->
<UButton
color="primary"
size="lg"
@click="goToProfile"
>
<template #leading>
<UIcon name="i-heroicons-user-circle" />
</template>
Ver Perfil
</UButton>
<!-- Botón de logout -->
<UButton
color="error"
size="lg"
variant="soft"
@click="logout"
>
<template #leading>
<UIcon name="i-heroicons-arrow-right-on-rectangle" />
</template>
Cerrar Sesión
</UButton>
</div>
</UCard>
</template>
<script setup lang="ts">
const { logout, goToProfile } = useAuthentik()
</script>

View File

@@ -0,0 +1,46 @@
<template>
<UCard class="w-full">
<div class="flex flex-wrap gap-2">
<!-- Estado de autenticación -->
<UBadge
:color="isAuthenticated ? 'success' : 'error'"
size="lg"
variant="subtle"
>
<template #leading>
<UIcon :name="isAuthenticated ? 'i-heroicons-check-circle' : 'i-heroicons-x-circle'" />
</template>
{{ isAuthenticated ? 'Autenticado' : 'No autenticado' }}
</UBadge>
<!-- Estado de conexión -->
<UBadge
color="success"
size="lg"
variant="subtle"
>
<template #leading>
<UIcon name="i-heroicons-signal" />
</template>
Conectado
</UBadge>
<!-- Número de grupos -->
<UBadge
v-if="user"
color="primary"
size="lg"
variant="subtle"
>
<template #leading>
<UIcon name="i-heroicons-user-group" />
</template>
{{ user.groups.length }} {{ user.groups.length === 1 ? 'Grupo' : 'Grupos' }}
</UBadge>
</div>
</UCard>
</template>
<script setup lang="ts">
const { user, isAuthenticated } = useAuthentik()
</script>

View File

@@ -0,0 +1,20 @@
<template>
<UCard v-if="user" class="w-full">
<div class="flex items-center gap-4">
<UAvatar
:src="user.avatar"
:alt="user.name || user.username"
size="xl"
/>
<div class="flex-1">
<h3 class="font-semibold text-lg">{{ user.name || user.username }}</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">{{ user.email }}</p>
<p class="text-xs text-gray-400 dark:text-gray-500 mt-1">ID: {{ user.uid }}</p>
</div>
</div>
</UCard>
</template>
<script setup lang="ts">
const { user } = useAuthentik()
</script>

View File

@@ -0,0 +1,73 @@
<template>
<UCard v-if="user" class="w-full">
<template #header>
<h3 class="font-semibold text-lg flex items-center gap-2">
<UIcon name="i-heroicons-information-circle" />
Metadatos del Usuario
</h3>
</template>
<div class="space-y-3">
<!-- Username -->
<div class="flex items-start gap-3">
<UIcon name="i-heroicons-at-symbol" class="text-gray-500 dark:text-gray-400 mt-0.5" />
<div class="flex-1">
<p class="text-xs text-gray-500 dark:text-gray-400">Username</p>
<p class="font-medium">{{ user.username }}</p>
</div>
</div>
<!-- Email -->
<div class="flex items-start gap-3">
<UIcon name="i-heroicons-envelope" class="text-gray-500 dark:text-gray-400 mt-0.5" />
<div class="flex-1">
<p class="text-xs text-gray-500 dark:text-gray-400">Email</p>
<p class="font-medium">{{ user.email }}</p>
</div>
</div>
<!-- Nombre completo -->
<div class="flex items-start gap-3">
<UIcon name="i-heroicons-user" class="text-gray-500 dark:text-gray-400 mt-0.5" />
<div class="flex-1">
<p class="text-xs text-gray-500 dark:text-gray-400">Nombre Completo</p>
<p class="font-medium">{{ user.name || 'No especificado' }}</p>
</div>
</div>
<!-- UID -->
<div class="flex items-start gap-3">
<UIcon name="i-heroicons-key" class="text-gray-500 dark:text-gray-400 mt-0.5" />
<div class="flex-1">
<p class="text-xs text-gray-500 dark:text-gray-400">ID Único</p>
<p class="font-mono text-sm">{{ user.uid }}</p>
</div>
</div>
<!-- Grupos -->
<div class="flex items-start gap-3">
<UIcon name="i-heroicons-user-group" class="text-gray-500 dark:text-gray-400 mt-0.5" />
<div class="flex-1">
<p class="text-xs text-gray-500 dark:text-gray-400 mb-2">Grupos</p>
<div class="flex flex-wrap gap-2">
<UBadge
v-for="group in user.groups"
:key="group"
color="primary"
variant="soft"
>
{{ group }}
</UBadge>
<UBadge v-if="user.groups.length === 0" color="gray" variant="soft">
Sin grupos
</UBadge>
</div>
</div>
</div>
</div>
</UCard>
</template>
<script setup lang="ts">
const { user } = useAuthentik()
</script>

View File

@@ -0,0 +1,51 @@
/**
* Composable para leer información de usuario de Authentik
* Los headers son inyectados por Authentik Proxy Outpost
*/
export const useAuthentik = () => {
const headers = useRequestHeaders()
const user = computed(() => {
const username = headers['x-authentik-username']
const email = headers['x-authentik-email']
const name = headers['x-authentik-name']
const groups = headers['x-authentik-groups']
const uid = headers['x-authentik-uid']
// Si no hay username, el usuario no está autenticado
if (!username) {
return null
}
return {
username,
email,
name,
groups: groups ? groups.split('|') : [],
uid,
// Generar avatar URL usando UI Avatars
avatar: `https://ui-avatars.com/api/?name=${encodeURIComponent(name || username)}&background=random&size=128`
}
})
const isAuthenticated = computed(() => !!user.value)
const logout = () => {
// Authentik Proxy Outpost maneja el logout
// Redirigir a /outpost.goauthentik.io/sign_out
navigateTo('/outpost.goauthentik.io/sign_out', { external: true })
}
const goToProfile = () => {
// URL de perfil de Authentik
const authentikUrl = useRuntimeConfig().public.authentikUrl || 'https://authentik.nucleoriofrio.com'
navigateTo(`${authentikUrl}/if/user/`, { external: true, open: { target: '_blank' } })
}
return {
user,
isAuthenticated,
logout,
goToProfile
}
}