Clean up codebase and prepare as template
All checks were successful
build-and-deploy / build (push) Successful in 2m1s
build-and-deploy / deploy (push) Successful in 4s

Remove unused files and code:
- Delete test/debug endpoints (test.get.ts, public.get.ts, user.get.ts, debug-config backup)
- Remove unused OAuth wrapper (oauth-authentik.ts)
- Clean up debug console.log statements
- Simplify code comments

Fix TypeScript errors:
- Add @types/node dependency
- Create index.d.ts with User interface extension
- Fix UButton color props (red→error, gray→neutral)
- Add type assertions in protected.get.ts

Update documentation:
- Enhance README.md as template documentation
- Update SETUP.md with correct API routes (/api/auth/* instead of /auth/*)
- Add NUXT_OAUTH_AUTHENTIK_SERVER_URL_INTERNAL documentation
- Update endpoint documentation

This commit prepares the repository to be used as a template for future Nuxt 4 + Authentik OAuth projects.
This commit is contained in:
2025-10-11 19:18:52 -06:00
parent 174e9565b8
commit b9ba7a80db
15 changed files with 158 additions and 241 deletions

100
README.md
View File

@@ -1,3 +1,99 @@
# Seguidor de Lotes # Nuxt 4 + Authentik OAuth + PWA Template
Proyecto reiniciado desde cero. Template de aplicación Nuxt 4 con autenticación OAuth/OIDC usando Authentik y funcionalidad PWA (Progressive Web App).
## 🚀 Características
-**Nuxt 4** - Framework Vue.js de última generación
-**Autenticación OAuth/OIDC** - Integración con Authentik
-**PWA** - Funciona offline con Service Workers
-**Docker** - Despliegue con Docker Compose
-**Traefik** - Configurado para reverse proxy
-**Gitea Actions** - CI/CD automático incluido
-**TypeScript** - Tipado estático
-**Nuxt UI** - Componentes UI modernos
## 📋 Requisitos Previos
- Docker y Docker Compose
- Authentik corriendo (en la misma red Docker)
- Traefik como reverse proxy
- Gitea (opcional, para CI/CD)
## 🛠️ Instalación Rápida
1. **Clona el repositorio**
```bash
git clone https://tu-repo.git
cd seguidor-lotes
```
2. **Configura variables de entorno** (ver `SETUP.md` para detalles)
3. **Despliega**
```bash
docker-compose up -d
```
## 📖 Documentación Completa
Ver [SETUP.md](./SETUP.md) para:
- Configuración detallada de Authentik
- Variables de entorno requeridas
- Configuración de Gitea Actions
- Troubleshooting
- Personalización
## 🏗️ Estructura del Proyecto
```
├── .gitea/
│ └── workflows/ # CI/CD workflows
├── nuxt4-app/
│ ├── app/
│ │ ├── components/ # Componentes Vue
│ │ ├── middleware/ # Middleware de autenticación
│ │ └── pages/ # Páginas de la aplicación
│ ├── server/
│ │ └── api/ # API endpoints
│ │ ├── auth/ # Rutas de autenticación OAuth
│ │ └── protected.get.ts # Ejemplo de API protegida
│ ├── public/ # Assets estáticos y PWA icons
│ ├── nuxt.config.ts # Configuración de Nuxt
│ └── Dockerfile # Dockerfile para producción
├── docker-compose.yml # Configuración de Docker Compose
└── SETUP.md # Guía de configuración detallada
```
## 🔐 Autenticación
El template implementa OAuth 2.0 / OpenID Connect con:
- Login con Authentik
- Protección de rutas con middleware
- Protección de APIs con `requireUserSession`
- Información de usuario (email, nombre, grupos)
- Logout completo
## 🎨 Personalización
1. **Cambiar nombre de la app**: Edita `nuxt4-app/nuxt.config.ts`
2. **Añadir páginas protegidas**: Usa `definePageMeta({ middleware: 'auth' })`
3. **Crear APIs protegidas**: Usa `requireUserSession(event)` en tus handlers
4. **Customizar UI**: Modifica componentes en `app/components/`
## 🐛 Troubleshooting
Problemas comunes y soluciones en [SETUP.md - Sección 10](./SETUP.md#10-troubleshooting)
## 📝 Licencia
MIT
## 🤝 Contribuir
Este es un template base. Siéntete libre de:
1. Fork el proyecto
2. Crear una rama para tu feature
3. Commit tus cambios
4. Push a la rama
5. Abrir un Pull Request

View File

@@ -34,7 +34,7 @@
- **Client type**: Confidential - **Client type**: Confidential
- **Client ID**: (se genera automáticamente, guárdalo) - **Client ID**: (se genera automáticamente, guárdalo)
- **Client Secret**: (se genera automáticamente, guárdalo) - **Client Secret**: (se genera automáticamente, guárdalo)
- **Redirect URIs**: `https://app.tudominio.com/auth/authentik` - **Redirect URIs**: `https://app.tudominio.com/api/auth/authentik`
- **Scopes**: openid, profile, email - **Scopes**: openid, profile, email
- **Subject mode**: Based on the User's UUID - **Subject mode**: Based on the User's UUID
- **Include claims in id_token**: ✅ Activado - **Include claims in id_token**: ✅ Activado
@@ -66,7 +66,8 @@ APP_DOMAIN=app.tudominio.com
NUXT_OAUTH_AUTHENTIK_CLIENT_ID=tu-client-id-aqui NUXT_OAUTH_AUTHENTIK_CLIENT_ID=tu-client-id-aqui
NUXT_OAUTH_AUTHENTIK_CLIENT_SECRET=tu-client-secret-aqui NUXT_OAUTH_AUTHENTIK_CLIENT_SECRET=tu-client-secret-aqui
NUXT_OAUTH_AUTHENTIK_SERVER_URL=https://auth.tudominio.com NUXT_OAUTH_AUTHENTIK_SERVER_URL=https://auth.tudominio.com
NUXT_OAUTH_AUTHENTIK_REDIRECT_URL=https://app.tudominio.com/auth/authentik NUXT_OAUTH_AUTHENTIK_REDIRECT_URL=https://app.tudominio.com/api/auth/authentik
NUXT_OAUTH_AUTHENTIK_SERVER_URL_INTERNAL=http://nombre-servicio-authentik:9000
# URL pública de la app # URL pública de la app
NUXT_PUBLIC_APP_URL=https://app.tudominio.com NUXT_PUBLIC_APP_URL=https://app.tudominio.com
@@ -158,14 +159,8 @@ docker-compose ps
### 7.2 Verificar APIs ### 7.2 Verificar APIs
```bash ```bash
# API Pública (sin auth) # API Protegida (requiere auth - debería retornar 401 sin sesión)
curl https://app.tudominio.com/api/public
# API Protegida (requiere auth - debería retornar 401)
curl https://app.tudominio.com/api/protected curl https://app.tudominio.com/api/protected
# Info de usuario (requiere auth)
curl https://app.tudominio.com/api/user
``` ```
### 7.3 Verificar PWA ### 7.3 Verificar PWA
@@ -183,19 +178,18 @@ curl https://app.tudominio.com/api/user
### Páginas ### Páginas
- `/` - Página de inicio (pública) - `/` - Página de inicio (pública)
- `/login` - Página de login (solo para usuarios no autenticados) - `/login` - Página de login
- `/dashboard` - Dashboard (requiere autenticación) - `/dashboard` - Dashboard (requiere autenticación)
- `/profile` - Perfil (requiere autenticación)
### API Endpoints ### API Endpoints
- `GET /api/public` - Endpoint público - `GET /api/protected` - Endpoint protegido (requiere autenticación)
- `GET /api/protected` - Endpoint protegido (requiere auth)
- `GET /api/user` - Información del usuario autenticado
### Auth Endpoints ### Auth Endpoints
- `GET /auth/authentik` - Inicia OAuth flow con Authentik - `GET /api/auth/authentik` - Inicia OAuth flow con Authentik y maneja callback
- `GET /auth/logout` - Cierra sesión - `GET /api/auth/logout` - Cierra sesión
## 9. Desarrollo Local ## 9. Desarrollo Local
@@ -218,8 +212,10 @@ npm run dev
``` ```
**Nota**: Para desarrollo local, necesitas: **Nota**: Para desarrollo local, necesitas:
- Configurar Authentik con redirect URI: `http://localhost:3000/auth/authentik` - Configurar Authentik con redirect URI: `http://localhost:3000/api/auth/authentik`
- O usar túnel como ngrok para https - O usar túnel como ngrok para https
- Usar la URL pública de Authentik para `NUXT_OAUTH_AUTHENTIK_SERVER_URL`
- No necesitas `NUXT_OAUTH_AUTHENTIK_SERVER_URL_INTERNAL` en desarrollo local
## 10. Troubleshooting ## 10. Troubleshooting

View File

@@ -26,7 +26,7 @@ const logout = async () => {
</div> </div>
<UButton <UButton
color="red" color="error"
variant="soft" variant="soft"
size="sm" size="sm"
@click="logout" @click="logout"

View File

@@ -88,7 +88,7 @@ onMounted(() => {
<UButton to="/dashboard" color="primary"> <UButton to="/dashboard" color="primary">
Ir al Dashboard Ir al Dashboard
</UButton> </UButton>
<UButton to="/profile" color="gray"> <UButton to="/profile" color="neutral">
Ver Perfil Ver Perfil
</UButton> </UButton>
</div> </div>

18
nuxt4-app/index.d.ts vendored Normal file
View File

@@ -0,0 +1,18 @@
// Global type declarations
declare module '#auth-utils' {
interface User {
id?: string
email?: string
name?: string
username?: string
picture?: string
groups?: string[]
}
interface UserSession {
user: User
loggedInAt: number
}
}
export {}

View File

@@ -14,6 +14,9 @@
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vue": "^3.5.22", "vue": "^3.5.22",
"vue-router": "^4.5.1" "vue-router": "^4.5.1"
},
"devDependencies": {
"@types/node": "^24.7.2"
} }
}, },
"node_modules/@adonisjs/hash": { "node_modules/@adonisjs/hash": {
@@ -5205,6 +5208,16 @@
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": {
"version": "24.7.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.2.tgz",
"integrity": "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.14.0"
}
},
"node_modules/@types/parse-path": { "node_modules/@types/parse-path": {
"version": "7.0.3", "version": "7.0.3",
"resolved": "https://registry.npmjs.org/@types/parse-path/-/parse-path-7.0.3.tgz", "resolved": "https://registry.npmjs.org/@types/parse-path/-/parse-path-7.0.3.tgz",
@@ -13680,6 +13693,13 @@
"node": ">=20.18.1" "node": ">=20.18.1"
} }
}, },
"node_modules/undici-types": {
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz",
"integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==",
"devOptional": true,
"license": "MIT"
},
"node_modules/unenv": { "node_modules/unenv": {
"version": "2.0.0-rc.21", "version": "2.0.0-rc.21",
"resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.21.tgz", "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.21.tgz",

View File

@@ -17,5 +17,8 @@
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vue": "^3.5.22", "vue": "^3.5.22",
"vue-router": "^4.5.1" "vue-router": "^4.5.1"
},
"devDependencies": {
"@types/node": "^24.7.2"
} }
} }

View File

@@ -3,15 +3,12 @@ import { withQuery } from 'ufo'
/** /**
* OAuth Authentik Login Handler * OAuth Authentik Login Handler
* Ruta: /api/auth/authentik * Handles OAuth flow: initial redirect and callback
*
* Este endpoint inicia el flujo OAuth con Authentik
*/ */
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const runtimeConfig = useRuntimeConfig(event) const runtimeConfig = useRuntimeConfig(event)
const query = getQuery(event) const query = getQuery(event)
// Configuración de Authentik
const config = { const config = {
clientId: runtimeConfig.oauth.authentik.clientId, clientId: runtimeConfig.oauth.authentik.clientId,
clientSecret: runtimeConfig.oauth.authentik.clientSecret, clientSecret: runtimeConfig.oauth.authentik.clientSecret,
@@ -21,13 +18,6 @@ export default defineEventHandler(async (event) => {
scope: ['openid', 'profile', 'email'], scope: ['openid', 'profile', 'email'],
} }
console.log('OAuth Authentik - Iniciando flujo:', {
serverUrl: config.serverUrl,
serverUrlInternal: config.serverUrlInternal,
redirectURL: config.redirectURL,
hasCode: !!query.code
})
// Handle OAuth callback // Handle OAuth callback
if (query.code) { if (query.code) {
try { try {
@@ -70,7 +60,6 @@ export default defineEventHandler(async (event) => {
loggedInAt: Date.now() loggedInAt: Date.now()
}) })
// Redirigir al dashboard después del login
return sendRedirect(event, '/') return sendRedirect(event, '/')
} catch (error: any) { } catch (error: any) {
console.error('Authentik OAuth error:', error) console.error('Authentik OAuth error:', error)
@@ -89,6 +78,5 @@ export default defineEventHandler(async (event) => {
} }
) )
console.log('Redirecting to:', authorizationUrl)
return sendRedirect(event, authorizationUrl) return sendRedirect(event, authorizationUrl)
}) })

