ixiclinicDocs
Desarrolladores

Autenticación

Los tres flujos de auth (staff, superadmin SaaS y cliente del portal), rutas públicas, plugins de auth y RBAC por módulo.

El API expone tres poblaciones de usuarios distintas y cada una tiene su propio flujo de autenticación, su propia tabla y su propio prefijo de cookies. Todas firman JWT con los mismos secretos, pero los tokens no son intercambiables: hay guardas explícitas para que un token de una población no funcione en las rutas de otra.

FlujoQuiénTablaCookiesEndpoints
Staff / tenantEmpleados del laboratoriousers (+ refresh_tokens)httpOnly sin prefijo/api/auth/* + rutas de tenant
Superadmin / SaaSEquipo interno de ixiclinicsaas_usersprefijo saas-/api/saas/*
Cliente / portalPacientes y empresasclients / pacientes(las gestiona el portal)/api/client/*

1. Staff / tenant (JWT + refresh)

Es el flujo principal (admin.ixiclinic.com). Vive en apps/api/src/modules/auth/service.ts y se firma con los helpers de apps/api/src/lib/jwt.ts.

  • Access token: 15 minutos (signAccessToken). Lleva el JwtPayload (userId, tenantId, branchId, role) descrito en Multi-tenancy.
  • Refresh token: 30 días (signRefreshToken). Se persiste en la tabla refresh_tokens con su expiresAt. En el refresh se valida que el token exista en la tabla y no haya expirado/revocado; el logout lo borra.
  • Cookies httpOnly: el admin guarda los tokens en cookies httpOnly (sin prefijo) y el api-client del admin reintenta automáticamente con el refresh al recibir un 401.
  • El refresh preserva el tenant/sucursal activos (los que fijó el conmutador de organización): si llega un tenantId distinto, verifica la membresía en userTenants antes de cambiarlo.

El plugin que valida el token es apps/api/src/plugins/auth.ts. Usa un hook preValidation (para abortar antes que los preHandler), lee el header Authorization: Bearer <token> y pobla request.user con verifyAccessToken.

Sesiones de impersonación / "actuar como"

signImpersonationToken emite un access token de 1 hora sin refresh, de modo que la sesión expira limpiamente y un admin no se queda logueado como otra persona. El JwtPayload de esa sesión lleva el campo impersonator con los datos del admin real.

2. Superadmin / SaaS

Separado del tenant. El login vive en el módulo saas-auth/ y los usuarios en saas_users.

  • El token lleva role: 'superadmin' y los centinelas tenantId: '__saas__', branchId: '__saas__'.
  • Las cookies usan prefijo saas- (saas-accessToken, saas-refreshToken, saas-user) para no colisionar con las del admin de tenant en el mismo navegador.
  • Todas las rutas /api/saas/* se guardan con el preHandler requireSuperadmin (apps/api/src/plugins/require-superadmin.ts), que exige a la vez el rol superadmin y el centinela __saas__. Excepción: POST /api/saas/demos es pública (la llama la landing).
  • A la inversa, el plugin tenant-scope bloquea (403) cualquier token con tenantId === '__saas__' que intente entrar a una ruta de tenant.

3. Cliente / portal (paciente y empresa)

El portal del paciente (portal.ixiclinic.com) y el sitio público del laboratorio (lab-site) usan tokens de cliente, distintos de los del staff. Soporta dos sub-flujos: login de paciente y login/registro de empresa.

Los tokens de cliente se firman con el mismo secreto que los del staff pero llevan la marca _client: true (signClientAccessToken / signClientRefreshToken en lib/jwt.ts). La marca es la defensa clave:

export function verifyAccessToken(token: string): JwtPayload {
  const decoded = jwt.verify(token, env.JWT_SECRET) as JwtPayload & { _client?: boolean };
  // Un token de cliente nunca debe valer en rutas de staff: si no, un paciente
  // logueado podría leer PHI de otros pacientes.
  if (decoded._client) {
    throw new Error('Client token cannot be used to authenticate staff routes');
  }
  return decoded;
}

El guard de las rutas de cliente es apps/api/src/plugins/client-auth.ts. Exporta requireClient(type?), que valida el Bearer, pobla request.client con verifyClientAccessToken y opcionalmente exige el tipo correcto:

if (type && request.client.type !== type) {
  return reply.status(403).send({ /* requiere una cuenta de tipo X */ });
}

