164 lines
8.2 KiB
Markdown
164 lines
8.2 KiB
Markdown
# 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:
|
|
|
|
1. **Admin del tenant** — rol local del tenant con permisos elevados (invitar usuarios, configurar, etc.)
|
|
2. **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
|
|
|
|
1. **"Admin global" como concepto:**
|
|
- Función `isGlobalAdmin(tenantId, role)` en `apps/api/src/utils/global-admin.ts` — sigue llamándose igual, internamente checa `role === 'owner'`
|
|
- Función `isGlobalAdminRfc(tenantRfc, role)` en `packages/shared/src/constants/roles.ts` — idem
|
|
- Constante `GLOBAL_ADMIN_RFC = 'HTS240708LJA'` — sin cambios
|
|
- Mensajes de error como "Solo el administrador global puede..." quedan intactos
|
|
|
|
2. **Email `admin@demo.com`:** es una constante de test, el rename solo afecta el rol interno. El email sobrevive como identificador estable.
|
|
|
|
3. **Variables semánticas `adminEmail`, `adminNombre`** en payloads de `createTenant`: 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.
|
|
|
|
4. **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 rol `owner`.
|
|
|
|
## Archivos tocados
|
|
|
|
### Shared
|
|
- `packages/shared/src/types/auth.ts`
|
|
- `packages/shared/src/constants/roles.ts`
|
|
|
|
### Backend API (17 archivos)
|
|
- `prisma/seed.ts` — migración idempotente + role seed con `nombre: 'owner'` + demo user con `rolNombre: 'owner'`
|
|
- `src/utils/global-admin.ts`
|
|
- `src/services/auth.service.ts`, `tenants.service.ts`, `payment/subscription.service.ts`, `payment/mercadopago.service.ts`
|
|
- `src/controllers/bancos.controller.ts`, `calendario.controller.ts`, `cfdi.controller.ts`, `conciliacion.controller.ts`, `facturacion.controller.ts`, `impuestos.controller.ts`, `regimen.controller.ts`, `usuarios.controller.ts`
|
|
- `src/routes/documentos.routes.ts`, `facturacion.routes.ts`, `sat.routes.ts`, `subscription.routes.ts`
|
|
|
|
### Frontend Web (11 archivos)
|
|
- `app/(auth)/register/page.tsx`
|
|
- `app/(dashboard)/usuarios/page.tsx`, `admin/usuarios/page.tsx`
|
|
- `app/(dashboard)/cfdi/page.tsx`, `calendario/page.tsx`, `documentos/page.tsx`
|
|
- `app/(dashboard)/configuracion/page.tsx`, `configuracion/csd/page.tsx`
|
|
- `app/(dashboard)/clientes/page.tsx`
|
|
- `components/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 ortogonal
|
|
- `README.md` — bullet nuevo en changelog v0.9.1
|
|
- `docs/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:
|
|
|
|
```typescript
|
|
// 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:seed` una 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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
1. Login con `admin@demo.com / demo123` → JWT nuevo trae `role: 'owner'`
|
|
2. Página `/usuarios` muestra "Dueño" como role label para el usuario demo
|
|
3. Select al invitar nuevo usuario: opción "Dueño" (antes "Administrador")
|
|
4. Mensajes de error tipo "Solo los dueños pueden invitar usuarios" cuando un contador intenta
|
|
5. **"Admin global" preservado**: ingresando como `admin@horux360.com` (si corriste `bootstrap: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
|
|
|
|
1. **Variables `adminEmail` / `adminNombre` en tipos de API:** renombrarlas a `ownerEmail` / `ownerNombre` por consistencia total. Requiere actualizar integraciones externas. No se hizo porque no es bloqueante.
|
|
2. **Revisar integraciones externas/scripts** que asuman `role = 'admin'` — fuera del alcance de este rename (no viven en este repo).
|
|
3. **Tests automatizados de authorization:** confirmarían que el rename no rompió ningún endpoint. Actualmente solo hay verificación manual + typecheck.
|