Solucionar errores de CORS manteniendo seguridad de Authentik
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:
2025-10-27 15:15:44 -06:00
parent ab0f79e103
commit 1ea50f0aa5
3 changed files with 136 additions and 11 deletions

View File

@@ -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) {

View File

@@ -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,11 +138,20 @@ export function useAuthentik() {
return;
}
// Error de red o servidor
toast.error('Error', {
description: 'No se pudo verificar el estado de la sesión',
});
console.error('Error checking session status:', error);
// 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);
}
}
};