Initial commit: Add Nuxt app with Authentik integration and profile editing
All checks were successful
build-and-deploy / build (push) Successful in 58s
build-and-deploy / deploy (push) Successful in 3s

- 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:
2025-10-16 17:14:49 -06:00
parent 43bcf4a647
commit 248af8f8d1
8 changed files with 366 additions and 1 deletions

View File

@@ -8,7 +8,7 @@ set -euo pipefail
# Configuración # Configuración
GITEA_URL="https://gitea.nucleoriofrio.com" GITEA_URL="https://gitea.nucleoriofrio.com"
OWNER="nucleo000" OWNER="nucleo000"
REPO="plantillaNuxtAuthentikProxy" REPO="perfil"
# Intentar cargar el token desde el entorno o desde ~/.bashrc # Intentar cargar el token desde el entorno o desde ~/.bashrc
GITEA_TOKEN="${GITEA_TOKEN:-}" GITEA_TOKEN="${GITEA_TOKEN:-}"

View File

@@ -54,3 +54,19 @@ REGISTRY_PASSWORD=mi-password-secreto
# - X-authentik-name: nombre completo # - X-authentik-name: nombre completo
# - X-authentik-groups: grupos del usuario (separados por |) # - X-authentik-groups: grupos del usuario (separados por |)
# - X-authentik-uid: ID único del usuario # - X-authentik-uid: ID único del usuario
# URL pública de Authentik (para redirecciones de login/logout)
NUXT_PUBLIC_AUTHENTIK_URL=https://authentik.nucleoriofrio.com
# ===========================================
# AUTHENTIK API (para edición de perfil)
# ===========================================
# Token de API de Authentik (SECRETO)
# Para crear un token:
# 1. Ve a Authentik Admin → Directory → Tokens & App passwords
# 2. Crea un nuevo token con el usuario que tendrá permisos para editar usuarios
# 3. Guarda el token de forma segura
NUXT_AUTHENTIK_API_TOKEN=tu-token-de-api-aqui
# URL de la API de Authentik (usualmente la misma que NUXT_PUBLIC_AUTHENTIK_URL)
NUXT_AUTHENTIK_API_URL=https://authentik.nucleoriofrio.com

56
CONFIGURACION_GITEA.md Normal file
View File

@@ -0,0 +1,56 @@
# Configuración de Variables en Gitea
Para completar la configuración del repositorio, necesitas agregar las siguientes variables en Gitea:
## Cómo configurar
1. Ve a: https://gitea.nucleoriofrio.com/nucleo000/perfil/settings/actions/variables
2. Para cada variable, haz click en "Add variable"
3. Ingresa el nombre y valor exactamente como se indica abajo
## Variables a configurar
### REGISTRY_URL
- **Nombre**: `REGISTRY_URL`
- **Valor**: `gitea.nucleoriofrio.com`
- **Descripción**: URL del registro Docker
### APP_NAME
- **Nombre**: `APP_NAME`
- **Valor**: `perfil`
- **Descripción**: Nombre de la aplicación
### APP_DOMAIN
- **Nombre**: `APP_DOMAIN`
- **Valor**: `inicio.nucleoriofrio.com`
- **Descripción**: Dominio donde se desplegará la aplicación
### NUXT_PUBLIC_APP_URL
- **Nombre**: `NUXT_PUBLIC_APP_URL`
- **Valor**: `https://inicio.nucleoriofrio.com`
- **Descripción**: URL pública de la aplicación
### NUXT_PUBLIC_AUTHENTIK_URL
- **Nombre**: `NUXT_PUBLIC_AUTHENTIK_URL`
- **Valor**: `https://authentik.nucleoriofrio.com`
- **Descripción**: URL pública de Authentik
### NUXT_AUTHENTIK_API_URL
- **Nombre**: `NUXT_AUTHENTIK_API_URL`
- **Valor**: `https://authentik.nucleoriofrio.com`
- **Descripción**: URL de la API de Authentik
---
## Secrets ya configurados ✅
Los siguientes secrets ya fueron configurados automáticamente:
-`REGISTRY_USERNAME`
-`REGISTRY_PASSWORD`
-`NUXT_AUTHENTIK_API_TOKEN`
## Verificación
Una vez configuradas todas las variables, puedes verificarlas en:
https://gitea.nucleoriofrio.com/nucleo000/perfil/settings/actions/variables

View File

@@ -26,6 +26,7 @@
<h3 class="text-lg font-semibold">Acciones de Sesión</h3> <h3 class="text-lg font-semibold">Acciones de Sesión</h3>
</template> </template>
<div class="flex flex-wrap gap-3"> <div class="flex flex-wrap gap-3">
<AuthEditProfileButton />
<AuthSessionStatusButton /> <AuthSessionStatusButton />
<AuthProfileButton /> <AuthProfileButton />
<AuthLogoutButton /> <AuthLogoutButton />

View 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>

View 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'
})
}
})

View 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'
})
}
})

View File

@@ -15,6 +15,11 @@ export default defineNuxtConfig({
css: ['~/assets/css/main.css'], css: ['~/assets/css/main.css'],
runtimeConfig: { 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: { public: {
authentikUrl: process.env.NUXT_PUBLIC_AUTHENTIK_URL || 'https://authentik.nucleoriofrio.com' authentikUrl: process.env.NUXT_PUBLIC_AUTHENTIK_URL || 'https://authentik.nucleoriofrio.com'
} }