Feat: Agregar middleware global de autenticación API
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m9s
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m9s
- Valida Bearer token para rutas expuestas (/api/messages/send, /api/mcp) - Valida x-authentik-username para rutas protegidas por Authentik - Rutas públicas (/api/health) sin autenticación - Defensa en profundidad: cualquier endpoint /api/* está protegido
This commit is contained in:
96
server/middleware/01.api-auth.ts
Normal file
96
server/middleware/01.api-auth.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
/**
|
||||||
|
* Middleware de autenticación para API
|
||||||
|
*
|
||||||
|
* Asegura que TODAS las rutas /api/* estén protegidas:
|
||||||
|
* - Rutas con Authentik: Requieren header x-authentik-username
|
||||||
|
* - Rutas sin Authentik (expuestas): Requieren Bearer token válido
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { query } from '../utils/database'
|
||||||
|
|
||||||
|
// Rutas que NO pasan por Authentik (expuestas con API Key)
|
||||||
|
const API_KEY_ROUTES = [
|
||||||
|
'/api/messages/send',
|
||||||
|
'/api/mcp'
|
||||||
|
]
|
||||||
|
|
||||||
|
// Rutas públicas (sin autenticación)
|
||||||
|
const PUBLIC_ROUTES = [
|
||||||
|
'/api/health'
|
||||||
|
]
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const path = getRequestURL(event).pathname
|
||||||
|
|
||||||
|
// Solo aplicar a rutas /api/*
|
||||||
|
if (!path.startsWith('/api/')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rutas públicas - sin autenticación
|
||||||
|
if (PUBLIC_ROUTES.some(route => path.startsWith(route))) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar si es ruta expuesta (sin Authentik)
|
||||||
|
const isExposedRoute = API_KEY_ROUTES.some(route => path.startsWith(route))
|
||||||
|
|
||||||
|
if (isExposedRoute) {
|
||||||
|
// Ruta expuesta: Requiere Bearer token
|
||||||
|
const authHeader = getHeader(event, 'authorization')
|
||||||
|
|
||||||
|
if (!authHeader?.startsWith('Bearer ')) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 401,
|
||||||
|
message: 'Authorization Bearer token required'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = authHeader.slice(7)
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
|
||||||
|
// Validar contra Master API Key
|
||||||
|
if (config.masterApiKey && token === config.masterApiKey) {
|
||||||
|
// Token válido - continuar
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validar contra API Keys en DB
|
||||||
|
const crypto = await import('crypto')
|
||||||
|
const keyHash = crypto.createHash('sha256').update(token).digest('hex')
|
||||||
|
|
||||||
|
const keyResult = await query(
|
||||||
|
`SELECT id FROM api_keys
|
||||||
|
WHERE key_hash = $1 AND is_active = TRUE
|
||||||
|
AND (expires_at IS NULL OR expires_at > NOW())`,
|
||||||
|
[keyHash]
|
||||||
|
)
|
||||||
|
|
||||||
|
if (keyResult.rows.length === 0) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 401,
|
||||||
|
message: 'Invalid API Key'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token válido - actualizar last_used
|
||||||
|
await query(
|
||||||
|
'UPDATE api_keys SET last_used_at = NOW() WHERE id = $1',
|
||||||
|
[keyResult.rows[0].id]
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ruta protegida por Authentik: Requiere header x-authentik-username
|
||||||
|
const authentikUser = getHeader(event, 'x-authentik-username')
|
||||||
|
|
||||||
|
if (!authentikUser) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 401,
|
||||||
|
message: 'Authentication required'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usuario autenticado - continuar
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user