View File

@@ -1,8 +1,6 @@
/** /**
* Logout Handler * Logout Handler
* Ruta: /api/auth/logout * Clears user session and redirects to home
*
* Limpia la sesión del usuario y redirige a la página de inicio
*/ */
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
await clearUserSession(event) await clearUserSession(event)

View File

@@ -1,7 +0,0 @@
/**
* Test endpoint
* Ruta: /api/auth/test
*/
export default defineEventHandler(() => {
return { message: 'Auth subfolder works!' }
})

View File

@@ -1,31 +0,0 @@
/**
* Debug Config Endpoint
* Ruta: /api/debug-config
*
* TEMPORAL: Para verificar que las variables de entorno se estén cargando
* BORRAR EN PRODUCCIÓN
*/
export default defineEventHandler((event) => {
const runtimeConfig = useRuntimeConfig(event)
return {
oauth: {
authentik: {
clientId: runtimeConfig.oauth.authentik.clientId ? 'SET (hidden)' : 'MISSING',
clientSecret: runtimeConfig.oauth.authentik.clientSecret ? 'SET (hidden)' : 'MISSING',
serverUrl: runtimeConfig.oauth.authentik.serverUrl || 'MISSING',
redirectURL: runtimeConfig.oauth.authentik.redirectURL || 'MISSING',
}
},
public: {
appUrl: runtimeConfig.public.appUrl || 'MISSING'
},
env: {
NODE_ENV: process.env.NODE_ENV,
// Verificar directamente las env vars
NUXT_OAUTH_AUTHENTIK_SERVER_URL: process.env.NUXT_OAUTH_AUTHENTIK_SERVER_URL || 'MISSING',
NUXT_OAUTH_AUTHENTIK_REDIRECT_URL: process.env.NUXT_OAUTH_AUTHENTIK_REDIRECT_URL || 'MISSING',
NUXT_PUBLIC_APP_URL: process.env.NUXT_PUBLIC_APP_URL || 'MISSING',
}
}
})

