8.2 KiB
Rename del rol admin → owner (label UI: "Dueño")
Resumen
Rename del rol per-tenant admin a owner en todo el código, con label visible en UI como "Dueño". El concepto ortogonal de "admin global" (tenant RFC HTS240708LJA con acceso transversal a todos los tenants) se preservó sin cambios de nombre.
Motivación
Tener dos cosas llamadas "admin" generaba confusión recurrente:
- Admin del tenant — rol local del tenant con permisos elevados (invitar usuarios, configurar, etc.)
- Admin global — Horux 360 como dueño de la plataforma, con poder sobre todos los tenants
En UI, mensajes, código y conversaciones era ambiguo: "Solo el admin puede..." ¿cuál admin? El rename disambigua sin perder el concepto de admin global.
Cambios
Código (identificadores)
| Antes | Ahora |
|---|---|
Role = 'admin' | 'cfo' | ... |
Role = 'owner' | 'cfo' | ... |
ROLES.admin |
ROLES.owner |
role === 'admin' |
role === 'owner' |
authorize('admin', 'cfo') |
authorize('owner', 'cfo') |
where: { rol: { nombre: 'admin' } } |
where: { rol: { nombre: 'owner' } } |
prisma.rol.findUnique({ where: { nombre: 'admin' } }) |
{ nombre: 'owner' } |
UI (labels visibles)
| Antes | Ahora |
|---|---|
| "Administrador" | "Dueño" |
| "Solo administradores pueden..." | "Solo los dueños pueden..." |
| "Datos del Administrador" (registro) | "Datos del Dueño" |
| "Administrador del Cliente" (crear tenant) | "Dueño del Cliente" |
| "Nombre del Administrador" | "Nombre del Dueño" |
| "Email del Administrador" | "Email del Dueño" |
| "Contacta al administrador" (CSD sin timbres) | "Contacta al dueño de la cuenta" |
| "Pide al administrador que configure MP" | "Pide al dueño de la cuenta..." |
BD (roles table central)
Row id=1: nombre = 'admin' → nombre = 'owner'. descripcion = 'Administrador - acceso completo' → 'Dueño - acceso completo'.
Demo user
admin@demo.com (email preservado) ahora tiene rol owner en BD. Password sigue siendo demo123. El Dueño Demo es el label que verás en UI.
Qué se preservó deliberadamente
-
"Admin global" como concepto:
- Función
isGlobalAdmin(tenantId, role)enapps/api/src/utils/global-admin.ts— sigue llamándose igual, internamente checarole === 'owner' - Función
isGlobalAdminRfc(tenantRfc, role)enpackages/shared/src/constants/roles.ts— idem - Constante
GLOBAL_ADMIN_RFC = 'HTS240708LJA'— sin cambios - Mensajes de error como "Solo el administrador global puede..." quedan intactos
- Función
-
Email
admin@demo.com: es una constante de test, el rename solo afecta el rol interno. El email sobrevive como identificador estable. -
Variables semánticas
adminEmail,adminNombreen payloads decreateTenant: representan "el email/nombre del admin del cliente que estás creando". No son identificadores de rol. Podrían renombrarse en un pase separado si se desea, pero no afecta funcionalidad. -
Script
bootstrap-horux360-admin.ts: nombre del script preservado — "bootstrap del admin global" es el propósito, no el rol. Internamente crea un user con rolowner.
Archivos tocados
Shared
packages/shared/src/types/auth.tspackages/shared/src/constants/roles.ts
Backend API (17 archivos)
prisma/seed.ts— migración idempotente + role seed connombre: 'owner'+ demo user conrolNombre: 'owner'src/utils/global-admin.tssrc/services/auth.service.ts,tenants.service.ts,payment/subscription.service.ts,payment/mercadopago.service.tssrc/controllers/bancos.controller.ts,calendario.controller.ts,cfdi.controller.ts,conciliacion.controller.ts,facturacion.controller.ts,impuestos.controller.ts,regimen.controller.ts,usuarios.controller.tssrc/routes/documentos.routes.ts,facturacion.routes.ts,sat.routes.ts,subscription.routes.ts
Frontend Web (11 archivos)
app/(auth)/register/page.tsxapp/(dashboard)/usuarios/page.tsx,admin/usuarios/page.tsxapp/(dashboard)/cfdi/page.tsx,calendario/page.tsx,documentos/page.tsxapp/(dashboard)/configuracion/page.tsx,configuracion/csd/page.tsxapp/(dashboard)/clientes/page.tsxcomponents/layouts/sidebar.tsx,sidebar-compact.tsx,sidebar-floating.tsx,topnav.tsx
Docs
CLAUDE.md— tabla de roles actualizada, explicación clara de "admin global" como concepto ortogonalREADME.md— bullet nuevo en changelog v0.9.1docs/plans/2026-04-13-subscriptions-self-serve.md— referencias a admin pasan a owner donde corresponde
Migración idempotente
El seed incluye este SQL al inicio de la sección de roles:
// Migración: renombra el rol legacy 'admin' a 'owner' si sobrevive de un seed viejo.
// Idempotente (no-op si ya se renombró o nunca existió).
await prisma.$executeRawUnsafe(`UPDATE roles SET nombre = 'owner' WHERE nombre = 'admin'`);
Esto permite que:
- Desarrolladores con BD vieja solo corran
pnpm db:seeduna vez para converger - Deploys a prod apliquen el rename sin necesidad de migración SQL manual
- Re-seedear no rompe la BD actualizada
Impacto en runtime
JWT invalidados
Los tokens JWT emitidos antes del rename tienen role: 'admin'. Después del rename, el middleware de auth usa Zod para validar el payload contra el nuevo enum Role, que ya no acepta 'admin'. Resultado: cualquier sesión activa obtiene 401 en la siguiente request → se fuerza re-login.
Mitigación: anuncio en release notes y email a clientes si se despliega a producción. En dev, simplemente cerrar sesión y volver a entrar.
Queries SQL directas
Si hay scripts externos al repo (reportes, jobs de ETL, etc.) que filtran por role = 'admin' en la tabla users o joins, fallarán silenciosamente (filtro no matchea nada). Revisar integraciones externas antes de desplegar a prod.
API consumidores externos
Si el API tiene consumidores que envían role: 'admin' en payloads de creación/update de usuarios, esos requests fallarán la validación de Zod. Actualizar docs de API + comunicar a integradores.
Verificación
# Backend
cd apps/api
pnpm typecheck # 0 errores
# BD
pnpm db:seed # idempotente — renombra legacy admin → owner, crea rol 'owner' si falta
Al correr seed post-rename, deberías ver en log:
✅ 5 roles cargados
✅ User created: admin@demo.com (owner)
Tests manuales
- Login con
admin@demo.com / demo123→ JWT nuevo traerole: 'owner' - Página
/usuariosmuestra "Dueño" como role label para el usuario demo - Select al invitar nuevo usuario: opción "Dueño" (antes "Administrador")
- Mensajes de error tipo "Solo los dueños pueden invitar usuarios" cuando un contador intenta
- "Admin global" preservado: ingresando como
admin@horux360.com(si corristebootstrap:admin-global) se ve la sección Clientes + Admin Usuarios + Todas las Suscripciones — comportamiento idéntico al de antes del rename
Decisiones descartadas
Renombrar isGlobalAdmin → isGlobalOwner
Tentación: consistencia léxica.
Por qué no: el concepto a nivel producto sigue llamándose "admin global" — así lo dice la UI, los docs, y así lo entienden los usuarios. Renombrar solo la función interna crea disonancia entre código y lenguaje del producto. 8 callsites tocados sin ganancia funcional.
Renombrar GLOBAL_ADMIN_RFC → GLOBAL_OWNER_RFC
Mismo razonamiento. El RFC HTS240708LJA identifica al admin global de la plataforma; su nombre constante refleja ese concepto.
Renombrar bootstrap-horux360-admin.ts → bootstrap-horux360-owner.ts
El script provisiona el tenant del admin global. Renombrarlo confunde el propósito (alguien pensaría que bootstrappea cualquier owner).
Pendientes opcionales
- Variables
adminEmail/adminNombreen tipos de API: renombrarlas aownerEmail/ownerNombrepor consistencia total. Requiere actualizar integraciones externas. No se hizo porque no es bloqueante. - Revisar integraciones externas/scripts que asuman
role = 'admin'— fuera del alcance de este rename (no viven en este repo). - Tests automatizados de authorization: confirmarían que el rename no rompió ningún endpoint. Actualmente solo hay verificación manual + typecheck.