ObsiViewer/docs/ARCHITECTURE/HUB_WORKSPACE_ARCHITECTURE.md
Bruno Charest b1da9b111d feat: reorganize context menu documentation into docs folder
- Moved CONTEXT_MENU_INDEX.md and CONTEXT_MENU_VERIFICATION.md into docs/ directory for better organization
- Consolidated all context menu documentation files in one location for easier maintenance
- Documentation remains complete with 1000+ lines covering implementation, integration, and verification

The change improves documentation structure by moving context menu related files into a dedicated docs folder, making it easier for developers to find an
2025-10-25 20:17:10 -04:00

13 KiB
Raw Blame History

📘 Document darchitecture — ObsiViewer Hub + Pods (Workspaces provisionnés)

Version: 1.0 — Dernière mise à jour: aujourdhui Portée: architecture complète, authentification, gestion utilisateurs/roles, workspaces, déploiement des pods, panneau dadministration, stockage des préférences, intégration avec limage Docker obsiviewer-angular:latest (ton image actuelle).


1) Objectifs & principes

🎯 Objectifs

  • Unifier laccès à plusieurs voutes (workspaces) via un Hub central.
  • Créer/arrêter/supprimer des workspaces via lUI (sans manipuler docker-compose à la main).
  • Appliquer RBAC (viewer/editor/admin/owner) par workspace.
  • Isoler chaque workspace dans son pod Docker dédié.
  • Conserver la source de vérité dans les fichiers Markdown/YAML + un config partagé dans la voute.
  • Stocker les préférences privées (par utilisateur et par workspace) en DB locale du pod.

🚫 Non-objectifs

  • Pas de Kubernetes requis (Docker Engine + Traefik suffisent).
  • Pas de synchro “live” entre différentes machines distantes (mais compatible Git/CI/CD).

🧠 Vocabulaire

  • Hub: service central (UI + API) qui gère identité, membres, invitations, et provisionne des Pods via Docker Engine.
  • Pod: conteneur ObsiViewer (ton image obsiviewer-angular:latest) dédié à 1 workspace.
  • Workspace: une voute (répertoire) + ses assets + sa DB locale de préférences.
  • IdP/SSO: Authelia/Authentik/Keycloak derrière reverse proxy.

2) Vue densemble

Composants

  • Reverse Proxy (Traefik/Caddy/Nginx) :

    • Routes: hub.local (Hub), obsiviewer-*.local (Pods)
    • TLS + éventuellement SSO devant le Hub
  • Hub (service Node/TS + SQLite) :

    • UI dadmin (Workspaces, Membres, Invitations)
    • Docker orchestrator (accès /var/run/docker.sock)
    • Authority JWT (RS256) → émet tokens courts signés
    • DB: users, workspaces, memberships, invitations
  • Pods (image obsiviewer-angular:latest) :

    • 1 pod = 1 workspace (montage: vault, assets, db)
    • Middleware JWT (valide signature Hub + workspace_id)
    • API existantes + /api/config (partagé), /api/prefs (privé)
    • DB locale SQLite /app/db/app.sqlite (prefs + historique)
  • MeiliSearch : index par workspace (obsiviewer_<workspaceId>_notes)

Flux « Ouvrir un workspace »

  1. User → Hub UI → liste mes workspaces
  2. Click Open → Hub signe JWT (claims: sub, workspace_id, role, exp)
  3. Redirection vers https://obsiviewer-<id>.local/?token=...
  4. Pod valide JWT (clé publique Hub), vérifie workspace_id
  5. Lapp charge config, prefs, index → RBAC appliqué

3) Sécurité & Authentification

SSO recommandé

  • SSO (Authelia/Authentik) devant le Hub via reverse proxy.
  • Le proxy injecte X-User-Id, X-User-Name, X-User-Email ou forwarde un JWT.
  • Le Hub crée le user sil nexiste pas (id stable = sub).

