docs: T-HUB README and validation guide
README: architecture diagrams (single account, shared accounts, components), status per platform, outreach, hardware and bandwidth requirements, ports, security practices and what to monitor. docs/T-HUB-VALIDACION.txt: per-commit test plan for reviewers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
d2a4209b0a
commit
533a0fb1c0
2 changed files with 417 additions and 6 deletions
267
README.md
267
README.md
|
|
@ -1,4 +1,259 @@
|
|||
# Tactical Media Broadcast Hub
|
||||
# T-HUB
|
||||
|
||||
<p align="center"><img src="docs/brand/t-hub.svg" width="160" alt="T-HUB"></p>
|
||||
|
||||
Copia de trabajo del [Tactical Media Broadcast Hub](https://gitlab.moxexley.com/mediatools/tacticalmediahub)
|
||||
para la red de acampadas: [acampada26s.net](https://acampada26s.net),
|
||||
[tomalacalle.net](https://tomalacalle.net) y [takethestreet.net](https://takethestreet.net).
|
||||
El despliegue en uso es [app.occupymedia.cc](https://app.occupymedia.cc).
|
||||
|
||||
Seguimos la rama `main` de upstream y mandamos allí los cambios que les sirvan.
|
||||
Lo propio de T-HUB va en este repo. Para revisar y validar los cambios, ver
|
||||
[docs/T-HUB-VALIDACION.txt](docs/T-HUB-VALIDACION.txt).
|
||||
|
||||
Cambios respecto a upstream:
|
||||
|
||||
- Instagram registrado en conexión y publicación; claves de la app de Meta solo en el entorno del servidor.
|
||||
- Panel **Outreach** para escribir por privado a mano, contacto a contacto, con seguimiento.
|
||||
- RLS forzado en `media_assets` (migración 042).
|
||||
- Estilo de acampada26s.net en la web: negro, blanco, neón `#00e5ff` y títulos en Anton.
|
||||
|
||||
| Remoto | URL |
|
||||
|---|---|
|
||||
| `origin` | `https://gitea.laenre.net/hacklab/T-HUB.git` |
|
||||
| `upstream` | `https://gitlab.moxexley.com/mediatools/tacticalmediahub.git` |
|
||||
|
||||
```bash
|
||||
git fetch upstream && git merge upstream/main
|
||||
```
|
||||
|
||||
## Arquitectura
|
||||
|
||||
### 1. Una cuenta
|
||||
|
||||
Una persona conecta una cuenta del colectivo una sola vez. A partir de ahí
|
||||
redacta, alguien aprueba y el Hub publica. La contraseña de la red social no
|
||||
pasa nunca por el Hub: la red devuelve un token y el Hub lo guarda cifrado.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
P([Persona]) -->|1. Conectar| H[T-HUB]
|
||||
H -->|2. Pantalla de permiso| R[(Red social)]
|
||||
R -->|3. Token| H
|
||||
P -->|4. Redacta| H
|
||||
H -->|5. Aprobación| A([Otra persona del colectivo])
|
||||
A -->|6. Visto bueno| H
|
||||
H -->|7. Publica a su hora| R
|
||||
```
|
||||
|
||||
### 2. Varias cuentas compartidas
|
||||
|
||||
Cada colectivo tiene sus cuentas y sus reglas de aprobación. Un borrador puede
|
||||
ir a varias cuentas a la vez: el Hub crea una copia por destino y cada una pasa
|
||||
la regla de su cuenta (automática, una persona, mayoría o consenso). Lo que deba
|
||||
salir en toda la red sube a la cola del colectivo padre.
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph TTS[TAKE THE STREET]
|
||||
Q[Cola común] --> IG1[Instagram internacional]
|
||||
Q --> M1[Mastodon @takethestreet]
|
||||
end
|
||||
subgraph S26[ACAMPADA 26S]
|
||||
D[Borrador] --> IG2[Instagram @acampada26s]
|
||||
D --> M2[Mastodon @26s]
|
||||
D --> T2[Telegram acampada26s]
|
||||
end
|
||||
subgraph SUB[Acampadas locales]
|
||||
B[Barcelona] --> IG3[Instagram propio]
|
||||
S[Sevilla] --> IG4[Instagram propio]
|
||||
end
|
||||
S26 -->|Sube para toda la red| Q
|
||||
SUB -->|Sube a 26S| D
|
||||
IG2 -.->|Regla: 2 aprobaciones| G{{Aprobación}}
|
||||
M2 -.->|Regla: automática| G
|
||||
```
|
||||
|
||||
### 3. Parte técnica
|
||||
|
||||
La web solo habla con la API. La API guarda el estado en Postgres con
|
||||
aislamiento por colectivo (RLS), los tokens van en un vault cifrado y el
|
||||
envío programado lo hace el scheduler con Temporal. Cada red es un conector;
|
||||
fuera de los conectores no hay código que distinga redes.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Cliente
|
||||
W[apps/web<br/>Next.js]
|
||||
end
|
||||
subgraph Control
|
||||
API[apps/api<br/>NestJS]
|
||||
SCH[apps/scheduler<br/>Temporal worker]
|
||||
REG[ConnectorRegistry<br/>packages/connector-*]
|
||||
end
|
||||
subgraph Datos
|
||||
PG[(PostgreSQL<br/>RLS por colectivo)]
|
||||
RD[(Redis<br/>estado OAuth 10 min)]
|
||||
V[Vault<br/>AES-256-GCM + KEK]
|
||||
S3[(S3 / R2<br/>medios cifrados)]
|
||||
end
|
||||
subgraph Medios
|
||||
MW[apps/media-worker<br/>FFmpeg]
|
||||
PT[PeerTube<br/>tube.occupymedia.cc]
|
||||
end
|
||||
subgraph Redes
|
||||
MA[Mastodon API]
|
||||
IG[Graph API<br/>Instagram]
|
||||
TG[Telegram Bot API]
|
||||
end
|
||||
|
||||
W -->|HTTPS + sesión| API
|
||||
API --> PG
|
||||
API --> RD
|
||||
API --> V
|
||||
V --> PG
|
||||
API -->|connect / identity| REG
|
||||
SCH -->|publish| REG
|
||||
SCH --> PG
|
||||
REG -->|providerFetch<br/>control de salida| MA
|
||||
REG --> IG
|
||||
REG --> TG
|
||||
MW --> S3
|
||||
MW -->|RTMP| PT
|
||||
```
|
||||
|
||||
Conectar una cuenta (OAuth con `state` de un solo uso y PKCE donde la red lo admite):
|
||||
|
||||
1. `POST /connections/authorize`: la API guarda en Redis, cifrado, el estado de la petición y devuelve la URL de la red.
|
||||
2. La persona acepta en la red y la red vuelve a `GET /connections/callback`.
|
||||
3. La API comprueba `state`, cookie y permisos, canjea el código y guarda el token en el vault.
|
||||
|
||||
## Estado por red
|
||||
|
||||
| Red | Conectar | Publicar | Notas |
|
||||
|---|---|---|---|
|
||||
| Mastodon | Sí | Texto | Sin claves de servidor; cada cuenta registra la app en su instancia. |
|
||||
| Telegram | Sí | Texto | Token del bot de BotFather. |
|
||||
| Instagram | Sí, con `INSTAGRAM_CLIENT_ID/SECRET` | No todavía | Meta descarga la imagen de una URL pública y los medios están cifrados: falta un endpoint temporal. Ver [docs/oauth-connect.md](docs/oauth-connect.md). |
|
||||
| PeerTube | No | Directo sí | Falta el puente OAuth en tube.occupymedia.cc. |
|
||||
| X | No | No | Sin conector. API de pago por uso. |
|
||||
| TikTok | No | No | Fuera de alcance: la API exige auditoría y sin ella solo publica en privado. |
|
||||
|
||||
## Panel Outreach (mensajes privados)
|
||||
|
||||
Lista de cuentas públicas (artistas, colectivos, medios) a las que escribir por
|
||||
privado para pedir difusión. Cada contacto tiene su mensaje, se puede rellenar
|
||||
con una plantilla (`{name}`, `{handle}`) y se marca enviado, respondido o
|
||||
rechazado. Se importa en CSV:
|
||||
|
||||
```
|
||||
name,platform,handle,profileUrl,category,language
|
||||
Climáximo,bluesky,climaximopt.bsky.social,https://bsky.app/profile/climaximopt.bsky.social,clima,pt
|
||||
```
|
||||
|
||||
El Hub **no envía** los mensajes. "Copy & open profile" copia el texto y abre el
|
||||
perfil; la persona lo pega y lo envía desde la propia red. Motivos:
|
||||
|
||||
| Red | Abrir un privado por API | Problema |
|
||||
|---|---|---|
|
||||
| Instagram | No | La API solo deja responder a quien te escribió en las últimas 24 h. |
|
||||
| X | Sí, de pago | Mensajes en masa a desconocidos van contra sus normas de automatización: suspensión. |
|
||||
| Mastodon | Sí (mención privada) | Las instancias silencian o bloquean cuentas que lo hacen en masa; puede caer la instancia entera. |
|
||||
| Bluesky | Sí | Mismo criterio de spam; los chats de desconocidos suelen estar cerrados. |
|
||||
|
||||
El contador de la pantalla avisa a partir de 20 primeros mensajes al día por
|
||||
colectivo. Repartidlos entre varias personas y días, y personalizad cada uno.
|
||||
|
||||
## Requisitos
|
||||
|
||||
Medido en el despliegue actual (ver
|
||||
[docs/ops/2026-10-12-cloud-hosting-requirements.md](docs/ops/2026-10-12-cloud-hosting-requirements.md)).
|
||||
Los medios van a R2, así que el disco no crece con los vídeos.
|
||||
|
||||
| Nivel | CPU | RAM | Disco | Para qué |
|
||||
|---|---|---|---|---|
|
||||
| Mínimo | 4 vCPU | 8 GB | 160 GB SSD | Todo el stack, transcodificación fuera de la máquina |
|
||||
| Recomendado | 8 vCPU | 16 GB | 240 GB SSD | Directos, transcodificación en la máquina, Collabora |
|
||||
| Nodo de render (opcional) | 4+ núcleos o GPU con NVENC | 8 GB | 50 GB | Se une por Tailscale; nunca guarda claves |
|
||||
|
||||
Uso real hoy: unos 2,6 GB de RAM entre todos los servicios.
|
||||
|
||||
Ancho de banda:
|
||||
|
||||
| Uso | Necesario |
|
||||
|---|---|
|
||||
| Panel, publicación, APIs de redes | < 1 Mbps sostenido |
|
||||
| Un directo saliente (1080p30 hacia PeerTube) | 6 Mbps de subida por directo; 3 Mbps a 720p |
|
||||
| Espectadores | Los sirve Cloudflare desde la caché de HLS; el origen no escala con la audiencia |
|
||||
| Subidas de vídeo | Picos de lo que dé la línea; límite de 2 GiB por archivo |
|
||||
|
||||
Coste orientativo: VPS de 30 €/mes + R2 de 1 a 5 $/mes. Cloudflare en plan gratuito.
|
||||
|
||||
## Red y puertos
|
||||
|
||||
Entrada desde internet: **ninguna** directa. Todo entra por el túnel de
|
||||
Cloudflare; el nginx de borde solo acepta rangos de Cloudflare en 80/443.
|
||||
|
||||
| Puerto | Servicio | Exponer a |
|
||||
|---|---|---|
|
||||
| 443/tcp | nginx de borde | Solo rangos de Cloudflare |
|
||||
| 80/tcp | nginx de borde (redirección) | Solo rangos de Cloudflare |
|
||||
| 3000/tcp | API | Solo 127.0.0.1 |
|
||||
| 3001/tcp | Web | Solo 127.0.0.1 |
|
||||
| 9000/tcp | PeerTube | Solo 127.0.0.1 o Tailscale |
|
||||
| 1935/tcp | RTMP de PeerTube | Solo Tailscale (worker de medios) |
|
||||
| 6167/tcp | Matrix (conduwuit) | Solo 127.0.0.1 |
|
||||
| 9200, 9980/tcp | oCIS, Collabora | Solo 127.0.0.1 |
|
||||
| 5432, 6379, 7233 | Postgres, Redis, Temporal | Nunca fuera de la red de Docker |
|
||||
|
||||
Salida que hace falta: 443/tcp a las APIs de las redes y a R2, 7844/tcp+udp
|
||||
para `cloudflared`, 41641/udp para Tailscale.
|
||||
|
||||
Docker publica puertos saltándose ufw. Para cerrarlos: publicar en `127.0.0.1:PUERTO`
|
||||
o en la IP de Tailscale, o añadir reglas en la cadena `DOCKER-USER`.
|
||||
|
||||
## Seguridad y monitorización
|
||||
|
||||
Prácticas para cualquier despliegue. Los fallos de seguridad se comunican en
|
||||
privado a quien mantiene el código, no en issues públicas.
|
||||
|
||||
- **Registro solo por invitación.** Un registro abierto da a cualquiera un colectivo propio y permisos de Owner sobre él.
|
||||
- **Un secreto por servicio.** La web y los workers no necesitan `SUPERPOSTER_KEK`, `S3_KEK` ni `SESSION_SECRET`. Claves distintas para el vault y para los medios.
|
||||
- **FFmpeg siempre en sandbox** (`NODE_ENV=production`, `MEDIA_SANDBOX=on`), sin red y sin variables de entorno heredadas.
|
||||
- **Cabeceras**: CSP estricta, HSTS, `X-Content-Type-Options`, `Referrer-Policy` en web y API.
|
||||
- **Límites de peticiones** en `/auth/*`, invitaciones, OIDC y subidas públicas, por `CF-Connecting-IP`.
|
||||
- **Metadatos fuera**: quitar EXIF/GPS de fotos y vídeos antes de guardarlos o publicarlos.
|
||||
- **Copias cifradas** (`age` o GPG) y fuera de la máquina; probar la restauración.
|
||||
- **Auditoría inalterable**: quitar `UPDATE`/`DELETE` sobre `audit_log` al rol `sp_app` y anclar fuera la cabeza de la cadena.
|
||||
|
||||
Qué vigilar (Prometheus + Grafana ya están en `infra/`; Grafana nunca con acceso anónimo):
|
||||
|
||||
| Señal | Alerta si |
|
||||
|---|---|
|
||||
| `401`/`403` en `/auth/login` por IP | > 20 en 5 min |
|
||||
| Registros nuevos / invitaciones canjeadas | Cualquiera fuera de lo previsto |
|
||||
| Eventos `audit_log`: `member.*`, `connection.removed`, `group.deleted`, `outreach.removed` | Picos o actores raros |
|
||||
| `connections` en estado `needs_credentials` | Cualquiera: token revocado o caducado |
|
||||
| Errores 5xx de la API | > 1 % durante 5 min |
|
||||
| Memoria de API y workers | > 80 % |
|
||||
| Cola de publicación `failed`/`unknown` | Cualquiera |
|
||||
| Logins al panel de Cloudflare y cambios de DNS | Siempre |
|
||||
|
||||
## Desarrollo
|
||||
|
||||
```bash
|
||||
corepack enable
|
||||
pnpm install
|
||||
pnpm exec turbo run build typecheck --filter=@tactical-media-broadcast-hub/api...
|
||||
pnpm --filter @tactical-media-broadcast-hub/connector-instagram test
|
||||
```
|
||||
|
||||
Los tests de integración de `apps/api` que usan Postgres se saltan si no hay
|
||||
base de datos configurada.
|
||||
|
||||
---
|
||||
|
||||
## Upstream: Tactical Media Broadcast Hub
|
||||
|
||||
Self-hosted multi-tenant social / video / livestream management platform with a
|
||||
generic connector system and a Virtual Channel / Auto Director subsystem.
|
||||
|
|
@ -8,7 +263,7 @@ concentrating access in one person's hands. Every credential lives in an
|
|||
encrypted vault. Every editorial decision lands in a tamper-evident audit log.
|
||||
Every feature is multi-tenant with row-level security.
|
||||
|
||||
## Features
|
||||
### Features
|
||||
|
||||
- **Publishing pipeline** — drafts → group approval → schedule → fenced dispatch → receipts. Per-connection publish governance: `auto`, one-admin, majority or consensus sign-off per account — boosts included. Multi-platform fan-out (Mastodon, Telegram, Bluesky, PeerTube, Facebook, Instagram, Threads, LinkedIn, Reddit, Twitch, Discord, Slack). DST-proof scheduling.
|
||||
- **Files SSO** — the Hub is the OIDC issuer for oCIS Drive: one sign-in, auto-provisioned personal drives, rotating refresh tokens that die with your Hub session (logout propagates).
|
||||
|
|
@ -20,7 +275,7 @@ Every feature is multi-tenant with row-level security.
|
|||
- **Archive** — content-addressed encrypted S3, dedup, full-text search, CSV/JSON export. Deleted platform content stays citable.
|
||||
- **Analytics** — month-partitioned snapshots, current + previous month with deltas, per-provider breakdown.
|
||||
|
||||
## Quickstart
|
||||
### Quickstart
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
|
|
@ -38,7 +293,7 @@ curl localhost:3000/healthz
|
|||
curl localhost:3000/readyz
|
||||
```
|
||||
|
||||
## Documentation
|
||||
### Documentation
|
||||
|
||||
| Section | Path |
|
||||
|---|---|
|
||||
|
|
@ -49,13 +304,13 @@ curl localhost:3000/readyz
|
|||
| Streaming architecture | [`docs/design/streaming-architecture.md`](docs/design/streaming-architecture.md) |
|
||||
| Deployment (occupymedia.cc) | [`infra/occupymedia/DEPLOY.md`](infra/occupymedia/DEPLOY.md) |
|
||||
|
||||
## Deterministic live-source testing
|
||||
### Deterministic live-source testing
|
||||
|
||||
```bash
|
||||
pnpm --filter @tactical-media-broadcast-hub/media-worker soak -- --hours 8 --out
|
||||
```
|
||||
|
||||
## Security & trust model
|
||||
### Security & trust model
|
||||
|
||||
Self-hosted, end to end — no corporate cloud, no accounts needed to run the
|
||||
system itself. Multi-tenant with row-level security; platform credentials in an
|
||||
|
|
|
|||
156
docs/T-HUB-VALIDACION.txt
Normal file
156
docs/T-HUB-VALIDACION.txt
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
T-HUB - Guía de validación de cambios
|
||||
=====================================
|
||||
Para: quien revise y valide los cambios de T-HUB antes de llevarlos a
|
||||
upstream (gitlab.moxexley.com/mediatools/tacticalmediahub) o a producción.
|
||||
Base: upstream main a6843e9.
|
||||
|
||||
Commits a revisar (en orden):
|
||||
|
||||
feat: wire the Instagram connector into connect and publishing
|
||||
feat: outreach contacts - track hand-written DMs per collective
|
||||
chore(db): force row-level security on media_assets
|
||||
style / docs: estilo de la web, logo y documentación
|
||||
|
||||
Cada commit se puede revisar y aplicar por separado.
|
||||
|
||||
|
||||
0. PREPARACIÓN
|
||||
--------------
|
||||
corepack enable
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm exec turbo run build typecheck --filter=@tactical-media-broadcast-hub/api...
|
||||
|
||||
Para los tests que usan Postgres hacen falta las variables *_IT_DATABASE_URL
|
||||
de cada suite; sin ellas se saltan (comportamiento de upstream).
|
||||
|
||||
Lint: apps/api ya tenía 54 errores antes de estos cambios y sigue teniendo
|
||||
los mismos 54. Los archivos tocados pasan eslint sin errores.
|
||||
|
||||
|
||||
1. INSTAGRAM
|
||||
----------------------
|
||||
Qué cambia
|
||||
- createInstagramConnector registrado en
|
||||
apps/api/src/connections/connector-registry.ts y en connectorFor de
|
||||
apps/api/src/publishing/publishing.service.ts.
|
||||
- Política de publicación instagram: { idempotency: "none" } en
|
||||
packages/connector-runtime/src/policy.ts (Graph no tiene clave de
|
||||
idempotencia; un envío dudoso no se repite).
|
||||
- Las credenciales de la app de Meta se leen SOLO de
|
||||
INSTAGRAM_CLIENT_ID / INSTAGRAM_CLIENT_SECRET. Se quitan los campos
|
||||
clientId/clientSecret del formulario y ya no se copian al vault.
|
||||
- El proveedor sale desactivado (connectEnabled=false) sin esas dos
|
||||
variables, con una nota que lo explica.
|
||||
- Si la persona autoriza varias cuentas de Instagram en el diálogo de
|
||||
Meta, la conexión falla en vez de elegir la primera.
|
||||
|
||||
Tests automáticos
|
||||
pnpm --filter @tactical-media-broadcast-hub/connector-instagram test
|
||||
esperado: 37 pass
|
||||
cd apps/api && npx vitest run src/connections
|
||||
esperado: 18 pass (incluye "offers Instagram only with the Hub's
|
||||
Facebook app configured, and keeps the app secret out of the browser")
|
||||
|
||||
Prueba manual (entorno de pruebas, app de Meta en modo desarrollo)
|
||||
1. Sin variables: GET /connections/providers -> instagram enabled=false.
|
||||
2. Con INSTAGRAM_CLIENT_ID/SECRET: enabled=true.
|
||||
3. En la app de Meta, Valid OAuth Redirect URI =
|
||||
${API_URL}/connections/callback (exacto).
|
||||
4. Canales -> Conectar Instagram con una cuenta Tester Business/Creator
|
||||
vinculada a una página -> vuelve con connect=success y la conexión
|
||||
sale healthy con @usuario.
|
||||
5. Autorizar dos cuentas a la vez -> connect=failure.
|
||||
|
||||
Límites conocidos (no resueltos en este commit)
|
||||
- Publicar imágenes NO funciona todavía: Meta descarga la imagen de una
|
||||
URL pública y los medios están cifrados en el almacén. Hace falta un
|
||||
endpoint temporal que sirva el JPEG descifrado (TTL corto, solo
|
||||
borradores aprobados). Además la regla del proyecto es que los bytes
|
||||
no pasen por la API. Decisión de diseño pendiente.
|
||||
- refreshCredentials usa grant_type=ig_refresh_token contra
|
||||
graph.facebook.com, que es el método de "Instagram Login", no de
|
||||
Facebook Login. Comprobar con un token real; posible caducidad a 60
|
||||
días.
|
||||
|
||||
|
||||
2. OUTREACH
|
||||
---------------------
|
||||
Qué cambia
|
||||
- Migración 041_outreach.sql: tabla outreach_contacts con RLS
|
||||
ENABLE+FORCE y WITH CHECK, índice único (workspace, platform,
|
||||
lower(handle)), trigger de workspace inmutable, permisos
|
||||
outreach.view (Owner, Admin, Editor, Moderator) y outreach.manage
|
||||
(Owner, Admin, Editor).
|
||||
- API: GET/POST/PATCH/DELETE /outreach/contacts (apps/api/src/outreach).
|
||||
Importación de hasta 500 por petición; las cuentas ya existentes se
|
||||
saltan. La auditoría guarda altas, bajas y cambios de estado, nunca el
|
||||
texto del mensaje.
|
||||
- Web: /outreach (barra lateral y "More"). Importar CSV, plantilla con
|
||||
{name} y {handle}, filtros, "Copy & open profile", contador diario
|
||||
con aviso a partir de 20.
|
||||
- El Hub no envía mensajes. No hay llamadas a APIs de redes en este
|
||||
módulo. Ver README, sección "Panel Outreach", para el motivo.
|
||||
|
||||
Tests automáticos
|
||||
cd apps/api && npx vitest run src/outreach esperado: 8 pass
|
||||
pnpm --filter @tactical-media-broadcast-hub/web test:outreach
|
||||
esperado: 4 pass
|
||||
|
||||
Verificado además (fuera del repo): la migración 041 y todas las
|
||||
consultas del servicio se ejecutaron sobre PGlite (Postgres compilado a WASM)
|
||||
como rol sin privilegios con app.workspace_id fijado: aislamiento entre
|
||||
colectivos, salto de duplicados en mayúsculas/minúsculas, sellos de
|
||||
sent_at/replied_at, vuelta a "todo" limpia los sellos.
|
||||
|
||||
Prueba manual
|
||||
1. Aplicar migraciones (src/db/migrate.cli.ts).
|
||||
2. Como Editor: importar
|
||||
name,platform,handle,profileUrl,category,language
|
||||
Prueba,bluesky,prueba.bsky.social,https://bsky.app/profile/prueba.bsky.social,test,es
|
||||
-> "1 contacts added". Reimportar -> "0 added, 1 already on the list".
|
||||
3. Plantilla "Hola {name}" -> el contacto pendiente recibe "Hola Prueba".
|
||||
4. Copy & open profile -> abre el perfil en otra pestaña con el texto
|
||||
en el portapapeles.
|
||||
5. Mark sent -> estado sent y sello de fecha; aparece outreach.sent en
|
||||
/audit sin el texto del mensaje.
|
||||
6. Como Analyst (sin outreach.view) -> GET /outreach/contacts da 403.
|
||||
7. Desde otro colectivo -> la lista no muestra los contactos del primero
|
||||
y PATCH sobre su id da 404.
|
||||
8. profileUrl con http:// o javascript: -> 400.
|
||||
|
||||
|
||||
3. RLS EN media_assets
|
||||
----------------------
|
||||
Qué cambia
|
||||
- 042_media_assets_rls.sql: ENABLE + FORCE RLS y política con
|
||||
WITH CHECK.
|
||||
- Las consultas a media_assets (media.service.ts, publishing.service.ts)
|
||||
filtran también por app.workspace_id.
|
||||
|
||||
Verificación
|
||||
- cd apps/api && npx vitest run src/media src/publishing
|
||||
esperado: 114 pass, 49 skipped (necesitan Postgres).
|
||||
- En la base de datos, después de migrar:
|
||||
SELECT relrowsecurity, relforcerowsecurity
|
||||
FROM pg_class WHERE relname = 'media_assets'; -> t, t
|
||||
- Prueba manual: subir un medio en el colectivo A y pedir
|
||||
GET /media/<id> con sesión del colectivo B -> 404.
|
||||
|
||||
|
||||
4. ESTILO Y LOGO
|
||||
----------------
|
||||
- apps/web/src/app/globals.css: paleta de acampada26s.net sobre fondo
|
||||
oscuro (negro, blanco, neón #00e5ff), títulos h1/h2 en Anton en
|
||||
mayúsculas. Regla que pone texto negro en botones neón.
|
||||
- Fuente Anton en apps/web/public/fonts (licencia SIL OFL incluida).
|
||||
- Logo: docs/brand/t-hub.svg, también como favicon.
|
||||
- Página pública de buzón (/submit/[token]) con el mismo color.
|
||||
Comprobar visualmente: login, Compose, Approvals, Channels, Outreach,
|
||||
Control Room y /submit/<token>, en escritorio y móvil. Buscar textos
|
||||
blancos sobre neón o bloques que hayan quedado con el rojo anterior.
|
||||
|
||||
|
||||
5. SEGURIDAD
|
||||
------------
|
||||
La revisión de seguridad se entrega en privado a quien mantiene el
|
||||
despliegue. Los fallos se comunican por canal privado, no en issues.
|
||||
Loading…
Add table
Add a link
Reference in a new issue