ea35c11f19
Document the wallet-model account flow in the Users tab: invite-link account creation (with the configurable client base URL for the join link), permanent hard-delete with strong confirmation, and the deploy gap (cluster still on bus v0.11.0; this branch merges after the bus reaches master). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
195 lines
10 KiB
Markdown
195 lines
10 KiB
Markdown
---
|
|
name: unibus_admin
|
|
lang: go
|
|
domain: infra
|
|
version: 0.2.0
|
|
description: "Panel web de administración de unibus: un binario Go que sirve una SPA Mantine embebida y expone una REST API. Tiene la identidad ADMIN del operador, firma cada petición al plano de control del bus, y gestiona rooms, miembros, claves, usuarios (alta por invitación + baja por hard-delete) y el estado del cluster."
|
|
tags: [service, messaging, admin, nats, e2e]
|
|
uses_functions:
|
|
- sign_ed25519_go_cybersecurity
|
|
uses_types: []
|
|
framework: ""
|
|
entry_point: "main.go"
|
|
dir_path: "projects/message_bus/apps/unibus_admin"
|
|
repo_url: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/dataforge/unibus_admin"
|
|
service:
|
|
port: 8480
|
|
health_endpoint: /healthz
|
|
health_timeout_s: 3
|
|
systemd_unit: unibus-admin.service
|
|
systemd_scope: system
|
|
restart_policy: always
|
|
runtime: systemd-system
|
|
pc_targets:
|
|
- magnus
|
|
is_local_only: false
|
|
e2e_checks:
|
|
- id: build
|
|
cmd: "CGO_ENABLED=0 go build ./..."
|
|
timeout_s: 180
|
|
- id: vet
|
|
cmd: "CGO_ENABLED=0 go vet ./..."
|
|
timeout_s: 120
|
|
- id: web_build
|
|
cmd: "cd web && pnpm install --frozen-lockfile && pnpm build"
|
|
timeout_s: 240
|
|
- id: smoke_mock
|
|
cmd: "./unibus_admin --mock --port 18490 & sleep 2 && curl -fsS http://127.0.0.1:18490/healthz && curl -fsS http://127.0.0.1:18490/api/cluster >/dev/null && kill %1"
|
|
timeout_s: 30
|
|
---
|
|
|
|
# unibus_admin
|
|
|
|
Panel web de administración del bus de mensajería **unibus** (NATS + JetStream con
|
|
cifrado E2E por room). Un único binario Go que:
|
|
|
|
1. **Sirve la SPA Mantine compilada y embebida** (`embed.FS` sobre `web/dist`), con
|
|
el mismo look índigo/oscuro que la web del cliente del bus.
|
|
2. **Expone una REST API de administración** bajo `/api`. El binario tiene la
|
|
identidad ADMIN del operador y media cada acción privilegiada contra el plano de
|
|
control de unibus, **firmando cada petición**. El navegador nunca firma, nunca
|
|
habla NATS y nunca ve una clave privada.
|
|
|
|
## Arquitectura
|
|
|
|
```
|
|
navegador (SPA Mantine)
|
|
│ fetch /api/* (basic auth de Caddy + bind loopback)
|
|
▼
|
|
unibus_admin (gateway Go, identidad ADMIN del operador)
|
|
├── pkg/client (unibus) → CreateRoom / Invite / Kick / ListMyRooms (firma + cripto E2E)
|
|
├── pkg/client (unibus) → ListUsers / AddUser / RevokeUser (API admin firmada; funciona en cluster)
|
|
├── GET firmado → /rooms/{id}/members (CanonicalRequest + SignEd25519, reusa la construcción del bus)
|
|
├── GET /healthz (CA-pinned)→ estado + posture de los 3 nodos del cluster
|
|
└── membership.Store (opc.) → users (allowlist) como fallback single-node con --db
|
|
▼
|
|
cluster unibus (magnus + homer + datardos, enforce + ACL + TLS + KV)
|
|
```
|
|
|
|
El gateway reutiliza el cliente del bus (`github.com/enmanuel/unibus/pkg/client`)
|
|
para todo lo que lleva criptografía (sellar la clave de room, firmar invite/rekey),
|
|
y construye GETs firmados con la **única fuente de verdad** de la firma del bus
|
|
(`membership.CanonicalRequest` + `cs.SignEd25519`) para las lecturas que el cliente
|
|
no expone. Nunca reimplementa firma ni cripto.
|
|
|
|
## Capacidades
|
|
|
|
| Pestaña | Qué hace | Vía |
|
|
|---|---|---|
|
|
| **Cluster** | up/down + posture (enforce/acl/tls/cluster/store) + latencia de cada nodo | `GET /healthz` (auth-exempt) de los nodos en `--nodes`, TLS pin a la CA del bus |
|
|
| **Rooms** | listar (rooms del admin), crear (subject + E2E/persist/firmado), ver miembros, invitar, expulsar+rekey | `pkg/client` (mutaciones) + GET firmado (miembros) |
|
|
| **Users** | listar la allowlist; **crear usuario** por enlace de invitación (modelo wallet, sin manejar claves); añadir por clave conocida; **revocar** (status flip, auditable) y **eliminar** (hard-delete permanente, con confirmación fuerte) | `pkg/client` (`ListUsers`/`AddUser`/`RevokeUser`/`DeleteUser`/`CreateInvite`/`ListInvites`) contra la API admin-only del plano de control, firmando como el operador. Funciona en cluster sin acceso directo al store. `--db` queda como fallback single-node opcional |
|
|
|
|
## Cómo arrancar
|
|
|
|
```bash
|
|
# Mock (iterar la SPA sin bus):
|
|
./unibus_admin --mock --port 8480
|
|
|
|
# Real contra un membershipd local (dev, sin TLS). Users vía la API del plano de
|
|
# control; añade --db sólo si quieres gestionar users contra un SQLite local:
|
|
./unibus_admin --port 8480 \
|
|
--ctrl-url http://127.0.0.1:8470 --nats-url nats://127.0.0.1:4250 \
|
|
--identity-pass unibus/operator-identity
|
|
|
|
# Producción (cluster magnus, enforce + TLS + nkey). Sin --db: la pestaña Users
|
|
# gestiona la allowlist por la API admin firmada del plano de control:
|
|
./unibus_admin --port 8480 --bind 127.0.0.1 \
|
|
--ctrl-url https://127.0.0.1:8470 --nats-url tls://127.0.0.1:4250 \
|
|
--ca /opt/unibus/tls/ca.crt \
|
|
--identity-file /opt/unibus_admin/identity.json \
|
|
--nodes "magnus=https://127.0.0.1:8470,homer=https://141.94.69.66:8470,datardos=https://51.91.100.142:8470"
|
|
```
|
|
|
|
## Build
|
|
|
|
```bash
|
|
cd web && pnpm install && pnpm build # compila la SPA a web/dist (embebida)
|
|
cd .. && CGO_ENABLED=0 go build -o unibus_admin .
|
|
```
|
|
|
|
## Seguridad
|
|
|
|
- La identidad admin se carga de `pass` (`unibus/operator-identity`) o de un fichero
|
|
0600 (`--identity-file`); nunca va hardcodeada, ni a git, ni a argv.
|
|
- El panel exige autenticación: en producción lo fronta Caddy con basic auth sobre
|
|
un subdominio ofuscado, y el gateway bindea sólo a loopback.
|
|
- Acciones destructivas (revocar user, expulsar miembro + rekey) piden confirmación
|
|
explícita en la UI.
|
|
- El operador debe estar en la allowlist del bus (rol admin) para que el gateway
|
|
pueda conectar bajo `enforce`.
|
|
|
|
## Despliegue actual
|
|
|
|
Desplegado en **magnus** como `unibus-admin.service` (systemd system, `Restart=always`),
|
|
puerto `8480` en loopback, fronteado por Caddy con basic auth en un subdominio
|
|
ofuscado (`admin-<hash>.organic-machine.com`). Credenciales en `pass`
|
|
(`unibus/admin-panel-password`, `unibus/admin-panel-url`). Artefactos de deploy en
|
|
`deploy/`.
|
|
|
|
## Creación de usuarios (modelo wallet) y enlace de invitación
|
|
|
|
La pestaña Users crea usuarios SIN que el operador maneje claves: «Crear usuario»
|
|
acuña un enlace de invitación de un solo uso (`POST /api/invites` → bus
|
|
`POST /invites`, admin-only). El gateway construye el enlace
|
|
`<<APP_BASE>>/join?token=XXX`, donde `<<APP_BASE>>` es la URL del **cliente
|
|
final** (la página que hospeda `/join`), NO la del panel. Se configura en el
|
|
gateway con `--join-base-url https://chat.unibus.example` o la variable
|
|
`UNIBUS_JOIN_BASE_URL`; el valor se expone en `/api/me` (`join_base_url`). Si no
|
|
se configura, la SPA usa su propio origen como respaldo y avisa de configurarlo.
|
|
El usuario abre el enlace en su dispositivo, genera ahí su par de claves (la
|
|
privada nunca sale del equipo) y se registra (`POST /register`, lo consume la
|
|
página `/join` del cliente web — ver el contrato en el report del bus).
|
|
|
|
«Eliminar» es un **hard-delete permanente** (`DELETE /api/users/{pub}` → bus
|
|
`DELETE /users/{signpub}`), distinto de «Revocar» (status flip auditable). La UI
|
|
exige teclear el handle para confirmarlo (no se dispara por un clic accidental).
|
|
|
|
## Gaps conocidos
|
|
|
|
- **Cuentas (invites + hard-delete) contra el cluster desplegado**: el gateway y la
|
|
SPA ya consumen `POST/GET /api/invites` y `DELETE /api/users/{pub}`, verificados
|
|
end-to-end contra un `membershipd` v0.12.0 local (crear invite → registrar por
|
|
`/register` sin firma → aparece en `/users` → hard-delete → desaparece) y vía
|
|
`--mock`. El gap es de **despliegue**: el cluster corre hoy v0.11.0 (sin
|
|
`/invites`/`/register`/`DELETE /users`), así que la pestaña Users sólo creará/
|
|
eliminará cuentas en producción cuando el bus se actualice a v0.12.0 (rollout
|
|
pendiente). Verificado contra el cluster vivo: `/register` devuelve 401 en
|
|
magnus/homer/datardos (ruta aún no desplegada) frente a 400 en el nodo v0.12.0
|
|
local. El merge de esta rama del panel a master debe seguir al merge del bus.
|
|
- **Users contra el cluster desplegado**: el código del plano de control (unibus
|
|
master, v0.10.0) ya expone la API admin-only de users (`GET/POST /users`,
|
|
`POST /users/{signpub}/revoke`) y el gateway la consume firmando como el operador.
|
|
La cadena completa (list/add/revoke + idempotencia 409) está verificada end-to-end
|
|
contra un `membershipd` master local. El gap restante es de **despliegue**: los
|
|
binarios `membershipd` que corren hoy en el cluster (magnus/homer/datardos) son
|
|
anteriores al merge de esta ruta y devuelven `404` en `/users`, así que la pestaña
|
|
Users sólo será funcional en producción cuando el bus se actualice a v0.10.0. El
|
|
gateway ya conecta y firma correctamente contra esos nodos (verificado: `/api/me`
|
|
responde con el endpoint real del operador y pasa el `enforce` de magnus). Para
|
|
gestión single-node sin esperar al cluster, `--db` sigue disponible.
|
|
- **meta-leader / tamaño de quórum** del cluster: `/healthz` no los expone; requieren
|
|
el endpoint de monitoreo de NATS (varz/jsz). La pestaña Cluster muestra up/posture.
|
|
- **Invite a room E2E**: requiere las claves públicas (sign_pub + kex_pub) del
|
|
invitado en hex, porque la clave de room se sella contra su X25519. La UI las pide
|
|
manualmente; no hay directorio de claves públicas todavía.
|
|
|
|
## Capability growth log
|
|
|
|
- v0.2.0 (2026-06-07) — capa de CUENTAS estilo WhatsApp sobre el bus v0.12.0. El
|
|
gateway gana `POST/GET /api/invites` (acuñar/listar invitaciones de un solo uso,
|
|
consumiendo `client.CreateInvite/ListInvites`) y `DELETE /api/users/{pub}`
|
|
(hard-delete, `client.DeleteUser`), con la misma doble vía que el resto de users
|
|
(plano de control firmado en cluster / store directo en single-node). El enlace de
|
|
invitación `<<APP_BASE>>/join?token=…` lo construye el gateway desde
|
|
`--join-base-url` / `UNIBUS_JOIN_BASE_URL` (URL del cliente final, no del panel),
|
|
expuesto en `/api/me`. La SPA añade a la pestaña Users: botón «Crear usuario»
|
|
(modal handle+rol+caducidad → enlace copiable), card de invitaciones pendientes
|
|
(handle, rol, token parcial, caducidad, copiar enlace), y botón «Eliminar» con
|
|
confirmación FUERTE (teclear el handle) que distingue el borrado permanente del
|
|
revoke. Verificado: flujo e2e contra `membershipd` v0.12.0 local (invite → register
|
|
por curl sin firma → aparece en /users → re-register 409 → hard-delete → desaparece)
|
|
y gateway vía `--mock` (las rutas nuevas + el SPA embebido sirven). El cluster vivo
|
|
sigue en v0.11.0 (rollout del bus pendiente del orquestador); esta rama del panel se
|
|
mergea a master tras el merge del bus a master. build/vet/web-build verdes.
|