Clean up codebase and prepare as template
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:
100
README.md
100
README.md
@@ -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
|
||||||
|
|||||||
28
SETUP.md
28
SETUP.md
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
18
nuxt4-app/index.d.ts
vendored
Normal 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 {}
|
||||||
20
nuxt4-app/package-lock.json
generated
20
nuxt4-app/package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
/**
|
|
||||||
* Test endpoint
|
|
||||||
* Ruta: /api/auth/test
|
|
||||||
*/
|
|
||||||
export default defineEventHandler(() => {
|
|
||||||
return { message: 'Auth subfolder works!' }
|
|
||||||
})
|
|
||||||
@@ -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',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user