Émission du JWT Hub → Pods

  • Algorithme: RS256 (clé privée côté Hub, clé publique montée dans chaque Pod)
  • TTL: 515 minutes
  • Claims (ex.) :
{
  "sub": "user-uuid-or-oidc-sub",
  "workspace_id": "it",
  "role": "editor",
  "name": "Bruno Charest",
  "email": "bruno@…",
  "iss": "obsiviewer-hub",
  "iat": 1730,
  "exp": 1730
}

Vérification côté Pod

  • Middleware lit Authorization: Bearer ou ?token=
  • jwt.verify(token, HUB_PUBLIC_KEY, { algorithms: ['RS256'], issuer: 'obsiviewer-hub' })
  • Vérifie payload.workspace_id === process.env.WORKSPACE_ID

RBAC (par route)

  • viewer : lecture seule (notes, assets, graph)
  • editor : CRUD sur fichiers/dossiers/notes
  • admin : + PUT /api/config (config partagée du workspace)
  • owner (option) : tout + gestion des membres & suppression du workspace (géré plutôt au Hub)

4) Modèle de données

Hub (SQLite ou Postgres)

CREATE TABLE users(
  id TEXT PRIMARY KEY,           -- sub issu du SSO
  display_name TEXT,
  email TEXT UNIQUE
);

CREATE TABLE workspaces(
  id TEXT PRIMARY KEY,           -- slug: main, it, recettes...
  name TEXT,
  url TEXT,                      -- ex: https://obsiviewer-it.local
  status TEXT DEFAULT 'active'   -- active/disabled
);

CREATE TABLE memberships(
  user_id TEXT,
  workspace_id TEXT,
  role TEXT CHECK(role IN ('owner','admin','editor','viewer')),
  PRIMARY KEY(user_id, workspace_id)
);

CREATE TABLE invitations(
  id TEXT PRIMARY KEY,           -- uuid
  workspace_id TEXT,
  email TEXT,
  role TEXT CHECK(role IN ('admin','editor','viewer')),
  token TEXT UNIQUE,             -- jeton dinvitation
  expires_at TEXT,
  accepted_by_user_id TEXT NULL
);

Pod (SQLite locale /app/db/app.sqlite)

CREATE TABLE IF NOT EXISTS user_prefs(
  user_id TEXT,
  key TEXT,               -- 'ui','editor','graph','notesList', etc.
  json_value TEXT,
  PRIMARY KEY(user_id, key)
);

CREATE TABLE IF NOT EXISTS search_history(
  user_id TEXT,
  ts TEXT,
  context TEXT,           -- 'notes','graph',...
  query TEXT
);

CREATE TABLE IF NOT EXISTS audit(
  id TEXT PRIMARY KEY, ts TEXT, user_id TEXT, action TEXT, details TEXT
);

5) Configuration & préférences

Partagé (par workspace, dans la voute)

Fichier vault/.obsiviewer/config.json (écriture atomique + .bak + ETag)

{
  "version": 1,
  "ui": { "defaultTheme": "blue-light", "uiMode": "Nimbus" },
  "notesList": { "density": "comfortable", "sort": "updatedAt:desc", "view": "cards" },
  "graph": { "physics": { "repulsion": 2500, "linkDistance": 60 } },
  "folders": { "exclusions": [".obsidian", "attachments"], "colors": { "Projects": "#9b87f5" } },
  "search": { "defaults": { "caseSensitive": false, "useRegex": true } },
  "quickLinks": [{ "label": "Drafts", "query": "status:draft" }]
}

Privé (par user et workspace, en DB du pod)

  • user_prefs: UI préférée, zoom/centre du graphe, onglet par défaut, options éditeur, etc.
  • search_history: 10 dernières requêtes par contexte

Ordre de précédence

Defaults app
  < config.json (partagé per-workspace)
    < user_prefs (privé per-user/workspace)
      < device-local volatil (scroll, pliage, etc.) en localStorage

6) APIs