View File

@@ -1,21 +1,16 @@
/** /**
* Protected API Endpoint * Protected API Endpoint
* Ruta: /api/protected * Requires authentication - returns user-specific data
*
* Endpoint protegido que requiere autenticación
* Retorna datos sensibles solo para usuarios autenticados
*/ */
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
// Verificar autenticación
const session = await requireUserSession(event) const session = await requireUserSession(event)
return { return {
message: 'Datos protegidos del usuario', message: 'Datos protegidos del usuario',
user: session.user.username, user: (session.user as any).username,
data: { data: {
// Aquí puedes añadir datos específicos del usuario
lotes: [], lotes: [],
permissions: session.user.groups || [] permissions: (session.user as any).groups || []
}, },
timestamp: new Date().toISOString() timestamp: new Date().toISOString()
} }

View File

@@ -1,13 +0,0 @@
/**
* Public API Endpoint
* Ruta: /api/public
*
* Endpoint público que no requiere autenticación
* Útil para datos que deben estar disponibles offline
*/
export default defineEventHandler(async (event) => {
return {
message: 'Este endpoint es público y funciona offline',
timestamp: new Date().toISOString()
}
})

View File

@@ -1,15 +0,0 @@
/**
* Get Current User API
* Ruta: /api/user
*
* Endpoint protegido que devuelve la información del usuario autenticado
*/
export default defineEventHandler(async (event) => {
// Obtener sesión del usuario (requiere autenticación)
const session = await requireUserSession(event)
return {
user: session.user,
loggedInAt: session.loggedInAt
}
})

