Ver registros de acceso
Exportar contenido
Consulta el historial de inicios y cierres de sesión de todos los usuarios asociados a tus negocios.
- Navega a Configuración > Seguridad.
- Desplázate hasta la sección “Registros de acceso”.
- Revisa la tabla con el historial — muestra los últimos 100 accesos.
Información mostrada
Sección titulada «Información mostrada»| Columna | Descripción | Ejemplo |
|---|---|---|
| Evento | Tipo de acceso (Inicio/Cierre) | Badge verde “Inicio de sesión” |
| Nombre | Nombre del usuario | Juan Pérez |
| Correo del usuario | juan@empresa.com | |
| Tipo Usuario | Rol del usuario | Propietario, Empleado, Asesor |
| Empresa | Negocio asociado | Mi Empresa S.L. |
| Fecha y Hora | Momento exacto del acceso | 16/02/2026, 10:30:45 |
| Hace | Tiempo transcurrido | Hace 2 horas |
Badges de evento
Sección titulada «Badges de evento»| Badge | Color | Significado |
|---|---|---|
| Inicio de sesión | Verde | Usuario inició sesión |
| Cierre de sesión | Rojo | Usuario cerró sesión |
Badges de tipo de usuario
Sección titulada «Badges de tipo de usuario»| Tipo | Color | Descripción |
|---|---|---|
| Propietario | Azul | Dueño del negocio |
| Empleado | Gris | Empleado o manager |
| Asesor | Blanco/outline | Asesor externo |
| Administrador | Rojo | Admin de Factux |
| Soporte | Gris secundario | Soporte técnico |
Alcance de los registros
Sección titulada «Alcance de los registros»- Propietarios: Ven todos los accesos de sus negocios
- Usuarios: Ven solo sus propios accesos
- Límite: Últimos 100 registros
- Ordenación: Más recientes primero
Detalles técnicos — Ver registros de acceso
Sección titulada «Detalles técnicos — Ver registros de acceso»Implementación de registros
Componente: AccessLogsTable.tsx
Ubicación: /src/panels/admin/components/AccessLogsTable.tsx
Query principal:
// 1. Obtener negocios del usuarioconst { data: userBusinesses } = await supabase .from('user_businesses') .select('business_id') .eq('user_id', targetUserId);
const businessIds = userBusinesses?.map(ub => ub.business_id) || [];
// 2. Cargar logs con información del negociolet query = supabase .from('user_access_logs') .select('*, businesses:business_id(name)') .order('login_at', { ascending: false }) .limit(100);
// 3. Filtrar por usuario o negociosif (businessIds.length > 0) { query = query.or(`user_id.eq.${targetUserId},business_id.in.(${businessIds.join(',')})`);} else { query = query.eq('user_id', targetUserId);}Interfaz de datos:
interface AccessLog { id: string; user_name: string; user_email: string; user_type: string; event_type: 'login' | 'logout'; login_at: string; business_id?: string; business_name?: string;}
type EventTypeFilter = 'all' | 'login' | 'logout';type UserTypeFilter = 'all' | 'owner' | 'manager' | 'employee' | 'advisor' | 'admin' | 'support';Funciones de formato:
// Formato de fechaconst formatDate = (dateString: string) => { const date = new Date(dateString); return date.toLocaleString('es-ES', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' });};
// Tiempo relativoconst getTimeAgo = (dateString: string) => { const now = new Date(); const date = new Date(dateString); const diffInMinutes = Math.floor((now.getTime() - date.getTime()) / (1000 * 60));
if (diffInMinutes < 1) return 'Ahora mismo'; if (diffInMinutes < 60) return `Hace ${diffInMinutes} minutos`;
const diffInHours = Math.floor(diffInMinutes / 60); if (diffInHours < 24) return `Hace ${diffInHours} horas`;
const diffInDays = Math.floor(diffInHours / 24); return `Hace ${diffInDays} días`;};
// Etiquetas de usuarioconst getUserTypeLabel = (userType: string) => { switch (userType) { case 'owner': return 'Propietario'; case 'manager': return 'Empleado'; case 'employee': return 'Empleado'; case 'advisor': return 'Asesor'; case 'admin': return 'Administrador'; case 'support': return 'Soporte'; default: return userType || 'Desconocido'; }};Tabla de base de datos:
CREATE TABLE IF NOT EXISTS public.user_access_logs ( id UUID DEFAULT gen_random_uuid() NOT NULL, user_id UUID NOT NULL, business_id UUID, user_name TEXT NOT NULL, user_email TEXT NOT NULL, user_type TEXT NOT NULL, login_at TIMESTAMPTZ DEFAULT now() NOT NULL, ip_address INET, user_agent TEXT, session_id TEXT, event_type TEXT NOT NULL DEFAULT 'login', created_at TIMESTAMPTZ DEFAULT now(),
CONSTRAINT user_access_logs_event_type_check CHECK (event_type IN ('login', 'logout')));
-- Índices para optimizaciónCREATE INDEX idx_user_access_logs_user_id ON user_access_logs(user_id);CREATE INDEX idx_user_access_logs_business_id ON user_access_logs(business_id);CREATE INDEX idx_user_access_logs_login_at ON user_access_logs(login_at);CREATE INDEX idx_user_access_logs_event_type ON user_access_logs(event_type);Edge Function de tracking:
interface AuthEvent { event: string; email: string; ip_address?: string; user_agent?: string; error_message?: string;}
// Registra eventos de login fallidoif (event === 'user.login.failed') { await supabaseAdmin.rpc('handle_failed_login_attempt', { p_email: email, p_ip_address: ip_address, p_user_agent: user_agent, p_failure_reason: error_message || 'invalid_credentials' });}Query de diagnóstico:
-- Accesos recientes con detallesSELECT ual.*, b.name as business_name, p.first_name || ' ' || p.last_name as full_nameFROM user_access_logs ualLEFT JOIN businesses b ON ual.business_id = b.idLEFT JOIN profiles p ON ual.user_id = p.user_idWHERE ual.login_at >= NOW() - INTERVAL '7 days'ORDER BY ual.login_at DESC;
-- Usuarios más activosSELECT user_email, COUNT(*) as total_accesos, COUNT(CASE WHEN event_type = 'login' THEN 1 END) as logins, COUNT(CASE WHEN event_type = 'logout' THEN 1 END) as logouts, MAX(login_at) as ultimo_accesoFROM user_access_logsWHERE login_at >= NOW() - INTERVAL '30 days'GROUP BY user_emailORDER BY total_accesos DESC;
-- Detección de accesos sospechososSELECT user_email, COUNT(DISTINCT ip_address) as diferentes_ips, array_agg(DISTINCT ip_address) as ips, COUNT(*) as total_loginsFROM user_access_logsWHERE login_at >= NOW() - INTERVAL '24 hours' AND event_type = 'login'GROUP BY user_emailHAVING COUNT(DISTINCT ip_address) > 3ORDER BY diferentes_ips DESC;