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.
| Flujo | Quién | Tabla | Cookies | Endpoints |
|---|---|---|---|---|
| Staff / tenant | Empleados del laboratorio | users (+ refresh_tokens) | httpOnly sin prefijo | /api/auth/* + rutas de tenant |
| Superadmin / SaaS | Equipo interno de ixiclinic | saas_users | prefijo saas- | /api/saas/* |
| Cliente / portal | Pacientes y empresas | clients / 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 elJwtPayload(userId, tenantId, branchId, role) descrito en Multi-tenancy. - Refresh token: 30 días (
signRefreshToken). Se persiste en la tablarefresh_tokenscon suexpiresAt. En elrefreshse valida que el token exista en la tabla y no haya expirado/revocado; ellogoutlo borra. - Cookies httpOnly: el admin guarda los tokens en cookies httpOnly (sin prefijo) y el
api-clientdel admin reintenta automáticamente con el refresh al recibir un 401. - El
refreshpreserva el tenant/sucursal activos (los que fijó el conmutador de organización): si llega untenantIddistinto, verifica la membresía enuserTenantsantes 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 centinelastenantId: '__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 preHandlerrequireSuperadmin(apps/api/src/plugins/require-superadmin.ts), que exige a la vez el rolsuperadminy el centinela__saas__. Excepción:POST /api/saas/demoses pública (la llama la landing). - A la inversa, el plugin
tenant-scopebloquea (403) cualquier token contenantId === '__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-ticketGET /api/queue/kiosks/{id}/config
Nota: El plugin
tenant-scopemantiene 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/:
| Plugin | Identidad que valida | Mecanismo |
|---|---|---|
auth | Staff de tenant | Bearer JWT → request.user |
client-auth | Paciente / empresa | Bearer JWT con _client → request.client |
require-superadmin | Superadmin SaaS | rol superadmin + centinela __saas__ |
rbac | Acceso por módulo del staff | requireModule('...') (ver abajo) |
instrument-auth | Instrumentos de laboratorio | header X-Instrument-Key → request.instrument |
bridge-auth | Agentes ixiclinic Connect | header X-Bridge-Token → request.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_MODULESes 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_ROLESdefine los grants de los roles de fábrica:admin(todos los módulos),doctor,pathologist,bioanalyst,cashier,receptionist. Estos se siembran en la tablarolesal crear un tenant (seedBuiltinRolesen 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.
Multi-sucursal
Cómo un laboratorio opera varias sucursales en ixiclinic — modelo de datos, sucursal activa, scoping de datos, acceso por usuario y gestión.
Tiempo real
Socket.IO en el API, namespaces y rooms por tenant/usuario, y los eventos en vivo de cola, notificaciones, chat y agentes de impresión.