Skip to content

Auth y RBAC ​

SipSop usa Better Auth con el plugin de organizaciones para autenticación y multi-tenancy. Esta página documenta el modelo de datos, los cuatro roles principales, sub-roles MSP, permisos, y cómo crear o gestionar usuarios.

Visión general de la arquitectura ​

Organización de Better Auth (1 org = 1 tenant)
    └── filas member (userId + organizationId + role)
    └── filas invitation (invitaciones org pendientes)

user.role (rol global — almacenado en la fila del usuario)
    → 'admin_msp'    — Equipo interno de Sopinf Tech LLC
    → 'client_admin' — Dueño del tenant / contacto de billing
    → 'client_user'  — Empleado del tenant
    → 'seller'       — Sales rep externo (solo portal de vendedores)

user.msp_roles (array — solo relevante cuando user.role = 'admin_msp')
    → ['super_admin', 'billing_clerk', 'technician', 'sales_rep']

Roles principales ​

admin_msp ​

Equipo interno de Sopinf Tech LLC. Puede acceder a la sección /admin/* del portal. Requerido para llegar a cualquier endpoint adminProcedure en la API.

  • Seteado por: manualmente en la DB (UPDATE "user" SET role = 'admin_msp' WHERE email = 'x') o via el flujo de invitación del equipo MSP (tabla msp_invitations).
  • No está scoped a una org — los usuarios admin_msp pueden ver todos los tenants cross-org.
  • Los sub-roles (msp_roles array) restringen adicionalmente qué acciones admin pueden realizar.

client_admin ​

El contacto principal/dueño de un tenant. Acceso completo al portal de su tenant: CDR, billing, gestión de equipo, settings. Puede invitar miembros client_user via el flujo de invitación org de Better Auth.

  • Creado durante el onboarding del tenant.
  • member.role = 'owner' o 'admin' en la tabla member de Better Auth.

client_user ​

Un empleado del tenant. Acceso de solo lectura a dashboard y CDR. No puede gestionar billing ni equipo.

  • Invitado por client_admin via la página de equipo del portal.
  • member.role = 'member' en la tabla member de Better Auth.

seller ​

Sales rep externo con acceso solo al portal de vendedores (/seller/* routes). Sin acceso al portal principal ni a la sección admin.

  • Otorgado via invitación por magic-link (ver Invitaciones de vendedores más abajo).
  • Tiene una fila correspondiente en commission_reps vinculada via commission_reps.user_id.
  • Auth verificado por sellerProcedure: requiere user.role = 'seller' Y una fila commission_reps activa.

Sub-roles MSP (solo admin_msp) ​

Los sub-roles MSP se almacenan como array de texto en user.msp_roles. Un super_admin puede tener todos los roles. Los sub-roles se almacenan en msp_role_definitions y se mapean a permisos via msp_role_permissions.

Sub-rol MSPPermisos típicos
super_adminTodos los permisos. Puede cambiar carrier mode, gestionar todas las funciones admin.
billing_clerkCRUD en fees, compliance dashboard, reportes trimestrales, invoice line items.
sales_repLeer tenants, leer leads, gestionar comisiones.
technicianLeer tenants, gestionar PBX/inventario (Fase 11).

Los sub-roles se verifican en trpc.ts via makeMspRoleProcedure(allowed):

ts
export const billingClerkProcedure = makeMspRoleProcedure(['super_admin', 'billing_clerk'])
export const salesProcedure = makeMspRoleProcedure(['super_admin', 'sales_rep'])
export const technicianProcedure = makeMspRoleProcedure(['super_admin', 'technician'])
export const superAdminProcedure = adminProcedure.use(async ({ ctx, next }) => {
  if (!ctx.mspRoles.includes('super_admin')) throw new TRPCError(...)
  ...
})

Sistema de permisos ​

Los permisos se cargan en createContext como un Set<string> (ctx.mspPermissions). Se calculan como la unión de todos los permisos otorgados a los sub-roles MSP del usuario.

Permisos conocidos (desde la config de navegación):

Permission keyUsado por
tenants.readVer lista y detalle de tenants
leads.readVer leads
tenant_requests.readVer y gestionar tenant requests
impersonation.read_logVer log de impersonation
team.readVer y gestionar equipo MSP
roles.readVer y gestionar roles MSP
commissions.manageAcceso completo al admin de comisiones

Los permisos se verifican en la API via requirePermission(permissionKey):

ts
export function requirePermission(permissionKey: string) {
  return adminProcedure.use(async ({ ctx, next }) => {
    if (!ctx.mspPermissions.has(permissionKey)) {
      throw new TRPCError({ code: 'FORBIDDEN', message: 'MISSING_PERMISSION' })
    }
    return next({ ctx })
  })
}

Tipos de procedure tRPC ​

ProcedureRequisito de authTenant gate
publicProcedureNingunoNinguno
protectedProcedureSesión válidaSí — bloquea suspended/cancelled
billingProcedureSesión válidaNo — permite tenants suspended para pagar
adminProcedureSesión + role = 'admin_msp'No (admin es cross-tenant)
superAdminProcedureadminProcedure + 'super_admin' en msp_rolesNo
billingClerkProcedureadminProcedure + billing_clerk o super_adminNo
salesProcedureadminProcedure + sales_rep o super_adminNo
technicianProcedureadminProcedure + technician o super_adminNo
sellerProcedureprotectedProcedure + role=seller + commission_rep activoEspecífico de seller
tenantAdminProcedureprotectedProcedure + activeMembershipRole=client_adminSí
impersonationAwareProcedureprotectedProcedure + NO en sesión de impersonationSí

Tablas de base de datos ​

user (gestionado por Better Auth) ​

id                 — text PK (Better Auth lo genera)
name               — nombre de display
email              — único
email_verified     — boolean
role               — 'admin_msp' | 'client_admin' | 'client_user' | 'seller'
msp_roles          — text[] (sub-roles para usuarios admin_msp)
is_active          — boolean (soft delete)
deactivated_at     — timestamp (cuándo fue desactivado)

organization (gestionado por Better Auth) ​

id        — text PK
name      — nombre de display del tenant
slug      — slug único URL-safe

member (gestionado por Better Auth) ​

organization_id   — FK → organization
user_id           — FK → user
role              — 'owner' | 'admin' | 'member' (roles de Better Auth dentro del org)

msp_role_definitions ​

key          — PK: 'super_admin' | 'billing_clerk' | 'sales_rep' | 'technician'
label        — label de display
description  — descripción
color        — color del badge en UI
is_system    — boolean (los roles de sistema no pueden borrarse)

msp_permissions ​

key         — PK: 'tenants.read', 'commissions.manage', etc.
category    — agrupa permisos en el editor de roles en la UI
label       — label de display

msp_role_permissions ​

role_key        — FK → msp_role_definitions.key
permission_key  — FK → msp_permissions.key

(PK compuesta en ambas columnas)

Tenant gate ​

checkTenantGate(tenantStatus) en apps/api/src/middleware/tenant-gate.ts:

Estado del tenantEfecto
activeAcceso completo
past_dueAcceso concedido, isPastDue = true. El portal muestra un banner rojo. CDR y billing son accesibles.
suspendedTenantGateError — protectedProcedure lanza FORBIDDEN. Solo funciona billingProcedure (para pagar). El portal muestra un bloqueo de pantalla completa.
cancelledIgual que suspended

Crear un nuevo usuario admin_msp ​

  1. Registrar al usuario a través del login del portal (email + contraseña).
  2. Conectarse a la base de datos y ejecutar:
sql
UPDATE "user"
SET role = 'admin_msp',
    msp_roles = ARRAY['billing_clerk']::text[]
WHERE email = 'nuevo@sopinf.com';

O dar acceso completo de super_admin:

sql
UPDATE "user"
SET role = 'admin_msp',
    msp_roles = ARRAY['super_admin']::text[]
WHERE email = 'nuevo@sopinf.com';
  1. El usuario puede ver ahora la sección admin en el portal.

Alternativamente, usar el flujo /admin/team → Invite del portal, que crea una fila en msp_invitations y envía un email. El invitado hace click en el link y completa el registro.

Crear un client_admin para un nuevo tenant ​

  1. Crear el tenant en /admin/tenants/new.
  2. El nuevo tenant crea una organization de Better Auth. Anotar el organization.id.
  3. Registrar el usuario cliente a través del portal.
  4. Setear su rol:
sql
UPDATE "user"
SET role = 'client_admin'
WHERE email = 'owner@clienteempresa.com';
  1. Crear la membresía org:
sql
-- Tabla member de Better Auth
INSERT INTO member (id, organization_id, user_id, role, created_at)
VALUES (gen_random_uuid()::text, '<org_id>', '<user_id>', 'owner', NOW());

O usar la API de Better Auth (POST a /api/auth/organization/add-member) — este es el enfoque preferido para mantenerse dentro de la gestión de sesiones de Better Auth.

Invitaciones de vendedores ​

Los vendedores (commission reps) acceden al portal via invitación por magic-link. El flujo:

  1. Un usuario admin_msp con commissions.manage crea un commission rep en /admin/commissions/vendedores.
  2. Desde la vista de detalle del rep, click en "Invite to portal".
  3. La API llama a inviteRepToPortal() en seller-invitations.service.ts:
    • Invalida cualquier invitación pendiente existente para este rep.
    • Genera un token aleatorio de 64 caracteres (32 bytes → hex).
    • Crea una fila en seller_invitations con expires_at = ahora + 7 días.
    • Retorna el token.
  4. El email de invitación se envía con un link a /seller/setup?token=<token>.
  5. El vendedor hace click en el link, el token se valida y consume atómicamente en consumeInvitationToken().
  6. Se crea un nuevo usuario con role = 'seller' via Better Auth.
  7. linkUserToRep() setea commission_reps.user_id = newUser.id.

Re-invitar a un vendedor: llamar a "Invite to portal" nuevamente invalida los tokens pendientes existentes antes de crear uno nuevo (seguro re-invitar si el link anterior expiró).

Revocar acceso de vendedor: revokeAccess() setea commission_reps.user_id = null e invalida invitaciones pendientes. La cuenta de usuario queda pero sellerProcedure los bloqueará ya que no tienen más una fila de rep activa.

Reset de contraseña ​

Better Auth maneja el reset de contraseña via email token. Si la entrega de email está rota o se necesita un reset manual:

sql
-- Opción 1: encontrar la fila de account y actualizar la contraseña hasheada
-- (Better Auth usa argon2 por defecto)
SELECT id FROM "user" WHERE email = 'usuario@ejemplo.com';
-- Luego usar el admin endpoint de Better Auth para disparar email de reset,
-- o actualizar el campo password de la tabla 'account' con un nuevo hash argon2.

-- Más fácil: usar el admin endpoint de Better Auth (si está configurado):
-- POST /api/auth/admin/set-password
-- { userId: '...', newPassword: '...' }

Ver Runbooks para el procedimiento paso a paso.

Gestión de sesiones ​

  • Las sesiones se almacenan en la tabla session con un timestamp expires_at.
  • active_organization_id en la sesión trackea qué org está viendo el usuario actualmente (para usuarios client_admin multi-tenant).
  • Los session tokens se almacenan en Redis con un TTL coincidente para verificación de auth rápida.
sql
-- Listar sesiones activas para un usuario
SELECT id, expires_at, ip_address, user_agent
FROM session
WHERE user_id = '<user_id>'
  AND expires_at > NOW()
ORDER BY created_at DESC;

-- Invalidar todas las sesiones de un usuario (forzar logout)
DELETE FROM session WHERE user_id = '<user_id>';

Documentación de SipSop. Producto operado por Sopinf Tech LLC.