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:
@@ -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:-}"
|
||||||
|
|||||||
16
.env.example
16
.env.example
@@ -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
56
CONFIGURACION_GITEA.md
Normal 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
|
||||||
@@ -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 />
|
||||||
|
|||||||
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'],
|
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'
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user