El ClientJwtPayload lleva clientId, tenantId, type: 'patient' | 'company' y patientId/companyId según el caso.

Rutas públicas (saltan auth)

El plugin de auth deja pasar sin token las rutas listadas en AUTH_PUBLIC de apps/api/src/plugins/auth.ts, más algunos casos por método:

  • /api/auth/login, /api/auth/register, /api/auth/refresh
  • /api/portal/*
  • /api/health
  • /docs (Scalar UI)
  • /api/saas/auth/*
  • /api/client/auth/* y /api/client/*
  • /api/queue/board/*, /api/queue/patient/* (pantallas públicas de la cola)
  • /api/webhooks/*
  • /api/bridge/* (los agentes de impresión autentican con su propio token de dispositivo)
  • POST /api/saas/demos (formulario de demo de la landing)
  • POST /api/instruments/{id}/push (instrumentos, autenticados por su API key)
  • POST /api/queue/kiosks/{id}/ticket, POST /api/queue/kiosks/{id}/appointment-ticket
  • GET /api/queue/kiosks/{id}/config

Nota: El plugin tenant-scope mantiene su propia lista de prefijos públicos que debe reflejar la de auth. Al agregar una ruta pública nueva, actualiza ambas.

Plugins de auth del API

Todos viven en apps/api/src/plugins/:

PluginIdentidad que validaMecanismo
authStaff de tenantBearer JWT → request.user
client-authPaciente / empresaBearer JWT con _clientrequest.client
require-superadminSuperadmin SaaSrol superadmin + centinela __saas__
rbacAcceso por módulo del staffrequireModule('...') (ver abajo)
instrument-authInstrumentos de laboratorioheader X-Instrument-Keyrequest.instrument
bridge-authAgentes ixiclinic Connectheader X-Bridge-Tokenrequest.bridge

RBAC: acceso por módulo

El control de acceso fino del staff se hace por módulo, no por rol embebido en la UI. La config pura vive en apps/api/src/plugins/rbac-config.ts y la resolución + el guard en apps/api/src/plugins/rbac.ts.

  • SYSTEM_MODULES es el catálogo de permisos: dashboard, patients, orders, results, billing, cash, catalog, inventory, qc, reports, doctors, companies, instruments, queue, settings, import, website, appointments, más los workspaces del Lab OS (ws_facturacion, ws_consulta, ws_laboratorio, ws_recepcion, ws_patologia, ws_contabilidad, ws_muestras).
  • BUILTIN_ROLES define los grants de los roles de fábrica: admin (todos los módulos), doctor, pathologist, bioanalyst, cashier, receptionist. Estos se siembran en la tabla roles al crear un tenant (seedBuiltinRoles en el service de auth).
  • Roles personalizados: sus permisos viven en roles.permissions (jsonb) y se resuelven con caché en memoria de 60 s (resolvePermissions).

El guard se aplica como preHandler en las rutas:

export function requireModule(module: string) {
  return async (request, reply) => {
    if (!request.user || !request.tenant) return;
    if (request.user.role === 'admin') return; // admin omite la verificación
    const perms = await resolvePermissions(request.user.role, request.tenant.id);
    if (!perms.includes(module)) {
      return reply.status(403).send({ message: `No tienes acceso al módulo "${module}"` });
    }
  };
}

Ejemplos reales de uso: las rutas de e-CF (apps/api/src/modules/ecf/routes.ts) están gateadas con requireModule('billing'), y las de impresión/hardware (apps/api/src/modules/printing/routes.ts) con requireModule('settings').

Siguiente: Tiempo real.

On this page