feat: migrar a useAuthentik y configurar CI/CD con Gitea Actions
- Migrar de useAuth() a useAuthentik() para autenticación SSR - Actualizar componentes UserMenu, AppSidebar y profile.vue - Configurar docker-compose.yml con variables dinámicas - Agregar Gitea Actions workflow para build y deploy automático - Implementar hook de monitoreo de Gitea Actions - Configurar secrets y variables para deploy seguro - Actualizar configuración de Traefik con Authentik Forward Auth
This commit is contained in:
152
.claude/hooks/README.md
Normal file
152
.claude/hooks/README.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# Gitea Actions Monitor Hook
|
||||
|
||||
Este hook monitorea automáticamente tus Gitea Actions después de hacer `git push` y te notifica cuando terminan.
|
||||
|
||||
## Configuración
|
||||
|
||||
### 1. Crear un token de acceso en Gitea
|
||||
|
||||
1. Ve a: https://gitea.nucleoriofrio.com/user/settings/applications
|
||||
2. En la sección **"Generate New Token"**:
|
||||
- **Token Name**: `claude-code-monitor` (o el nombre que prefieras)
|
||||
- **Select Permissions**: Marca `read:repository` o `repo` (lectura de repositorio)
|
||||
3. Click en **"Generate Token"**
|
||||
4. **IMPORTANTE**: Copia el token inmediatamente (solo se muestra una vez)
|
||||
|
||||
### 2. Configurar la variable de entorno
|
||||
|
||||
Agrega el token a tu archivo de configuración de shell:
|
||||
|
||||
**Para Bash** (`~/.bashrc`):
|
||||
```bash
|
||||
export GITEA_TOKEN='tu_token_aqui'
|
||||
```
|
||||
|
||||
**Para Zsh** (`~/.zshrc`):
|
||||
```bash
|
||||
export GITEA_TOKEN='tu_token_aqui'
|
||||
```
|
||||
|
||||
**Para Fish** (`~/.config/fish/config.fish`):
|
||||
```fish
|
||||
set -x GITEA_TOKEN 'tu_token_aqui'
|
||||
```
|
||||
|
||||
Luego recarga la configuración:
|
||||
```bash
|
||||
source ~/.bashrc # o ~/.zshrc o reinicia la terminal
|
||||
```
|
||||
|
||||
### 3. Verificar que funciona
|
||||
|
||||
Verifica que la variable está configurada:
|
||||
```bash
|
||||
echo $GITEA_TOKEN
|
||||
```
|
||||
|
||||
Deberías ver tu token.
|
||||
|
||||
## Uso
|
||||
|
||||
Una vez configurado, el hook se activa automáticamente cuando Claude Code ejecuta `git push`:
|
||||
|
||||
1. Claude ejecuta `git push`
|
||||
2. El hook verifica que Actions estén habilitadas
|
||||
3. El hook se activa automáticamente y comienza a monitorear
|
||||
4. **Claude se congela** mientras espera a que termine la Gitea Action (máximo 10 minutos)
|
||||
5. Puedes presionar **Ctrl+C** para interrumpir la espera si es necesario
|
||||
6. Cuando termine, Claude te muestra información detallada:
|
||||
|
||||
**Ejemplo de action exitosa:**
|
||||
```
|
||||
✅ Gitea Action completada: EXITOSO
|
||||
|
||||
📋 Detalles:
|
||||
• Workflow: build-and-deploy (build-and-deploy.yml)
|
||||
• Run #3
|
||||
• Evento: push
|
||||
• Branch: master
|
||||
• Commit: a1b2c3d4
|
||||
• Título: Update README: Add Claude Code hooks feature
|
||||
• Duración: 5m 23s
|
||||
• Iniciado: 2025-10-12T14:30:05Z
|
||||
• Finalizado: 2025-10-12T14:35:28Z
|
||||
|
||||
🔗 Ver logs completos:
|
||||
https://gitea.nucleoriofrio.com/nucleo000/plantillaNuxtAuthentikProxy/actions/runs/123
|
||||
```
|
||||
|
||||
**Si las Actions no están habilitadas:**
|
||||
```
|
||||
⚠️ Git push exitoso, pero las Gitea Actions NO están habilitadas en este repositorio.
|
||||
|
||||
📝 Para habilitar Actions:
|
||||
1. Ve a: https://gitea.nucleoriofrio.com/nucleo000/plantillaNuxtAuthentikProxy/settings
|
||||
2. Busca la sección 'Actions' o 'Workflows'
|
||||
3. Activa las Actions
|
||||
```
|
||||
|
||||
## Configuración Avanzada
|
||||
|
||||
### Cambiar el timeout
|
||||
|
||||
Edita `.claude/settings.local.json` y modifica el valor `timeout` (en segundos):
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Bash",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/monitor-gitea-action.sh",
|
||||
"timeout": 300 // 5 minutos en lugar de 10
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Cambiar el intervalo de polling
|
||||
|
||||
Edita `.claude/hooks/monitor-gitea-action.sh` y modifica:
|
||||
|
||||
```bash
|
||||
POLL_INTERVAL=10 # Consultar cada 10 segundos (puedes cambiar a 5, 15, 30, etc.)
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### El hook no se activa
|
||||
|
||||
1. Verifica que el archivo de configuración es correcto: `cat .claude/settings.local.json`
|
||||
2. Reinicia Claude Code para que recargue la configuración
|
||||
3. Revisa los logs con `claude --debug`
|
||||
|
||||
### Error "falta GITEA_TOKEN"
|
||||
|
||||
El token no está configurado. Sigue los pasos de configuración arriba.
|
||||
|
||||
### Timeout: La action todavía está corriendo
|
||||
|
||||
La action tardó más de 10 minutos. Puedes:
|
||||
- Aumentar el `timeout` en la configuración
|
||||
- Verificar el estado manualmente en Gitea
|
||||
- Optimizar tu Gitea Action para que sea más rápida
|
||||
|
||||
### El script no puede conectarse a la API
|
||||
|
||||
1. Verifica que puedes acceder a Gitea: `curl https://gitea.nucleoriofrio.com`
|
||||
2. Verifica que el token es válido:
|
||||
```bash
|
||||
curl -H "Authorization: token $GITEA_TOKEN" \
|
||||
https://gitea.nucleoriofrio.com/api/v1/repos/nucleo000/plantillaNuxtAuthentikProxy/actions/tasks?limit=1
|
||||
```
|
||||
|
||||
## Desactivar el hook
|
||||
|
||||
Si quieres desactivar temporalmente el hook, comenta o elimina la sección `hooks` en `.claude/settings.local.json`.
|
||||
197
.claude/hooks/monitor-gitea-action.sh
Executable file
197
.claude/hooks/monitor-gitea-action.sh
Executable file
@@ -0,0 +1,197 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Monitor Gitea Action after git push
|
||||
# Este script se ejecuta después de un git push y espera a que termine la Gitea Action
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Configuración
|
||||
GITEA_URL="https://gitea.nucleoriofrio.com"
|
||||
OWNER="nucleo000"
|
||||
REPO="analiticaNucleo"
|
||||
|
||||
# Intentar cargar el token desde el entorno o desde ~/.bashrc
|
||||
GITEA_TOKEN="${GITEA_TOKEN:-}"
|
||||
if [ -z "$GITEA_TOKEN" ] && [ -f "$HOME/.bashrc" ]; then
|
||||
# Intentar extraer el token de .bashrc
|
||||
GITEA_TOKEN=$(grep -oP "export GITEA_TOKEN=['\"]?\K[^'\"]*" "$HOME/.bashrc" 2>/dev/null || echo "")
|
||||
fi
|
||||
|
||||
MAX_WAIT_SECONDS=600 # 10 minutos
|
||||
POLL_INTERVAL=10 # Consultar cada 10 segundos
|
||||
|
||||
# Leer el input JSON del hook
|
||||
INPUT=$(cat)
|
||||
|
||||
# Verificar si el comando fue un git push
|
||||
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
|
||||
if [[ ! "$COMMAND" =~ git[[:space:]]+push ]]; then
|
||||
# No fue un git push, salir sin hacer nada
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Verificar que existe el token
|
||||
if [ -z "$GITEA_TOKEN" ]; then
|
||||
cat <<EOF
|
||||
{
|
||||
"decision": "block",
|
||||
"reason": "⚠️ Git push exitoso, pero no puedo monitorear la Gitea Action: falta GITEA_TOKEN.\n\nConfigura el token en ~/.bashrc o ~/.zshrc:\nexport GITEA_TOKEN='tu_token_aqui'"
|
||||
}
|
||||
EOF
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Función para verificar si las actions están habilitadas
|
||||
check_actions_enabled() {
|
||||
local repo_info=$(curl -s -H "Authorization: token $GITEA_TOKEN" \
|
||||
"$GITEA_URL/api/v1/repos/$OWNER/$REPO")
|
||||
echo "$repo_info" | jq -r '.has_actions // false'
|
||||
}
|
||||
|
||||
# Función para consultar el estado de la última action
|
||||
get_latest_action_status() {
|
||||
local response=$(curl -s -H "Authorization: token $GITEA_TOKEN" \
|
||||
"$GITEA_URL/api/v1/repos/$OWNER/$REPO/actions/tasks?limit=1")
|
||||
|
||||
# Verificar si hay un error de permisos
|
||||
if echo "$response" | jq -e '.message' > /dev/null 2>&1; then
|
||||
echo "ERROR: $(echo "$response" | jq -r '.message')" >&2
|
||||
echo ""
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "$response" | jq -r '.workflow_runs[0] // .data[0] // empty'
|
||||
}
|
||||
|
||||
# Función para formatear el resultado
|
||||
format_result() {
|
||||
local status="$1"
|
||||
local task_data="$2"
|
||||
|
||||
local id=$(echo "$task_data" | jq -r '.id // "N/A"')
|
||||
local workflow_name=$(echo "$task_data" | jq -r '.name // "N/A"')
|
||||
local workflow_file=$(echo "$task_data" | jq -r '.workflow_id // "N/A"')
|
||||
local run_number=$(echo "$task_data" | jq -r '.run_number // "N/A"')
|
||||
local event=$(echo "$task_data" | jq -r '.event // "N/A"')
|
||||
local branch=$(echo "$task_data" | jq -r '.head_branch // "N/A"')
|
||||
local title=$(echo "$task_data" | jq -r '.display_title // "N/A"')
|
||||
local created=$(echo "$task_data" | jq -r '.created_at // "N/A"')
|
||||
local started=$(echo "$task_data" | jq -r '.run_started_at // .started_at // "N/A"')
|
||||
local updated=$(echo "$task_data" | jq -r '.updated_at // .stopped_at // "N/A"')
|
||||
local commit=$(echo "$task_data" | jq -r '.head_sha[0:8] // "N/A"')
|
||||
local run_url=$(echo "$task_data" | jq -r '.url // ""')
|
||||
|
||||
# Calcular duración si es posible
|
||||
local duration="N/A"
|
||||
if [[ "$started" != "N/A" && "$updated" != "N/A" ]]; then
|
||||
local start_ts=$(date -d "$started" +%s 2>/dev/null || echo "0")
|
||||
local end_ts=$(date -d "$updated" +%s 2>/dev/null || echo "0")
|
||||
if [[ $start_ts -gt 0 && $end_ts -gt 0 ]]; then
|
||||
local diff=$((end_ts - start_ts))
|
||||
if [[ $diff -lt 60 ]]; then
|
||||
duration="${diff}s"
|
||||
else
|
||||
local mins=$((diff / 60))
|
||||
local secs=$((diff % 60))
|
||||
duration="${mins}m ${secs}s"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
case "$status" in
|
||||
success)
|
||||
local emoji="✅"
|
||||
local msg="EXITOSO"
|
||||
;;
|
||||
failure)
|
||||
local emoji="❌"
|
||||
local msg="FALLÓ"
|
||||
;;
|
||||
cancelled)
|
||||
local emoji="🚫"
|
||||
local msg="CANCELADO"
|
||||
;;
|
||||
*)
|
||||
local emoji="⚠️"
|
||||
local msg="DESCONOCIDO ($status)"
|
||||
;;
|
||||
esac
|
||||
|
||||
# Construir URL de logs si no está disponible
|
||||
if [[ -z "$run_url" || "$run_url" == "null" ]]; then
|
||||
run_url="$GITEA_URL/$OWNER/$REPO/actions/runs/$id"
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"decision": "block",
|
||||
"reason": "$emoji Gitea Action completada: $msg\n\n📋 Detalles:\n • Workflow: $workflow_name ($workflow_file)\n • Run #$run_number\n • Evento: $event\n • Branch: $branch\n • Commit: $commit\n • Título: $title\n • Duración: $duration\n • Iniciado: $started\n • Finalizado: $updated\n\n🔗 Ver logs completos:\n $run_url"
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
# Verificar si las actions están habilitadas
|
||||
echo "🔍 Verificando si Actions están habilitadas..." >&2
|
||||
ACTIONS_ENABLED=$(check_actions_enabled)
|
||||
|
||||
if [[ "$ACTIONS_ENABLED" != "true" ]]; then
|
||||
cat <<EOF
|
||||
{
|
||||
"decision": "block",
|
||||
"reason": "⚠️ Git push exitoso, pero las Gitea Actions NO están habilitadas en este repositorio.\n\n📝 Para habilitar Actions:\n1. Ve a: $GITEA_URL/$OWNER/$REPO/settings\n2. Busca la sección 'Actions' o 'Workflows'\n3. Activa las Actions\n\nLuego podrás ver tus workflows en:\n$GITEA_URL/$OWNER/$REPO/actions"
|
||||
}
|
||||
EOF
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Notificar que empezamos a monitorear
|
||||
echo "🔄 Monitoreando Gitea Action (máximo ${MAX_WAIT_SECONDS}s)..." >&2
|
||||
|
||||
# Esperar un poco antes de la primera consulta (dar tiempo a que Gitea cree la action)
|
||||
sleep 5
|
||||
|
||||
# Polling loop
|
||||
elapsed=0
|
||||
while [ $elapsed -lt $MAX_WAIT_SECONDS ]; do
|
||||
# Consultar el estado
|
||||
TASK_DATA=$(get_latest_action_status)
|
||||
|
||||
if [ -z "$TASK_DATA" ]; then
|
||||
echo "⏳ Esperando que Gitea cree la action... (${elapsed}s)" >&2
|
||||
sleep $POLL_INTERVAL
|
||||
elapsed=$((elapsed + POLL_INTERVAL))
|
||||
continue
|
||||
fi
|
||||
|
||||
STATUS=$(echo "$TASK_DATA" | jq -r '.status // "unknown"')
|
||||
|
||||
echo "📊 Estado actual: $STATUS (${elapsed}s)" >&2
|
||||
|
||||
# Verificar si terminó
|
||||
case "$STATUS" in
|
||||
success|failure|cancelled)
|
||||
# Action terminó!
|
||||
format_result "$STATUS" "$TASK_DATA"
|
||||
exit 0
|
||||
;;
|
||||
running|pending|waiting)
|
||||
# Todavía corriendo
|
||||
sleep $POLL_INTERVAL
|
||||
elapsed=$((elapsed + POLL_INTERVAL))
|
||||
;;
|
||||
*)
|
||||
# Estado desconocido
|
||||
echo "⚠️ Estado desconocido: $STATUS" >&2
|
||||
sleep $POLL_INTERVAL
|
||||
elapsed=$((elapsed + POLL_INTERVAL))
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Timeout alcanzado
|
||||
cat <<EOF
|
||||
{
|
||||
"decision": "block",
|
||||
"reason": "⏱️ Timeout: La Gitea Action todavía está corriendo después de ${MAX_WAIT_SECONDS}s.\n\n🔗 Verifica el estado manualmente en:\n$GITEA_URL/$OWNER/$REPO/actions"
|
||||
}
|
||||
EOF
|
||||
16
.env.example
16
.env.example
@@ -4,5 +4,17 @@ SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
|
||||
SUPABASE_ANON_KEY=your-anon-key
|
||||
|
||||
# Authentik Configuration
|
||||
AUTHENTIK_URL=https://authentik.nucleoriofrio.com
|
||||
AUTHENTIK_APP_SLUG=your-app-slug
|
||||
NUXT_PUBLIC_AUTHENTIK_URL=https://authentik.nucleoriofrio.com
|
||||
|
||||
# PostgreSQL Configuration
|
||||
POSTGRES_USER=postgres
|
||||
POSTGRES_PASSWORD=your-secure-password
|
||||
POSTGRES_DB=analitica
|
||||
|
||||
# PostgREST Configuration
|
||||
PGRST_DB_AUTHENTICATOR_PASSWORD=your-authenticator-password
|
||||
PGRST_DB_SCHEMA=public
|
||||
PGRST_DB_ANON_ROLE=web_anon
|
||||
PGRST_JWT_SECRET=your-jwt-secret-min-32-chars
|
||||
PGRST_OPENAPI_SERVER_PROXY_URI=https://api.analitica.nucleoriofrio.com
|
||||
NUXT_PUBLIC_POSTGREST_URL=https://api.analitica.nucleoriofrio.com
|
||||
|
||||
73
.gitea/workflows/build-and-deploy.yml
Normal file
73
.gitea/workflows/build-and-deploy.yml
Normal file
@@ -0,0 +1,73 @@
|
||||
name: build-and-deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, master ]
|
||||
|
||||
jobs:
|
||||
#───────────────── build & push ─────────────────
|
||||
build:
|
||||
runs-on: docker
|
||||
env:
|
||||
REG: ${{ vars.REGISTRY_URL }}
|
||||
APP_NAME: ${{ vars.APP_NAME }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: docker/setup-buildx-action@v2
|
||||
- uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ${{ vars.REGISTRY_URL }}
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
|
||||
- name: Build+push ${{ vars.APP_NAME }}
|
||||
run: |
|
||||
cd nuxt4-app
|
||||
docker build -t $REG/${{ github.repository_owner }}/$APP_NAME:${{ github.sha }} -t $REG/${{ github.repository_owner }}/$APP_NAME:latest .
|
||||
docker push $REG/${{ github.repository_owner }}/$APP_NAME:${{ github.sha }}
|
||||
docker push $REG/${{ github.repository_owner }}/$APP_NAME:latest
|
||||
|
||||
#───────────────── deploy ─────────────────
|
||||
deploy:
|
||||
needs: build
|
||||
runs-on: docker
|
||||
env:
|
||||
REG: ${{ vars.REGISTRY_URL }}
|
||||
REPO_OWNER: ${{ github.repository_owner }}
|
||||
APP_NAME: ${{ vars.APP_NAME }}
|
||||
APP_DOMAIN: ${{ vars.APP_DOMAIN }}
|
||||
# Variables sensibles de entorno desde secrets
|
||||
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
|
||||
PGRST_DB_AUTHENTICATOR_PASSWORD: ${{ secrets.PGRST_DB_AUTHENTICATOR_PASSWORD }}
|
||||
PGRST_JWT_SECRET: ${{ secrets.PGRST_JWT_SECRET }}
|
||||
SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
|
||||
SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}
|
||||
SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }}
|
||||
# Variables públicas desde vars
|
||||
POSTGRES_USER: ${{ vars.POSTGRES_USER }}
|
||||
POSTGRES_DB: ${{ vars.POSTGRES_DB }}
|
||||
PGRST_DB_SCHEMA: ${{ vars.PGRST_DB_SCHEMA }}
|
||||
PGRST_DB_ANON_ROLE: ${{ vars.PGRST_DB_ANON_ROLE }}
|
||||
PGRST_OPENAPI_SERVER_PROXY_URI: ${{ vars.PGRST_OPENAPI_SERVER_PROXY_URI }}
|
||||
NUXT_PUBLIC_POSTGREST_URL: ${{ vars.NUXT_PUBLIC_POSTGREST_URL }}
|
||||
NUXT_PUBLIC_AUTHENTIK_URL: ${{ vars.NUXT_PUBLIC_AUTHENTIK_URL }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Login to registry
|
||||
run: docker login ${{ vars.REGISTRY_URL }} -u ${{ secrets.REGISTRY_USERNAME }} -p ${{ secrets.REGISTRY_PASSWORD }}
|
||||
|
||||
- name: Info about environment
|
||||
run: |
|
||||
echo "ℹ️ Deploying ${{ vars.APP_NAME }}"
|
||||
echo " Domain: ${{ vars.APP_DOMAIN }}"
|
||||
echo " Image: ${{ vars.REGISTRY_URL }}/${{ github.repository_owner }}/${{ vars.APP_NAME }}:latest"
|
||||
echo " Network: principal"
|
||||
|
||||
- name: Pull fresh images used in compose
|
||||
run: docker compose pull
|
||||
|
||||
- name: Clean up stack
|
||||
run: docker compose --project-name $APP_NAME down
|
||||
|
||||
- name: Update stack
|
||||
run: docker compose --project-name $APP_NAME up -d --remove-orphans --wait
|
||||
197
POSTGRES_SETUP.md
Normal file
197
POSTGRES_SETUP.md
Normal file
@@ -0,0 +1,197 @@
|
||||
# Configuración de PostgreSQL y PostgREST
|
||||
|
||||
Esta aplicación utiliza PostgreSQL como base de datos principal y PostgREST para exponer las tablas y vistas como una API REST.
|
||||
|
||||
## Arquitectura
|
||||
|
||||
```
|
||||
Airbyte → PostgreSQL → PostgREST → Nuxt App
|
||||
```
|
||||
|
||||
- **Airbyte**: Sincroniza datos de diversas fuentes hacia PostgreSQL
|
||||
- **PostgreSQL**: Base de datos principal que almacena todos los datos
|
||||
- **PostgREST**: Expone las tablas y vistas de PostgreSQL como API REST
|
||||
- **Nuxt App**: Consume los datos desde la API de PostgREST
|
||||
|
||||
## Configuración Inicial
|
||||
|
||||
### 1. Variables de Entorno
|
||||
|
||||
Copia el archivo `.env.example` a `.env` y configura las siguientes variables:
|
||||
|
||||
```bash
|
||||
# PostgreSQL
|
||||
POSTGRES_USER=postgres
|
||||
POSTGRES_PASSWORD=tu-contraseña-segura
|
||||
POSTGRES_DB=analitica
|
||||
|
||||
# PostgREST
|
||||
PGRST_DB_AUTHENTICATOR_PASSWORD=tu-contraseña-authenticator
|
||||
PGRST_DB_SCHEMA=public
|
||||
PGRST_DB_ANON_ROLE=web_anon
|
||||
PGRST_JWT_SECRET=tu-jwt-secret-minimo-32-caracteres
|
||||
PGRST_OPENAPI_SERVER_PROXY_URI=https://api.analitica.nucleoriofrio.com
|
||||
NUXT_PUBLIC_POSTGREST_URL=https://api.analitica.nucleoriofrio.com
|
||||
```
|
||||
|
||||
### 2. Generar JWT Secret
|
||||
|
||||
El JWT secret debe tener al menos 32 caracteres. Puedes generarlo con:
|
||||
|
||||
```bash
|
||||
openssl rand -base64 32
|
||||
```
|
||||
|
||||
### 3. Iniciar los Servicios
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
Esto iniciará:
|
||||
- PostgreSQL en el puerto interno 5432
|
||||
- PostgREST expuesto vía Traefik en `api.analitica.nucleoriofrio.com`
|
||||
- La aplicación Nuxt en `analitica.nucleoriofrio.com`
|
||||
|
||||
## Estructura de Base de Datos
|
||||
|
||||
### Esquemas
|
||||
|
||||
Por defecto, PostgREST expone el esquema `public`. Puedes cambiar esto modificando la variable `PGRST_DB_SCHEMA`.
|
||||
|
||||
### Roles y Permisos
|
||||
|
||||
Se crean automáticamente dos roles:
|
||||
|
||||
1. **web_anon**: Rol anónimo con permisos de solo lectura
|
||||
- Se usa para consultas sin autenticación
|
||||
- Solo tiene permiso SELECT en las tablas
|
||||
|
||||
2. **authenticator**: Rol de conexión para PostgREST
|
||||
- Usado por PostgREST para conectarse a la base de datos
|
||||
- Puede cambiar al rol web_anon según sea necesario
|
||||
|
||||
### Scripts de Inicialización
|
||||
|
||||
Los scripts en `/init-db` se ejecutan automáticamente cuando se crea la base de datos por primera vez:
|
||||
|
||||
- `01-init.sql`: Crea el rol web_anon y configura permisos
|
||||
- `02-create-authenticator.sh`: Crea el rol authenticator
|
||||
|
||||
## Uso de PostgREST
|
||||
|
||||
### API REST Automática
|
||||
|
||||
PostgREST expone automáticamente todas las tablas y vistas del esquema configurado como endpoints REST:
|
||||
|
||||
#### Listar registros
|
||||
```bash
|
||||
GET https://api.analitica.nucleoriofrio.com/nombre_tabla
|
||||
```
|
||||
|
||||
#### Filtrar registros
|
||||
```bash
|
||||
GET https://api.analitica.nucleoriofrio.com/nombre_tabla?columna=eq.valor
|
||||
```
|
||||
|
||||
#### Seleccionar columnas específicas
|
||||
```bash
|
||||
GET https://api.analitica.nucleoriofrio.com/nombre_tabla?select=columna1,columna2
|
||||
```
|
||||
|
||||
#### Ordenar resultados
|
||||
```bash
|
||||
GET https://api.analitica.nucleoriofrio.com/nombre_tabla?order=columna.desc
|
||||
```
|
||||
|
||||
### Documentación de la API
|
||||
|
||||
PostgREST genera automáticamente documentación OpenAPI en:
|
||||
```
|
||||
https://api.analitica.nucleoriofrio.com/
|
||||
```
|
||||
|
||||
## Airbyte → PostgreSQL
|
||||
|
||||
### Configurar Destino en Airbyte
|
||||
|
||||
1. En Airbyte, crea un nuevo destino de tipo PostgreSQL
|
||||
2. Usa los siguientes parámetros de conexión:
|
||||
- **Host**: `analiticanucleo-postgres` (si Airbyte está en la misma red Docker) o la IP del servidor
|
||||
- **Port**: `5432`
|
||||
- **Database**: El valor de `POSTGRES_DB`
|
||||
- **User**: El valor de `POSTGRES_USER`
|
||||
- **Password**: El valor de `POSTGRES_PASSWORD`
|
||||
- **Schema**: `public` o el esquema que prefieras
|
||||
|
||||
3. Configura tus conexiones en Airbyte para sincronizar datos hacia este destino
|
||||
|
||||
## Mantenimiento
|
||||
|
||||
### Ver logs de PostgreSQL
|
||||
```bash
|
||||
docker logs analiticanucleo-postgres
|
||||
```
|
||||
|
||||
### Ver logs de PostgREST
|
||||
```bash
|
||||
docker logs analiticanucleo-postgrest
|
||||
```
|
||||
|
||||
### Conectarse a PostgreSQL
|
||||
```bash
|
||||
docker exec -it analiticanucleo-postgres psql -U postgres -d analitica
|
||||
```
|
||||
|
||||
### Reiniciar la base de datos (¡CUIDADO!)
|
||||
```bash
|
||||
docker-compose down
|
||||
docker volume rm analiticanucleo_postgres_data
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## Crear Vistas para la API
|
||||
|
||||
Para exponer datos procesados, crea vistas en PostgreSQL:
|
||||
|
||||
```sql
|
||||
CREATE VIEW vista_resumen AS
|
||||
SELECT
|
||||
columna1,
|
||||
COUNT(*) as total,
|
||||
AVG(columna2) as promedio
|
||||
FROM tabla_origen
|
||||
GROUP BY columna1;
|
||||
|
||||
-- Dar permisos al rol web_anon
|
||||
GRANT SELECT ON vista_resumen TO web_anon;
|
||||
```
|
||||
|
||||
Esta vista estará automáticamente disponible en:
|
||||
```
|
||||
GET https://api.analitica.nucleoriofrio.com/vista_resumen
|
||||
```
|
||||
|
||||
## Seguridad
|
||||
|
||||
### Autenticación JWT (Opcional)
|
||||
|
||||
Para proteger endpoints específicos, puedes configurar autenticación JWT. PostgREST validará automáticamente los tokens JWT firmados con `PGRST_JWT_SECRET`.
|
||||
|
||||
### Permisos Granulares
|
||||
|
||||
Puedes crear roles adicionales con permisos más específicos:
|
||||
|
||||
```sql
|
||||
CREATE ROLE usuario_lectura NOLOGIN;
|
||||
GRANT USAGE ON SCHEMA public TO usuario_lectura;
|
||||
GRANT SELECT ON tabla_especifica TO usuario_lectura;
|
||||
GRANT usuario_lectura TO authenticator;
|
||||
```
|
||||
|
||||
## Próximos Pasos
|
||||
|
||||
1. Configurar Airbyte para sincronizar datos hacia PostgreSQL
|
||||
2. Crear vistas y tablas según tus necesidades
|
||||
3. Consumir la API desde la aplicación Nuxt
|
||||
4. (Opcional) Configurar autenticación JWT para endpoints protegidos
|
||||
@@ -1,11 +1,55 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: analiticanucleo-postgres
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- POSTGRES_USER=${POSTGRES_USER:-postgres}
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||
- POSTGRES_DB=${POSTGRES_DB:-analitica}
|
||||
- PGRST_DB_AUTHENTICATOR_PASSWORD=${PGRST_DB_AUTHENTICATOR_PASSWORD}
|
||||
ports:
|
||||
- "3000:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
- ./init-db:/docker-entrypoint-initdb.d
|
||||
networks:
|
||||
- principal
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
postgrest:
|
||||
image: postgrest/postgrest:latest
|
||||
container_name: analiticanucleo-postgrest
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- PGRST_DB_URI=postgres://authenticator:${PGRST_DB_AUTHENTICATOR_PASSWORD}@postgres:5432/${POSTGRES_DB:-analitica}
|
||||
- PGRST_DB_SCHEMA=${PGRST_DB_SCHEMA:-public}
|
||||
- PGRST_DB_ANON_ROLE=${PGRST_DB_ANON_ROLE:-web_anon}
|
||||
- PGRST_JWT_SECRET=${PGRST_JWT_SECRET}
|
||||
- PGRST_OPENAPI_SERVER_PROXY_URI=${PGRST_OPENAPI_SERVER_PROXY_URI:-https://api.analitica.nucleoriofrio.com}
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- principal
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.analiticanucleo-api.rule=Host(`api.analitica.nucleoriofrio.com`)"
|
||||
- "traefik.http.routers.analiticanucleo-api.entrypoints=websecure"
|
||||
- "traefik.http.routers.analiticanucleo-api.tls=true"
|
||||
- "traefik.http.routers.analiticanucleo-api.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.analiticanucleo-api.loadbalancer.server.port=3000"
|
||||
- "traefik.docker.network=principal"
|
||||
|
||||
nuxt-app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: analiticanucleo-nuxt-app
|
||||
image: ${REG}/${REPO_OWNER}/${APP_NAME}:latest
|
||||
container_name: ${APP_NAME}
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
@@ -18,19 +62,37 @@ services:
|
||||
- NEXT_PUBLIC_SUPABASE_URL=${SUPABASE_URL}
|
||||
- NEXT_PUBLIC_SUPABASE_ANON_KEY=${SUPABASE_ANON_KEY}
|
||||
# Authentik configuration
|
||||
- NUXT_PUBLIC_AUTHENTIK_URL=${AUTHENTIK_URL}
|
||||
- NUXT_PUBLIC_AUTHENTIK_APP_SLUG=${AUTHENTIK_APP_SLUG}
|
||||
- NUXT_PUBLIC_AUTHENTIK_URL=${NUXT_PUBLIC_AUTHENTIK_URL:-https://authentik.nucleoriofrio.com}
|
||||
# PostgREST API URL
|
||||
- NUXT_PUBLIC_POSTGREST_URL=${NUXT_PUBLIC_POSTGREST_URL:-https://api.analitica.nucleoriofrio.com}
|
||||
depends_on:
|
||||
- postgrest
|
||||
networks:
|
||||
- principal
|
||||
- traefik-network
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.analiticanucleo.rule=Host(`analitica.nucleoriofrio.com`)"
|
||||
- "traefik.http.routers.analiticanucleo.entrypoints=websecure"
|
||||
- "traefik.http.routers.analiticanucleo.tls=true"
|
||||
- "traefik.http.routers.analiticanucleo.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.analiticanucleo.loadbalancer.server.port=3000"
|
||||
- "traefik.docker.network=principal"
|
||||
|
||||
# Service
|
||||
- "traefik.http.services.${APP_NAME}.loadbalancer.server.port=3000"
|
||||
|
||||
# Router principal con Authentik Forward Auth
|
||||
- "traefik.http.routers.${APP_NAME}.rule=Host(`${APP_DOMAIN}`)"
|
||||
- "traefik.http.routers.${APP_NAME}.entrypoints=websecure"
|
||||
- "traefik.http.routers.${APP_NAME}.tls=true"
|
||||
- "traefik.http.routers.${APP_NAME}.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.routers.${APP_NAME}.service=${APP_NAME}"
|
||||
- "traefik.http.routers.${APP_NAME}.middlewares=authentik-forward-auth@file,${APP_NAME}-headers"
|
||||
|
||||
# Custom headers middleware
|
||||
- "traefik.http.middlewares.${APP_NAME}-headers.headers.customrequestheaders.X-Forwarded-Proto=https"
|
||||
|
||||
networks:
|
||||
principal:
|
||||
external: true
|
||||
traefik-network:
|
||||
external: true
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
const { user, loading, fetchUser, logout } = useAuth()
|
||||
const { user, isAuthenticated, logout, goToProfile } = useAuthentik()
|
||||
|
||||
// Estado para el dropdown
|
||||
const isOpen = ref(false)
|
||||
|
||||
// Cargar usuario al montar
|
||||
onMounted(() => {
|
||||
fetchUser()
|
||||
})
|
||||
|
||||
// Computed para el avatar del usuario con gradiente dinámico
|
||||
const userAvatar = computed(() => ({
|
||||
src: user.value?.avatar_url || `https://ui-avatars.com/api/?name=${encodeURIComponent(user.value?.name || user.value?.username || 'User')}&background=random&bold=true&format=svg`,
|
||||
src: user.value?.avatar || `https://ui-avatars.com/api/?name=${encodeURIComponent(user.value?.name || user.value?.username || 'User')}&background=random&bold=true&format=svg`,
|
||||
alt: user.value?.name || user.value?.username || 'User'
|
||||
}))
|
||||
|
||||
@@ -72,7 +67,7 @@ const items = computed(() => [
|
||||
|
||||
<template>
|
||||
<UDropdownMenu
|
||||
v-if="user?.authenticated && !loading"
|
||||
v-if="isAuthenticated"
|
||||
v-model:open="isOpen"
|
||||
:items="items"
|
||||
:ui="{
|
||||
@@ -194,18 +189,4 @@ const items = computed(() => [
|
||||
</div>
|
||||
</template>
|
||||
</UDropdownMenu>
|
||||
|
||||
<div
|
||||
v-else-if="loading"
|
||||
class="relative"
|
||||
>
|
||||
<USkeleton
|
||||
class="h-9 w-9"
|
||||
:ui="{
|
||||
rounded: 'rounded-full',
|
||||
background: 'bg-gradient-to-br from-gray-200 to-gray-300 dark:from-gray-700 dark:to-gray-800'
|
||||
}"
|
||||
/>
|
||||
<div class="absolute inset-0 rounded-full bg-gradient-to-br from-primary-500/20 to-transparent animate-pulse" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
</template>
|
||||
|
||||
<template #footer="{ collapsed: isCollapsed }">
|
||||
<div v-if="user?.authenticated && !loading" class="space-y-3">
|
||||
<div v-if="isAuthenticated" class="space-y-3">
|
||||
<!-- User Profile Section -->
|
||||
<div
|
||||
v-if="!isCollapsed"
|
||||
@@ -212,17 +212,6 @@
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div v-else-if="loading">
|
||||
<div v-if="!isCollapsed" class="space-y-2">
|
||||
<USkeleton class="h-14 w-full" :ui="{ rounded: 'rounded-lg', background: 'bg-gray-200/60 dark:bg-gray-800/60' }" />
|
||||
<USkeleton class="h-28 w-full" :ui="{ rounded: 'rounded-lg', background: 'bg-gray-200/60 dark:bg-gray-800/60' }" />
|
||||
</div>
|
||||
<div v-else class="flex flex-col items-center gap-3">
|
||||
<USkeleton class="h-9 w-9" :ui="{ rounded: 'rounded-lg', background: 'bg-gray-200/60 dark:bg-gray-800/60' }" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</UDashboardSidebar>
|
||||
</template>
|
||||
@@ -282,16 +271,11 @@ const navigationPrimary = computed<NavigationMenuItem[]>(() => [
|
||||
}
|
||||
])
|
||||
|
||||
const { user, loading, fetchUser, logout } = useAuth()
|
||||
|
||||
// Cargar usuario al montar
|
||||
onMounted(() => {
|
||||
fetchUser()
|
||||
})
|
||||
const { user, isAuthenticated, logout } = useAuthentik()
|
||||
|
||||
// Computed para el avatar del usuario
|
||||
const userAvatar = computed(() => ({
|
||||
src: user.value?.avatar_url || `https://ui-avatars.com/api/?name=${encodeURIComponent(user.value?.name || user.value?.username || 'User')}&background=3b82f6&color=fff&bold=true&format=svg`,
|
||||
src: user.value?.avatar || `https://ui-avatars.com/api/?name=${encodeURIComponent(user.value?.name || user.value?.username || 'User')}&background=3b82f6&color=fff&bold=true&format=svg`,
|
||||
alt: user.value?.name || user.value?.username || 'User'
|
||||
}))
|
||||
</script>
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
export interface AuthUser {
|
||||
username: string | null
|
||||
email: string | null
|
||||
name: string | null
|
||||
uid: string | null
|
||||
groups: string[]
|
||||
authenticated: boolean
|
||||
}
|
||||
|
||||
export const useAuth = () => {
|
||||
const user = useState<AuthUser | null>('auth-user', () => null)
|
||||
const loading = useState<boolean>('auth-loading', () => false)
|
||||
|
||||
const fetchUser = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await $fetch<AuthUser>('/api/auth/user')
|
||||
user.value = data
|
||||
} catch (error) {
|
||||
console.error('Error fetching user:', error)
|
||||
user.value = null
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
// Limpiar estado local
|
||||
user.value = null
|
||||
loading.value = false
|
||||
|
||||
// Obtener configuración de Authentik desde variables de entorno
|
||||
const config = useRuntimeConfig()
|
||||
const authentikUrl = config.public.authentikUrl || 'https://authentik.nucleoriofrio.com'
|
||||
const appSlug = config.public.authentikAppSlug || 'devserver'
|
||||
|
||||
// Redirigir al endpoint de logout de Authentik con el slug de la aplicación
|
||||
// Esto cierra la sesión completa de Authentik (OIDC end-session)
|
||||
window.location.href = `${authentikUrl}/application/o/${appSlug}/end-session/`
|
||||
}
|
||||
|
||||
return {
|
||||
user: readonly(user),
|
||||
loading: readonly(loading),
|
||||
fetchUser,
|
||||
logout
|
||||
}
|
||||
}
|
||||
220
nuxt4-app/app/composables/useAuthentik.ts
Normal file
220
nuxt4-app/app/composables/useAuthentik.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
/**
|
||||
* Composable para leer información de usuario de Authentik
|
||||
* Los headers son inyectados por Authentik Proxy Outpost
|
||||
*
|
||||
* Documentación de headers disponibles:
|
||||
* - x-authentik-username: Username del usuario
|
||||
* - x-authentik-email: Email del usuario
|
||||
* - x-authentik-name: Nombre completo del usuario
|
||||
* - x-authentik-uid: UID único del usuario
|
||||
* - x-authentik-groups: Grupos separados por |
|
||||
* - x-authentik-meta-app: Slug de la aplicación en Authentik
|
||||
* - x-authentik-meta-outpost: Nombre del outpost
|
||||
* - Nota: Los roles RBAC son internos de Authentik y no se exponen via headers
|
||||
*/
|
||||
|
||||
interface AuthentikUser {
|
||||
username: string
|
||||
email: string | undefined
|
||||
name: string | undefined
|
||||
groups: string[]
|
||||
uid: string | undefined
|
||||
avatar: string
|
||||
// Metadata de la aplicación y outpost
|
||||
appSlug?: string
|
||||
outpostName?: string
|
||||
}
|
||||
|
||||
interface AuthStatusResponse {
|
||||
authenticated: boolean
|
||||
user?: {
|
||||
username: string
|
||||
name?: string
|
||||
}
|
||||
}
|
||||
|
||||
export const useAuthentik = () => {
|
||||
// Leer headers en el servidor y almacenarlos en state
|
||||
const authentikUser = useState<AuthentikUser | null>('authentikUser', () => {
|
||||
// Solo en el servidor, leer los headers
|
||||
if (import.meta.server) {
|
||||
const headers = useRequestHeaders()
|
||||
|
||||
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']
|
||||
const appSlug = headers['x-authentik-meta-app']
|
||||
const outpostName = headers['x-authentik-meta-outpost']
|
||||
|
||||
// Si no hay username, el usuario no está autenticado
|
||||
if (!username) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
username,
|
||||
email,
|
||||
name,
|
||||
groups: groups ? groups.split('|').filter(g => g.trim()) : [],
|
||||
uid,
|
||||
appSlug,
|
||||
outpostName,
|
||||
// Generar avatar URL usando UI Avatars
|
||||
avatar: `https://ui-avatars.com/api/?name=${encodeURIComponent(name || username)}&background=random&size=128`
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
const user = computed(() => authentikUser.value)
|
||||
const isAuthenticated = computed(() => !!user.value)
|
||||
|
||||
const logout = () => {
|
||||
// Logout completo: invalida la sesión de Authentik completamente
|
||||
// Esto cierra sesión en todas las aplicaciones
|
||||
const authentikUrl = useRuntimeConfig().public.authentikUrl || 'https://authentik.nucleoriofrio.com'
|
||||
navigateTo(`${authentikUrl}/flows/-/default/invalidation/`, { 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' } })
|
||||
}
|
||||
|
||||
const checkSessionStatus = async () => {
|
||||
const toast = useToast()
|
||||
|
||||
// Verificar si está offline primero
|
||||
if (!navigator.onLine) {
|
||||
toast.add({
|
||||
title: 'Modo Offline',
|
||||
description: 'No se puede validar sesión sin conexión',
|
||||
color: 'neutral',
|
||||
icon: 'i-heroicons-wifi'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Mostrar toast de "verificando..."
|
||||
toast.add({
|
||||
title: 'Verificando sesión...',
|
||||
description: 'Consultando estado en Authentik',
|
||||
color: 'info',
|
||||
icon: 'i-heroicons-arrow-path'
|
||||
})
|
||||
|
||||
try {
|
||||
// Consultar el endpoint de API que verifica contra Authentik
|
||||
const response = await $fetch<AuthStatusResponse>('/api/auth/status')
|
||||
|
||||
if (response.authenticated && response.user) {
|
||||
// Sesión activa en Authentik
|
||||
toast.add({
|
||||
title: 'Sesión Activa',
|
||||
description: `Conectado como: ${response.user.name || response.user.username}`,
|
||||
color: 'success',
|
||||
icon: 'i-heroicons-check-circle'
|
||||
})
|
||||
} else {
|
||||
// Sin sesión en Authentik
|
||||
toast.add({
|
||||
title: 'Sin Sesión',
|
||||
description: 'No hay sesión activa en Authentik',
|
||||
color: 'warning',
|
||||
icon: 'i-heroicons-exclamation-triangle',
|
||||
actions: [{
|
||||
label: 'Iniciar Sesión',
|
||||
onClick: () => {
|
||||
// Recargar la página forzará a Authentik a redirigir al login
|
||||
window.location.reload()
|
||||
}
|
||||
}]
|
||||
})
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
// Verificar si está offline ahora (pudo desconectarse durante la petición)
|
||||
if (!navigator.onLine) {
|
||||
toast.add({
|
||||
title: 'Modo Offline',
|
||||
description: 'No se puede validar sesión sin conexión',
|
||||
color: 'neutral',
|
||||
icon: 'i-heroicons-wifi'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Si el error es por redirect de Authentik (CORS/fetch error), significa que no hay sesión
|
||||
// Authentik redirige a login cuando no hay sesión válida, causando error CORS en fetch
|
||||
const errorMessage = (error as Error)?.message || String(error)
|
||||
const isCorsOrRedirectError = errorMessage.includes('Failed to fetch') ||
|
||||
errorMessage.includes('CORS') ||
|
||||
(error as any)?.statusCode === 302
|
||||
|
||||
if (isCorsOrRedirectError) {
|
||||
// Interpretar como sesión expirada/inválida
|
||||
toast.add({
|
||||
title: 'Sin Sesión',
|
||||
description: 'No hay sesión activa en Authentik',
|
||||
color: 'warning',
|
||||
icon: 'i-heroicons-exclamation-triangle',
|
||||
actions: [{
|
||||
label: 'Iniciar Sesión',
|
||||
onClick: () => {
|
||||
// Recargar la página forzará a Authentik a redirigir al login
|
||||
window.location.reload()
|
||||
}
|
||||
}]
|
||||
})
|
||||
} else {
|
||||
// Error real de red o servidor
|
||||
toast.add({
|
||||
title: 'Error',
|
||||
description: 'No se pudo verificar el estado de la sesión',
|
||||
color: 'error',
|
||||
icon: 'i-heroicons-x-circle'
|
||||
})
|
||||
}
|
||||
console.error('Error checking session status:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica si el usuario pertenece a un grupo específico (frontend)
|
||||
* Lee los grupos desde el estado local (headers de Authentik)
|
||||
*/
|
||||
const hasGroup = (groupName: string): boolean => {
|
||||
if (!user.value) return false
|
||||
return user.value.groups.includes(groupName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica si el usuario pertenece a un grupo específico (backend)
|
||||
* Consulta al servidor para validar contra Authentik
|
||||
*/
|
||||
const checkGroupBackend = async (groupName: string): Promise<boolean> => {
|
||||
try {
|
||||
const response = await $fetch<{ hasGroup: boolean }>(`/api/auth/check-group`, {
|
||||
method: 'POST',
|
||||
body: { groupName }
|
||||
})
|
||||
return response.hasGroup
|
||||
} catch (error) {
|
||||
console.error('Error checking group membership:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
isAuthenticated,
|
||||
logout,
|
||||
goToProfile,
|
||||
checkSessionStatus,
|
||||
hasGroup,
|
||||
checkGroupBackend
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
const { user } = useAuth()
|
||||
|
||||
definePageMeta({
|
||||
middleware: 'auth'
|
||||
})
|
||||
const { user, isAuthenticated } = useAuthentik()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -166,8 +166,7 @@ export default defineNuxtConfig({
|
||||
process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
|
||||
},
|
||||
public: {
|
||||
authentikUrl: process.env.NUXT_PUBLIC_AUTHENTIK_URL || '',
|
||||
authentikAppSlug: process.env.NUXT_PUBLIC_AUTHENTIK_APP_SLUG || ''
|
||||
authentikUrl: process.env.NUXT_PUBLIC_AUTHENTIK_URL || 'https://authentik.nucleoriofrio.com'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
export default defineEventHandler((event) => {
|
||||
const headers = getHeaders(event)
|
||||
|
||||
// Authentik envía información del usuario en headers específicos
|
||||
const user = {
|
||||
username: headers['x-authentik-username'] || null,
|
||||
email: headers['x-authentik-email'] || null,
|
||||
name: headers['x-authentik-name'] || null,
|
||||
uid: headers['x-authentik-uid'] || null,
|
||||
groups: headers['x-authentik-groups'] ? headers['x-authentik-groups'].split(',') : [],
|
||||
authenticated: !!headers['x-authentik-username']
|
||||
}
|
||||
|
||||
return user
|
||||
})
|
||||
Reference in New Issue
Block a user