Hub

  • GET /api/my/workspaces → workspaces accessibles (id, name, url, role)
  • GET /api/token?workspaceId=<id>{ token, url } (JWT court)
  • POST /api/workspaces { name, slug }provisionne (crée dossiers + pod)
  • DELETE /api/workspaces/:id → stop + remove container + purge (option)
  • POST /api/invitations (owner/admin) → créer invitation
  • POST /api/invitations/accept → rattache user au workspace

Pod

  • GET /api/config → lit config.json (+ ETag)
  • PUT /api/config (admin) → If-Match: <etag> → write atomique + .bak
  • GET /api/prefs → agrégat des prefs privées (clé→JSON)
  • PUT /api/prefs → upsert clés/prefs
  • Notes CRUD (existant) → protégé par role >= editor

7) Provisioning des Pods (Hub → Docker)

Montage standard

/data/workspaces/<id>/vault:/app/vault
/data/workspaces/<id>/assets:/app/assets
/data/workspaces/<id>/db:/app/db
/srv/hub/keys:/keys:ro              # hub_rsa.pub

Variables denvironnement Pod

PORT=4000
NODE_ENV=production
TZ=America/Montreal
WORKSPACE_ID=<id>
REQUIRE_JWT=true
HUB_JWT_PUBLIC_KEY_FILE=/keys/hub_rsa.pub

Labels Traefik (ex.)

"traefik.enable": "true"
"traefik.http.routers.obsiviewer-<id>.rule": "Host(`obsiviewer-<id>.local`)"
"traefik.http.services.obsiviewer-<id>.loadbalancer.server.port": "4000"

Exemple (Node/TS, dockerode)

const container = await docker.createContainer({
  Image: "docker-registry.dev.home:5000/obsiviewer-angular:latest",
  name: `obsiviewer-${slug}`,
  Env: [
    "PORT=4000",
    "NODE_ENV=production",
    "TZ=America/Montreal",
    `WORKSPACE_ID=${slug}`,
    "REQUIRE_JWT=true",
    "HUB_JWT_PUBLIC_KEY_FILE=/keys/hub_rsa.pub"
  ],
  HostConfig: {
    Binds: [
      `/data/workspaces/${slug}/vault:/app/vault`,
      `/data/workspaces/${slug}/assets:/app/assets`,
      `/data/workspaces/${slug}/db:/app/db`,
      `/srv/hub/keys:/keys:ro`
    ],
    RestartPolicy: { Name: "unless-stopped" }
  },
  ExposedPorts: { "4000/tcp": {} },
  Labels: {
    "traefik.enable": "true",
    [`traefik.http.routers.obsiviewer-${slug}.rule`]: `Host(\`obsiviewer-${slug}.local\`)`,
    [`traefik.http.services.obsiviewer-${slug}.loadbalancer.server.port`]: "4000"
  }
});
await container.start();

8) Reverse proxy

Traefik — idée de base

  • hub.local → service hub:4100 (protégé SSO)
  • obsiviewer-*.local → services Pods (labels dynamiques)

(Tu peux aussi faire du DNS local type *.local pointant vers le reverse proxy.)


9) Intégration MeiliSearch

  • 1 index par workspace: obsiviewer_<workspaceId>_notes
  • Le Pod connaît WORKSPACE_ID, donc paramètre lindex au démarrage.
  • Préférences de recherche (regex/case…) = côté client (user_prefs) + params de requête.

10) Panneau dadministration (Hub UI)

Sections

  • Workspaces

    • Liste (Nom, Id, URL, Status, Membres, Dernier index)
    • Actions: Create, Open, Stop/Start, Delete, Reindex
  • Members

    • Par workspace: lister membres (role) + Invite (email + rôle)
    • Changer rôle / retirer membre
  • Invitations

    • Générer lien, expiration, statut
  • Settings

    • Clé publique/privée JWT (rotation)
    • Paramètres Meili, SMTP (si envoi demail), base path
  • Diagnostics

    • Logs récents (Hub), santé des Pods, usage disque /data/workspaces

11) Opérations, sauvegardes & reprise

