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_admin
impersonationAwareProcedureprotectedProcedure + NO en sesión de impersonation

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.
suspendedTenantGateErrorprotectedProcedure 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.