Solucionar errores de CORS manteniendo seguridad de Authentik
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 46s
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 46s
PROBLEMA: - Frontend hacía fetch a APIs protegidas por Authentik - Cuando sesión expiraba, Authentik redirigía causando error de CORS - TypeError: Failed to fetch SOLUCIÓN: 1. Backend inyecta window.__AUTHENTIK_USER__ en HTML inicial (app.js) - Servidor lee headers de Authentik y los pasa al frontend - Evita fetch innecesario en carga inicial 2. Frontend usa window.__AUTHENTIK_USER__ como fuente principal (useAuthentik.js) - Solo hace fetch cuando se fuerza refresh - Detecta errores de CORS como señal de sesión expirada - Muestra mensaje claro al usuario 3. App.vue detecta errores de autenticación en APIs - Cuando fetch falla con CORS, recarga página automáticamente - Authentik manejará la re-autenticación SEGURIDAD: - Todos los endpoints /api/* siguen protegidos por Authentik - No se exponen APIs sin autenticación - Headers de Authentik solo presentes con sesión válida
This commit is contained in:
@@ -188,6 +188,17 @@ const userExpanded = reactive({});
|
||||
const deviceExpanded = reactive({});
|
||||
// formulario inline removido: se usa modal con UserForm
|
||||
|
||||
// Helper para detectar errores de autenticación
|
||||
function isAuthError(error) {
|
||||
// Si es un TypeError de fetch, probablemente es CORS (redirección de Authentik)
|
||||
return error instanceof TypeError && error.message.includes('fetch');
|
||||
}
|
||||
|
||||
function handleAuthError() {
|
||||
console.warn('Sesión expirada o error de autenticación, redirigiendo...');
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
const showEventFilters = ref(false);
|
||||
const showUserFilters = ref(false);
|
||||
const eventFilters = reactive({ text: '', type: '' });
|
||||
@@ -203,8 +214,17 @@ async function fetchUsers() {
|
||||
loading.users = true;
|
||||
try {
|
||||
const res = await fetch('/api/users');
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
||||
}
|
||||
const data = await res.json();
|
||||
users.value = data.items || [];
|
||||
} catch (error) {
|
||||
if (isAuthError(error)) {
|
||||
handleAuthError();
|
||||
} else {
|
||||
console.error('Error fetching users:', error);
|
||||
}
|
||||
} finally { loading.users = false; }
|
||||
}
|
||||
|
||||
@@ -212,17 +232,35 @@ async function fetchRequests() {
|
||||
loading.requests = true;
|
||||
try {
|
||||
const res = await fetch('/api/requests');
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
||||
}
|
||||
const data = await res.json();
|
||||
requests.value = data.items || [];
|
||||
} catch (error) {
|
||||
if (isAuthError(error)) {
|
||||
handleAuthError();
|
||||
} else {
|
||||
console.error('Error fetching requests:', error);
|
||||
}
|
||||
} finally { loading.requests = false; }
|
||||
}
|
||||
|
||||
async function fetchDevices() {
|
||||
try {
|
||||
const res = await fetch('/api/devices');
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
||||
}
|
||||
const data = await res.json();
|
||||
devices.value = data.items || [];
|
||||
} catch {}
|
||||
} catch (error) {
|
||||
if (isAuthError(error)) {
|
||||
handleAuthError();
|
||||
} else {
|
||||
console.error('Error fetching devices:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleDisable(u) {
|
||||
|
||||
@@ -11,17 +11,53 @@ import { useToast } from './useToast.js';
|
||||
const authentikUser = ref(null);
|
||||
const isLoading = ref(false);
|
||||
|
||||
// Flag para indicar si ya se intentó cargar desde window
|
||||
let initialLoadAttempted = false;
|
||||
|
||||
export function useAuthentik() {
|
||||
const { toast } = useToast();
|
||||
|
||||
/**
|
||||
* Obtener datos del usuario desde el backend
|
||||
* El backend lee los headers inyectados por Authentik Proxy Outpost
|
||||
* Obtener datos del usuario desde el backend o desde window.__AUTHENTIK_USER__
|
||||
* El backend inyecta esta información en el HTML al cargarlo
|
||||
*/
|
||||
const fetchUserData = async () => {
|
||||
const fetchUserData = async (forceRefresh = false) => {
|
||||
// Primero intentar cargar desde window.__AUTHENTIK_USER__ (inyectado por el servidor)
|
||||
if (!initialLoadAttempted && typeof window !== 'undefined' && window.__AUTHENTIK_USER__) {
|
||||
initialLoadAttempted = true;
|
||||
const data = window.__AUTHENTIK_USER__;
|
||||
|
||||
if (data.authenticated && data.user) {
|
||||
// Generar avatar URL usando UI Avatars
|
||||
const avatarName = encodeURIComponent(data.user.name || data.user.username);
|
||||
data.user.avatar = `${AVATAR_CONFIG.provider}?name=${avatarName}&background=${AVATAR_CONFIG.background}&size=${AVATAR_CONFIG.defaultSize}`;
|
||||
|
||||
authentikUser.value = data.user;
|
||||
} else {
|
||||
authentikUser.value = null;
|
||||
}
|
||||
|
||||
// Si window.__AUTHENTIK_USER__ indica que no hay autenticación, no hacer fetch adicional
|
||||
// a menos que se fuerce el refresh
|
||||
if (!forceRefresh && !data.authenticated) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Solo hacer fetch si se fuerza o si no hay datos iniciales
|
||||
if (!forceRefresh && initialLoadAttempted && authentikUser.value === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Si no hay datos inyectados o se fuerza el refresh, hacer fetch al backend
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const response = await fetch('/api/auth/status');
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.authenticated && data.user) {
|
||||
@@ -75,6 +111,11 @@ export function useAuthentik() {
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/auth/status');
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.authenticated && data.user) {
|
||||
@@ -97,12 +138,21 @@ export function useAuthentik() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Error de red o servidor
|
||||
// Error de fetch generalmente indica CORS (redirección de Authentik)
|
||||
// Esto significa que la sesión expiró o no existe
|
||||
if (error instanceof TypeError && error.message.includes('fetch')) {
|
||||
toast.warning('Sesión Expirada', {
|
||||
description: 'Tu sesión ha expirado. Recarga la página para autenticarte nuevamente.',
|
||||
});
|
||||
console.warn('Session expired or CORS error:', error);
|
||||
} else {
|
||||
// Otro tipo de error
|
||||
toast.error('Error', {
|
||||
description: 'No se pudo verificar el estado de la sesión',
|
||||
});
|
||||
console.error('Error checking session status:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,7 @@ import express from 'express';
|
||||
import morgan from 'morgan';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { readFileSync } from 'fs';
|
||||
import apiRouter from './routes/api.js';
|
||||
import radiusRouter from './routes/radius.js';
|
||||
import authRouter from './routes/auth.js';
|
||||
@@ -27,8 +28,44 @@ export function createApp() {
|
||||
// Simple health endpoint for reverse proxies / checks
|
||||
app.get('/healthz', (_req, res) => res.json({ ok: true }));
|
||||
|
||||
app.get('/', (_req, res) => {
|
||||
res.sendFile(path.join(__dirname, '..', 'public', 'index.html'));
|
||||
// Servir index.html con información de usuario inyectada
|
||||
app.get('/', (req, res) => {
|
||||
const indexPath = path.join(__dirname, '..', 'public', 'index.html');
|
||||
let html = readFileSync(indexPath, 'utf-8');
|
||||
|
||||
// Leer headers de Authentik
|
||||
const username = req.headers['x-authentik-username'];
|
||||
const email = req.headers['x-authentik-email'];
|
||||
const name = req.headers['x-authentik-name'];
|
||||
const groups = req.headers['x-authentik-groups'];
|
||||
const uid = req.headers['x-authentik-uid'];
|
||||
|
||||
// Crear objeto de usuario
|
||||
const userData = username ? {
|
||||
authenticated: true,
|
||||
user: {
|
||||
username,
|
||||
email,
|
||||
name,
|
||||
groups: groups ? groups.split('|').filter(g => g.trim()) : [],
|
||||
uid
|
||||
}
|
||||
} : {
|
||||
authenticated: false,
|
||||
user: null
|
||||
};
|
||||
|
||||
// Inyectar en el HTML antes de </head>
|
||||
const injection = `
|
||||
<script>
|
||||
window.__AUTHENTIK_USER__ = ${JSON.stringify(userData)};
|
||||
</script>
|
||||
</head>`;
|
||||
|
||||
html = html.replace('</head>', injection);
|
||||
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
res.send(html);
|
||||
});
|
||||
|
||||
return app;
|
||||
|
||||
Reference in New Issue
Block a user