Ir al contenido

Ver registros de acceso

Consulta el historial de inicios y cierres de sesión de todos los usuarios asociados a tus negocios.

  1. Navega a Configuración > Seguridad.
  2. Desplázate hasta la sección “Registros de acceso”.
  3. Revisa la tabla con el historial — muestra los últimos 100 accesos.
ColumnaDescripciónEjemplo
EventoTipo de acceso (Inicio/Cierre)Badge verde “Inicio de sesión”
NombreNombre del usuarioJuan Pérez
EmailCorreo del usuariojuan@empresa.com
Tipo UsuarioRol del usuarioPropietario, Empleado, Asesor
EmpresaNegocio asociadoMi Empresa S.L.
Fecha y HoraMomento exacto del acceso16/02/2026, 10:30:45
HaceTiempo transcurridoHace 2 horas
BadgeColorSignificado
Inicio de sesiónVerdeUsuario inició sesión
Cierre de sesiónRojoUsuario cerró sesión
TipoColorDescripción
PropietarioAzulDueño del negocio
EmpleadoGrisEmpleado o manager
AsesorBlanco/outlineAsesor externo
AdministradorRojoAdmin de Factux
SoporteGris secundarioSoporte técnico
  • 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 usuario
const { 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 negocio
let query = supabase
.from('user_access_logs')
.select('*, businesses:business_id(name)')
.order('login_at', { ascending: false })
.limit(100);
// 3. Filtrar por usuario o negocios
if (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 fecha
const 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 relativo
const 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 usuario
const 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ón
CREATE 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:

supabase/functions/track-auth-events/index.ts
interface AuthEvent {
event: string;
email: string;
ip_address?: string;
user_agent?: string;
error_message?: string;
}
// Registra eventos de login fallido
if (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 detalles
SELECT
ual.*,
b.name as business_name,
p.first_name || ' ' || p.last_name as full_name
FROM user_access_logs ual
LEFT JOIN businesses b ON ual.business_id = b.id
LEFT JOIN profiles p ON ual.user_id = p.user_id
WHERE ual.login_at >= NOW() - INTERVAL '7 days'
ORDER BY ual.login_at DESC;
-- Usuarios más activos
SELECT
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_acceso
FROM user_access_logs
WHERE login_at >= NOW() - INTERVAL '30 days'
GROUP BY user_email
ORDER BY total_accesos DESC;
-- Detección de accesos sospechosos
SELECT
user_email,
COUNT(DISTINCT ip_address) as diferentes_ips,
array_agg(DISTINCT ip_address) as ips,
COUNT(*) as total_logins
FROM user_access_logs
WHERE login_at >= NOW() - INTERVAL '24 hours'
AND event_type = 'login'
GROUP BY user_email
HAVING COUNT(DISTINCT ip_address) > 3
ORDER BY diferentes_ips DESC;