fase 3.5: licenciamiento por plan (clave firmada, planes basico/profesional/enterprise/custom, modulos incluidos y limites de usuarios/sucursales/citas/almacenamiento, modo aviso o bloqueo, panel de licencia) + API keys gestionables con alcances y revocacion (arregla la API publica que estaba rota) + webhooks salientes firmados con outbox, reintentos y bitacora; herramientas de vendedor y docs

This commit is contained in:
2026-10-11 19:53:44 +00:00
parent ab9d84cb67
commit 3ed0d327d2
30 changed files with 3705 additions and 89 deletions

118
docs/producto/licencias.md Normal file
View File

@@ -0,0 +1,118 @@
# Licenciamiento por plan
Producto comercial: cada instancia white-label puede llevar una **licencia
firmada** que decide qué módulos incluye y sus límites de uso. Sin licencia
aplicada (el default) el sistema se comporta **exactamente como hoy**.
## Bloque `licencia` de clinic-settings
```json
{
"plan": "custom",
"modo": "aviso",
"cliente": "",
"vence": "",
"clave": "",
"modulos": [],
"limites": {
"usuarios_max": 0,
"sucursales_max": 0,
"citas_mes_max": 0,
"almacenamiento_mb": 0
}
}
```
* `plan`: `basico | profesional | enterprise | custom`.
* `modo`: `aviso` (default; **solo advierte**) o `bloqueo` (aplica de verdad).
* `limites`: `0` = **sin límite** (default → nada cambia).
* `modulos`: lista de módulos incluidos; **vacío = todos**.
* `clave`: la clave firmada por el vendedor. Vacía = sin licencia aplicada.
La licencia **nunca** bloquea lo indispensable: login, agenda, pacientes, POS,
inventario, reportes y configuración siempre están disponibles. Solo aplica a
los *módulos gateables* y a los límites indicados.
## Tabla de planes
| Plan | Módulos incluidos |
| --- | --- |
| `basico` | `agenda`, `pacientes`, `pos`, `inventario`, `reportes` (sin whatsapp, notificaciones, reservas, recursos, membresías, compras, comisiones, formularios) |
| `profesional` | Básico **+** `whatsapp`, `notificaciones`, `reservas_online`, `recursos` |
| `enterprise` | Todos los módulos (los 5 base + los 8 gateables) |
| `custom` | Lo que diga la clave (si no lista módulos, todos) |
Módulos *gateables* (controlables por licencia): `whatsapp`, `notificaciones`,
`reservas_online`, `recursos`, `membresias`, `compras`, `comisiones`,
`formularios`.
## Aplicación con `modo=bloqueo`
* **Módulos no incluidos**: sus secciones desaparecen del menú, sus endpoints
responden **403** con un mensaje claro (`Módulo no incluido en tu plan`) y su
bloque `activo` de clinic-settings se considera apagado.
* **Límites**: se impide crear más allá de `usuarios_max`, `sucursales_max`,
`citas_mes_max` (citas del mes en curso) y `almacenamiento_mb` (suma de
adjuntos). El error indica el límite y cómo ampliarlo.
* Con `modo=aviso` nada se bloquea; los excesos solo quedan advertidos.
Las tareas internas (cron, migraciones) pueden saltarse los límites con el
contexto `skeen_licencia_skip=True`.
## Formato de la clave de licencia
Texto en una línea:
```
SKN1.<base64url(payload_json)>.<base64url(hmac_sha256(payload_b64, vendor_key))>
```
* `payload_json` = JSON con claves ordenadas y separadores mínimos:
`cliente`, `plan`, `vence`, `emitida`, `modulos`, `limites`.
* Firmado con **HMAC-SHA256** y la clave del vendedor.
### Clave del vendedor
Debe estar presente en la instancia para poder validar (HMAC es simétrico):
1. `ir.config_parameter 'skeen.licencia.vendor_key'`
2. Variable de entorno `SKEEN_LICENCIA_VENDOR_KEY`
3. Archivo `/root/.skeen-licencias/vendor.key` (permisos `600`)
> **Ruta del secreto:** `/root/.skeen-licencias/vendor.key` (fuera del repo, 600).
> Nota de seguridad: al ser HMAC simétrico, quien tenga la clave del vendedor
> puede forjar licencias; mantenerla fuera de las instancias de cliente en un
> despliegue real (aquí vive en un VPS de desarrollo compartido).
### Herramienta del vendedor
`/root/skeen-proyecto/infra/licencias/generar_licencia.py` (CLI, Python stdlib):
```bash
generar_licencia.py generar --cliente "Spa Bella" --plan profesional --vence 2027-12-31
generar_licencia.py generar --cliente "Spa Bella" --plan custom --vence 2027-12-31 \
--modulos agenda,pacientes,pos,inventario,reportes,compras \
--usuarios-max 10 --sucursales-max 2 --citas-mes-max 500 --almacenamiento-mb 4096
generar_licencia.py verificar --clave "SKN1...."
```
## Endpoints
| Método | Ruta | Acceso | Descripción |
| --- | --- | --- | --- |
| GET | `/skeen/frontend/v1/licencia` | admin/gerencia | Estado completo + uso vs límites |
| PUT | `/skeen/frontend/v1/licencia` | admin | Aplicar/actualizar/quitar licencia |
| POST | `/skeen/frontend/v1/licencia/validar` | admin/gerencia | Probar una clave sin aplicarla |
`PUT` con `{"clave": "SKN1..."}` verifica firma y vigencia; si es inválida o
vencida responde **400** y **no sobrescribe** la licencia previa. `{"clave": ""}`
quita la licencia y vuelve a habilitarlo todo. `{"modo": "bloqueo"}` cambia el
modo sin re-firmar.
## UI
Configuración → pestaña **Licencia** (admin; gerencia también ve el estado):
plan y cliente, vencimiento con días restantes, campo para pegar la clave,
tabla de uso vs límites con semáforo, tabla de módulos incluidos/no incluidos,
avisos de vencimiento (≤ 15 días) y, si no hay licencia, el mensaje
*"sin licencia aplicada — todas las funciones habilitadas"*.