View File

@@ -1,131 +0,0 @@
/**
* Custom OAuth Provider for Authentik
*
* Este archivo extiende nuxt-auth-utils para soportar Authentik como provider OAuth
*/
import type { H3Event } from 'h3'
import { eventHandler, getQuery, sendRedirect } from 'h3'
import { withQuery } from 'ufo'
import { defu } from 'defu'
import { useRuntimeConfig } from '#imports'
export interface OAuthAuthentikConfig {
/**
* Authentik OAuth Client ID
* @default process.env.NUXT_OAUTH_AUTHENTIK_CLIENT_ID
*/
clientId?: string
/**
* Authentik OAuth Client Secret
* @default process.env.NUXT_OAUTH_AUTHENTIK_CLIENT_SECRET
*/
clientSecret?: string
/**
* Authentik Server URL
* @default process.env.NUXT_OAUTH_AUTHENTIK_SERVER_URL
*/
serverUrl?: string
/**
* Redirect URL
* @default process.env.NUXT_OAUTH_AUTHENTIK_REDIRECT_URL
*/
redirectURL?: string
/**
* Require email from user, adds the ['email'] scope if not present
* @default false
*/
emailRequired?: boolean
/**
* Authentik OAuth Scope
* @default ['openid', 'profile', 'email']
*/
scope?: string[]
}
export function oauthAuthentikEventHandler({
config,
onSuccess,
onError,
}: {
config?: OAuthAuthentikConfig
onSuccess: (event: H3Event, result: { user: any; tokens: any }) => Promise<void> | void
onError?: (event: H3Event, error: any) => Promise<void> | void
}) {
return eventHandler(async (event: H3Event) => {
const runtimeConfig = useRuntimeConfig(event)
// Debug: Log configuration
console.log('OAuth Authentik Config:', {
clientId: runtimeConfig.oauth.authentik.clientId ? '***' : 'MISSING',
clientSecret: runtimeConfig.oauth.authentik.clientSecret ? '***' : 'MISSING',
serverUrl: runtimeConfig.oauth.authentik.serverUrl,
redirectURL: runtimeConfig.oauth.authentik.redirectURL,
})
// Merge config with defaults
const authentikConfig = defu(config, {
clientId: runtimeConfig.oauth.authentik.clientId,
clientSecret: runtimeConfig.oauth.authentik.clientSecret,
serverUrl: runtimeConfig.oauth.authentik.serverUrl,
redirectURL: runtimeConfig.oauth.authentik.redirectURL,
scope: ['openid', 'profile', 'email'],
emailRequired: false
}) as Required<OAuthAuthentikConfig>
const query = getQuery(event)
// Handle callback
if (query.code) {
try {
// Exchange code for tokens
const tokenUrl = `${authentikConfig.serverUrl}/application/o/token/`
const tokenResponse = await $fetch(tokenUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: authentikConfig.clientId,
client_secret: authentikConfig.clientSecret,
code: query.code as string,
redirect_uri: authentikConfig.redirectURL,
}),
})
const tokens = tokenResponse as any
// Get user info
const userInfoUrl = `${authentikConfig.serverUrl}/application/o/userinfo/`
const user = await $fetch(userInfoUrl, {
headers: {
Authorization: `Bearer ${tokens.access_token}`,
},
})
return onSuccess(event, { user, tokens })
} catch (error: any) {
if (onError) return onError(event, error)
throw error
}
}
// Initial redirect to Authentik
const authorizationUrl = withQuery(
`${authentikConfig.serverUrl}/application/o/authorize/`,
{
client_id: authentikConfig.clientId,
redirect_uri: authentikConfig.redirectURL,
response_type: 'code',
scope: authentikConfig.scope.join(' '),
}
)
return sendRedirect(event, authorizationUrl)
})
}
// Export for use in defineOAuthAuthentikEventHandler
export const oauth = {
authentikEventHandler: oauthAuthentikEventHandler
}