Initial commit: Add Nuxt app with Authentik integration and profile editing
- Add Nuxt 4 application with Authentik Proxy Outpost integration - Add EditProfileButton component for editing user profile via Authentik API - Add API endpoints for Authentik user management (GET/PATCH) - Configure Gitea Actions workflow for automated deployment - Add monitoring hook for Gitea Actions - Configure environment variables and Docker Compose setup
This commit is contained in:
@@ -26,6 +26,7 @@
|
||||
<h3 class="text-lg font-semibold">Acciones de Sesión</h3>
|
||||
</template>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<AuthEditProfileButton />
|
||||
<AuthSessionStatusButton />
|
||||
<AuthProfileButton />
|
||||
<AuthLogoutButton />
|
||||
|
||||
156
nuxt4/app/components/auth/EditProfileButton.vue
Normal file
156
nuxt4/app/components/auth/EditProfileButton.vue
Normal file
@@ -0,0 +1,156 @@
|
||||
<template>
|
||||
<UButton
|
||||
icon="i-heroicons-pencil-square"
|
||||
color="primary"
|
||||
variant="soft"
|
||||
:loading="isLoading"
|
||||
@click="openModal"
|
||||
>
|
||||
Editar Perfil
|
||||
</UButton>
|
||||
|
||||
<UModal v-model="isOpen">
|
||||
<UCard>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg font-semibold">Editar Perfil</h3>
|
||||
<UButton
|
||||
color="gray"
|
||||
variant="ghost"
|
||||
icon="i-heroicons-x-mark"
|
||||
@click="isOpen = false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<form @submit.prevent="updateProfile" class="space-y-4">
|
||||
<UFormGroup label="Nombre Completo" name="name">
|
||||
<UInput
|
||||
v-model="formData.name"
|
||||
placeholder="Tu nombre completo"
|
||||
:disabled="isUpdating"
|
||||
/>
|
||||
</UFormGroup>
|
||||
|
||||
<UFormGroup label="Email" name="email">
|
||||
<UInput
|
||||
v-model="formData.email"
|
||||
type="email"
|
||||
placeholder="tu@email.com"
|
||||
:disabled="isUpdating"
|
||||
/>
|
||||
</UFormGroup>
|
||||
|
||||
<UFormGroup label="Username" name="username">
|
||||
<UInput
|
||||
v-model="formData.username"
|
||||
disabled
|
||||
:ui="{ base: 'cursor-not-allowed opacity-50' }"
|
||||
/>
|
||||
<template #help>
|
||||
<span class="text-xs text-gray-500">El username no se puede cambiar</span>
|
||||
</template>
|
||||
</UFormGroup>
|
||||
</form>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<UButton
|
||||
color="gray"
|
||||
variant="ghost"
|
||||
:disabled="isUpdating"
|
||||
@click="isOpen = false"
|
||||
>
|
||||
Cancelar
|
||||
</UButton>
|
||||
<UButton
|
||||
color="primary"
|
||||
:loading="isUpdating"
|
||||
@click="updateProfile"
|
||||
>
|
||||
Guardar Cambios
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
</UModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { user } = useAuthentik()
|
||||
const toast = useToast()
|
||||
|
||||
const isOpen = ref(false)
|
||||
const isLoading = ref(false)
|
||||
const isUpdating = ref(false)
|
||||
|
||||
const formData = ref({
|
||||
name: '',
|
||||
email: '',
|
||||
username: ''
|
||||
})
|
||||
|
||||
const openModal = async () => {
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
// Obtener información del usuario desde Authentik
|
||||
const userData = await $fetch('/api/authentik/user')
|
||||
|
||||
formData.value = {
|
||||
name: userData.name || '',
|
||||
email: userData.email || '',
|
||||
username: userData.username || ''
|
||||
}
|
||||
|
||||
isOpen.value = true
|
||||
} catch (error: any) {
|
||||
toast.add({
|
||||
title: 'Error',
|
||||
description: error.message || 'No se pudo cargar la información del usuario',
|
||||
color: 'error',
|
||||
icon: 'i-heroicons-x-circle'
|
||||
})
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const updateProfile = async () => {
|
||||
isUpdating.value = true
|
||||
|
||||
try {
|
||||
await $fetch('/api/authentik/user', {
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
name: formData.value.name,
|
||||
email: formData.value.email
|
||||
}
|
||||
})
|
||||
|
||||
toast.add({
|
||||
title: 'Perfil Actualizado',
|
||||
description: 'Los cambios se han guardado correctamente. Recarga la página para verlos.',
|
||||
color: 'success',
|
||||
icon: 'i-heroicons-check-circle',
|
||||
actions: [{
|
||||
label: 'Recargar',
|
||||
onClick: () => {
|
||||
window.location.reload()
|
||||
}
|
||||
}]
|
||||
})
|
||||
|
||||
isOpen.value = false
|
||||
} catch (error: any) {
|
||||
toast.add({
|
||||
title: 'Error',
|
||||
description: error.message || 'No se pudo actualizar el perfil',
|
||||
color: 'error',
|
||||
icon: 'i-heroicons-x-circle'
|
||||
})
|
||||
} finally {
|
||||
isUpdating.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
57
nuxt4/app/server/api/authentik/user.get.ts
Normal file
57
nuxt4/app/server/api/authentik/user.get.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Obtiene la información del usuario actual desde Authentik
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const config = useRuntimeConfig()
|
||||
const headers = getRequestHeaders(event)
|
||||
|
||||
// Obtener el username desde los headers de Authentik
|
||||
const username = headers['x-authentik-username']
|
||||
|
||||
if (!username) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
message: 'Usuario no autenticado'
|
||||
})
|
||||
}
|
||||
|
||||
// Obtener la URL y token de Authentik desde variables de entorno
|
||||
const authentikUrl = config.authentikApiUrl || config.public.authentikUrl
|
||||
const authentikToken = config.authentikApiToken
|
||||
|
||||
if (!authentikToken) {
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
message: 'Token de Authentik no configurado'
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
// Consultar la API de Authentik para obtener información detallada del usuario
|
||||
const response = await $fetch(`${authentikUrl}/api/v3/core/users/?username=${username}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${authentikToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
// La API devuelve un array de resultados
|
||||
const users = response as any
|
||||
|
||||
if (!users.results || users.results.length === 0) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
message: 'Usuario no encontrado'
|
||||
})
|
||||
}
|
||||
|
||||
// Devolver el primer resultado (debería ser único por username)
|
||||
return users.results[0]
|
||||
} catch (error: any) {
|
||||
console.error('Error al obtener usuario de Authentik:', error)
|
||||
throw createError({
|
||||
statusCode: error.statusCode || 500,
|
||||
message: error.message || 'Error al obtener información del usuario'
|
||||
})
|
||||
}
|
||||
})
|
||||
74
nuxt4/app/server/api/authentik/user.patch.ts
Normal file
74
nuxt4/app/server/api/authentik/user.patch.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Actualiza la información del usuario en Authentik
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const config = useRuntimeConfig()
|
||||
const headers = getRequestHeaders(event)
|
||||
|
||||
// Obtener el username desde los headers de Authentik
|
||||
const username = headers['x-authentik-username']
|
||||
|
||||
if (!username) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
message: 'Usuario no autenticado'
|
||||
})
|
||||
}
|
||||
|
||||
// Obtener la URL y token de Authentik desde variables de entorno
|
||||
const authentikUrl = config.authentikApiUrl || config.public.authentikUrl
|
||||
const authentikToken = config.authentikApiToken
|
||||
|
||||
if (!authentikToken) {
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
message: 'Token de Authentik no configurado'
|
||||
})
|
||||
}
|
||||
|
||||
// Leer el body de la petición
|
||||
const body = await readBody(event)
|
||||
|
||||
try {
|
||||
// Primero, obtener el ID del usuario
|
||||
const usersResponse = await $fetch(`${authentikUrl}/api/v3/core/users/?username=${username}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${authentikToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
const users = usersResponse as any
|
||||
|
||||
if (!users.results || users.results.length === 0) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
message: 'Usuario no encontrado'
|
||||
})
|
||||
}
|
||||
|
||||
const userId = users.results[0].pk
|
||||
|
||||
// Actualizar el usuario
|
||||
const updateResponse = await $fetch(`${authentikUrl}/api/v3/core/users/${userId}/`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${authentikToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: {
|
||||
name: body.name,
|
||||
// Otros campos que se puedan actualizar
|
||||
...(body.email && { email: body.email })
|
||||
}
|
||||
})
|
||||
|
||||
return updateResponse
|
||||
} catch (error: any) {
|
||||
console.error('Error al actualizar usuario en Authentik:', error)
|
||||
throw createError({
|
||||
statusCode: error.statusCode || 500,
|
||||
message: error.message || 'Error al actualizar información del usuario'
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -15,6 +15,11 @@ export default defineNuxtConfig({
|
||||
css: ['~/assets/css/main.css'],
|
||||
|
||||
runtimeConfig: {
|
||||
// Variables privadas del servidor (no expuestas al cliente)
|
||||
authentikApiToken: process.env.NUXT_AUTHENTIK_API_TOKEN || '',
|
||||
authentikApiUrl: process.env.NUXT_AUTHENTIK_API_URL || 'https://authentik.nucleoriofrio.com',
|
||||
|
||||
// Variables públicas (expuestas al cliente)
|
||||
public: {
|
||||
authentikUrl: process.env.NUXT_PUBLIC_AUTHENTIK_URL || 'https://authentik.nucleoriofrio.com'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user