Backups

  • Vaults: sauvegarde fichier régulière (Git + snapshots ZFS/btrfs si possible)
  • Pod DB: sauvegarde /data/workspaces/*/db
  • Hub DB: sauvegarde /srv/hub/db
  • Clés JWT: /srv/hub/keys (privée/publique) → sécuriser & sauvegarder

Reprise après incident

  • Le Hub réconcilie la liste des containers via Docker API, actualise létat en DB si besoin.
  • Une suppression dun Pod ne supprime pas automatiquement vault/assets/db (garde un drapeau “dangling” et propose “Purge files”).

12) Sécurité & bonnes pratiques

  • Tokens courts (10 min) + rotation de clé possible (multi-keys JWKS si besoin).
  • WRITE de config.json uniquement via lAPI du Pod (pas décriture par le client).
  • Écriture atomique: config.tmp + fs.rename + .bak datés.
  • Ajv + schema versionné pour config.json.
  • RBAC strict; endpoints sensibles (PUT /api/config, suppression fichier) → role >= admin.

13) Migration & compatibilité

  • Tes Pods actuels peuvent progressivement activer REQUIRE_JWT=false → tests → true.

  • Écran “Migration des préférences locales” (une fois) :

    • Importer ui-mode, graph-settings, folderFilterConfig, folderColors, search prefs
    • Choisir Mes préférences (user_prefs) vs Défauts du workspace (config.json, admin only)
  • Aucun changement sur le modèle “1 pod = 1 voute”; tu ajoutes juste le Hub par-dessus.


14) Exemples de docker-compose (extraits)

Hub

services:
  hub:
    image: ghcr.io/tonorg/obsiviewer-hub:latest
    container_name: obsiviewer-hub
    environment:
      - NODE_ENV=production
      - PORT=4100
      - TZ=America/Montreal
      - HUB_JWT_PRIVATE_KEY_FILE=/keys/hub_rsa
      - HUB_JWT_PUBLIC_KEY_FILE=/keys/hub_rsa.pub
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ${DIR_HUB}/db:/app/db
      - ${DIR_HUB}/keys:/keys:ro
    ports: ["4100:4100"]
    restart: unless-stopped

Pod (généré par le Hub)

services:
  obsiviewer-it:
    image: docker-registry.dev.home:5000/obsiviewer-angular:latest
    environment:
      - PORT=4000
      - NODE_ENV=production
      - TZ=America/Montreal
      - WORKSPACE_ID=it
      - REQUIRE_JWT=true
      - HUB_JWT_PUBLIC_KEY_FILE=/keys/hub_rsa.pub
    volumes:
      - /data/workspaces/it/vault:/app/vault
      - /data/workspaces/it/assets:/app/assets
      - /data/workspaces/it/db:/app/db
      - ${DIR_HUB}/keys:/keys:ro
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.obsiviewer-it.rule=Host(`obsiviewer-it.local`)"
      - "traefik.http.services.obsiviewer-it.loadbalancer.server.port=4000"
    restart: unless-stopped

15) Roadmap (évolution)

  • 🔒 Liens publics en read-only (URLs signées, expirables) par workspace
  • 🔁 Refresh token Hub (silent refresh côté Pod, optionnel)
  • 📈 Observabilité (Prometheus/Grafana) pour Hub + Pods
  • 🧩 Plugins par workspace (charges dynamiques)
  • 💼 Org/Teams au niveau Hub (groupes → rôles par défaut)

16) Résumé exécutif

  • Concept “Workspace = Voute” conservé → isolation & simplicité.
  • Hub ajoute: identité, membres, invitations, provisioning automatique des Pods, tokens JWT, panel dadministration.
  • Pods restent proches de ton image actuelle, avec un simple middleware JWT + APIs config/prefs + RBAC.
  • Création de workspace via UI = instantané (le Hub crée le dossier + lance le conteneur + publie via Traefik).

Si tu veux, je peux te livrer dans un message suivant :

  • un squelette Hub (Express + dockerode + SQLite + Ajv) prêt à builder,
  • le middleware JWT+RBAC TypeScript côté Pod,
  • et un mini guide dinstallation Traefik (labels, wildcard DNS).