Superadmin e impersonación
🗄️ Instalación de Base de Datos
1. Ejecutar Migraciones
Ejecuta los siguientes scripts SQL en tu base de datos Supabase:
# 1. Crear tablas y campos necesarios
psql -h [HOST] -U [USER] -d [DATABASE] -f database/migrations/001_superadmin_impersonation.sql
# 2. Crear funciones RPC
psql -h [HOST] -U [USER] -d [DATABASE] -f database/functions/superadmin_functions.sql
O usa el editor SQL de Supabase para ejecutar los archivos:
-
Tabla de impersonación y campos en tenants:
- Archivo:
database/migrations/001_superadmin_impersonation.sql - Crea:
impersonation_logs, agrega campossuspendedatenants
- Archivo:
-
Funciones RPC:
- Archivo:
database/functions/superadmin_functions.sql - Funciones:
is_superadmin,tenant_list_accessible(todos los tenants si eres superadmin),create_tenant_with_admin_user, etc. superadmin_list_all_tenantsnunca existió en la base; el listado usatenant_list_accessible.
- Archivo:
👤 Configurar Primer Superadmin
Método 1: SQL Directo
Para marcar un usuario existente como superadmin, ejecuta en Supabase SQL Editor:
-- Reemplaza '[email protected]' con el email del usuario
UPDATE auth.users
SET raw_user_meta_data = raw_user_meta_data || '{"is_super_admin": true}'::jsonb
Método 2: Función de Supabase
O crea una función helper:
CREATE OR REPLACE FUNCTION public.make_superadmin(p_email text)
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
BEGIN
UPDATE auth.users
SET raw_user_meta_data = raw_user_meta_data || '{"is_super_admin": true}'::jsonb
WHERE email = p_email;
END;
$$;
-- Usar:
Verificar
-- Verificar que el usuario es superadmin
SELECT
id,
email,
raw_user_meta_data->>'is_super_admin' as is_super_admin
FROM auth.users
🚀 Uso del Sistema
Acceder al Panel de Tenants
- Inicia sesión con un usuario superadmin
- Navega a:
/admin/tenants - Verás la lista de todos los tenants del sistema
Crear un Nuevo Tenant
- Click en "Nuevo Tenant"
- Completa el formulario:
- Nombre del Tenant: Nombre de la empresa
- Plan: free, starter, pro, enterprise
- Sector (opcional): pre-siembra módulos y la plantilla del bot
- Email del Administrador: correo del futuro admin del tenant
- Nombre del Admin: Nombre completo
Ya no se pide contraseña. El alta crea el usuario con una clave aleatoria que nadie ve y, al terminar, muestra el enlace de configuración para enviárselo al cliente (copiar o WhatsApp). Con ese enlace el administrador elige su contraseña, acepta las políticas y entra directo a los 3 pasos del onboarding (ADR 0007).
- El enlace es de un solo uso y caduca a los 7 días.
- Se puede volver a generar desde el menú "..." de la fila del tenant → Enlace de configuración. Generar uno nuevo invalida el anterior.
- Detalle técnico: tabla
tenant_onboarding_invites(solo se guarda el hash del token, cerrada aanon/authenticated), server actions enfrontend/src/actions/onboarding-invite.ts, pantalla pública en/invite/setup/[token]. La comprobación de superadmin se hace por sesión (user_is_superadmin), nunca con lo que llegue en la petición.
Nota: El admin se crea en Auth en el mismo paso (createTenantWithAdmin) con
user_metadata.invited_flow: true, para que el trigger de signup no le abra un
tenant personal extra. Detalle: Alta de usuarios e invited_flow.
Impersonar un Tenant
Hay dos formas de acceder como administrador de un tenant:
Opción 1: Desde la Lista
- En la lista de tenants, click en el ícono de login (🔓)
- Serás redirigido al dashboard como si fueras el admin del tenant
- Aparecerá arriba el banner de impersonación, con el ambiente en un distintivo y el banner entero de su color (ver Ambiente en el banner)
Opción 2: Desde el Menú
- Click en el botón "..." en la fila del tenant
- Selecciona "Acceder como Admin"
Salir de la Impersonación
- Click en el botón "Salir" del banner de impersonación
- Serás redirigido de vuelta al panel de tenants
Ambiente en el banner
Para no cambiar nada en el ambiente equivocado, el banner de impersonación y el de "Entrar al tenant" muestran un distintivo con el ambiente. En la impersonación, además, el banner entero toma su color:
| Distintivo | Color | Cuándo |
|---|---|---|
| PRODUCCIÓN (pulsa) | Rojo | app.xenpia.com, cualquier host desconocido, o cualquier host cuya NEXT_PUBLIC_SUPABASE_URL sea la base de producción |
| TEST | Ámbar | Host con test, qa o staging como etiqueta (test.xenpia.com, cliente-test…) |
| LOCAL | Verde | localhost, 127.*, *.localhost o IP privada |
Si el host y la base no coinciden, el distintivo lo dice (p. ej. Local · app en Local, base
de Test). Un frontend local apuntando a la base de producción sale en rojo: los cambios
caen en producción. La regla está en frontend/src/utils/app-environment.ts (con test); si
cambia el ref de un proyecto Supabase, hay que actualizarlo ahí.
Entrar al tenant (sin impersonar)
Distinto de impersonar: el JWT sigue siendo el del superadmin y no hay membership
en ese tenant. El menú se carga elevado; las mutaciones de usuarios van por RPC
SECURITY DEFINER que aceptan is_superadmin():
- cambiar rol →
update_membership_role - quitar del tenant →
remove_user_from_tenant(migración20261008000001)
Si la baja falla con toast genérico y la migración no está aplicada, el superadmin
elevated no tiene permiso (user_has_permission exige membership).
🎯 Funcionalidades
Panel de Gestión de Tenants
Vista Principal (/admin/tenants):
- ✅ Lista de todos los tenants
- ✅ Búsqueda y filtros
- ✅ Ver cantidad de usuarios y workspaces por tenant
- ✅ Ver estado (activo/suspendido)
- ✅ Ver plan actual (sale de la suscripción espejada de Odoo, no de
tenants)
Acciones Disponibles:
- ✅ Crear nuevo tenant
- ✅ Editar tenant (nombre) y su plan (tarjeta Plan → se guarda en Odoo)
- ✅ Suspender/reactivar tenant
- ✅ Acceder como admin (impersonación)
Sistema de Impersonación
Características:
- ✅ Banner visual cuando estás impersonando
- ✅ Muestra nombre del tenant y hora de inicio
- ✅ Botón rápido para salir
- ✅ Log completo de todas las impersonaciones
- ✅ Acceso completo como si fueras el admin del tenant
Logs de Auditoría:
Todas las impersonaciones quedan registradas en:
SELECT
il.id,
il.started_at,
il.ended_at,
p.full_name as superadmin_name,
t.name as tenant_name,
il.reason
FROM impersonation_logs il
JOIN profiles p ON p.id = il.superadmin_id
JOIN tenants t ON t.id = il.target_tenant_id
ORDER BY il.started_at DESC;
🔐 Seguridad
Políticas RLS
El sistema usa RLS (Row Level Security) simple:
- Todos los usuarios autenticados pueden leer/escribir (como especificaste)
- La verificación de superadmin se hace a nivel de aplicación y funciones RPC
Verificaciones
En el Backend (RPCs):
-- Todas las funciones RPC verifican:
IF NOT public.is_superadmin() THEN
RAISE EXCEPTION 'Access denied. Superadmin privileges required.';
END IF;
En el Frontend:
// Guard de rutas
<SuperadminGuard>
{children}
</SuperadminGuard>
// En componentes
const { is_super_admin } = useAuthContext();
if (!is_super_admin) return null;
Auditoría
Todas las acciones de superadmin quedan registradas en:
impersonation_logs: Log de impersonacionesaudit_logs: Log general de acciones
-- Ver acciones de un superadmin
SELECT
action,
object_type,
meta,
created_at
FROM audit_logs
WHERE user_id = '[SUPERADMIN_ID]'
ORDER BY created_at DESC;
📝 Flujo Completo de Impersonación
1. Superadmin → Click "Acceder" en tenant
↓
2. Frontend → startImpersonation(tenant_id)
↓
3. Backend RPC → Verificar is_superadmin
↓
4. Backend RPC → Crear registro en impersonation_logs
↓
5. Backend RPC → Devolver tenant_id, workspace_id, role_id
↓
6. Frontend → Actualizar contexto de usuario
↓
7. Frontend → Mostrar banner de impersonación
↓
8. Frontend → Redirigir a /dashboard
↓
9. Usuario navega como admin del tenant
↓
10. Usuario → Click "Salir" en banner
↓
11. Frontend → endImpersonation(impersonation_id)
↓
12. Backend RPC → Marcar ended_at en log
↓
13. Frontend → Recargar página → Volver a /admin/tenants
🎨 Rutas del Sistema
/admin/tenants → Lista de tenants (solo superadmin)
/admin/tenants/new → Crear nuevo tenant
/admin/tenants/[id]/edit → Editar tenant
🧪 Testing
1. Crear Usuario Superadmin de Prueba
-- Crear usuario superadmin de prueba
UPDATE auth.users
SET raw_user_meta_data = raw_user_meta_data || '{"is_super_admin": true}'::jsonb
2. Verificar Funciones RPC
-- Verificar que funciona is_superadmin
SELECT is_superadmin();
-- Listar todos los tenants (como superadmin)
SELECT * FROM tenant_list_accessible();
3. Probar Impersonación
- Login como superadmin
- Ir a
/admin/tenants - Click en "Acceder" en un tenant
- Verificar que aparece el banner
- Navegar por el dashboard
- Click "Salir" para terminar
⚠️ Notas Importantes
-
Admin del tenant nuevo:
createTenantWithAdmincrea el usuario de Auth (coninvited_flow: true) y luego el tenant. No hace falta registrarlo antes. Si el email ya existe, el alta falla a propósito para no duplicar memberships. -
Impersonación persiste en sesión: La impersonación se mantiene mientras no cierres sesión o hagas logout.
-
RLS simple: Como pediste, las políticas RLS son simples (authenticated = true). La seguridad real está en las funciones RPC.
-
Logs de auditoría: Todas las acciones quedan registradas. Revisa periódicamente los logs.
🐛 Troubleshooting
Error: "Access denied. Superadmin privileges required"
Causa: El usuario no tiene is_super_admin: true en su metadata.
Solución:
UPDATE auth.users
SET raw_user_meta_data = raw_user_meta_data || '{"is_super_admin": true}'::jsonb
Error: "El email ya está registrado"
Causa: Al crear un tenant, el email del admin ya existe en auth.users.
Solución: Usa otro correo. Reutilizar un usuario existente le daría una segunda membership. Ver Alta de usuarios e invited_flow.
Banner de impersonación no aparece
Causa: La impersonación no se detectó correctamente.
Solución:
- Verifica que hay un registro activo en
impersonation_logs:
SELECT * FROM impersonation_logs WHERE ended_at IS NULL;
- Refresca la página (F5)
En “Entrar al tenant” no se pueden borrar usuarios
Causa: remove_user_from_tenant no dejaba pasar a un superadmin sin membership.
Solución: aplicar supabase/migrations/20261008000001_remove_user_from_tenant_superadmin.sql.
El botón ya estaba; fallaba la RPC.
Al crear un usuario aparece un tenant extra / al impersonar el menú está vacío
Causa: createUser se llamó sin invited_flow: true. El trigger de signup creó un tenant
personal y el usuario quedó con dos memberships.
Solución: las altas desde la UI ya pasan el flag. Para usuarios ya rotos, impersonar
sigue funcionando (gana el tenant del log de impersonación). Limpieza de tenants
huérfanos: scripts backend/scripts/repair-*-memberships.ts. Documentación:
Alta de usuarios e invited_flow.
📞 Soporte
Si tienes dudas o problemas:
- Revisa los logs del navegador (F12 → Console)
- Revisa los logs de Supabase (Logs & Analytics)
- Verifica las tablas de auditoría (
impersonation_logs,audit_logs)
¡Sistema de Superadmin configurado exitosamente! 🎉