# 📘 Document d’architecture — ObsiViewer **Hub + Pods** (Workspaces provisionnĂ©s) > Version: 1.0 — DerniĂšre mise Ă  jour: aujourd’hui > PortĂ©e: architecture complĂšte, authentification, gestion utilisateurs/roles, workspaces, dĂ©ploiement des pods, panneau d’administration, stockage des prĂ©fĂ©rences, intĂ©gration avec l’image Docker **`obsiviewer-angular:latest`** (ton image actuelle). --- ## 1) Objectifs & principes ### 🎯 Objectifs * Unifier l’accĂšs Ă  **plusieurs voutes** (workspaces) via un **Hub** central. * CrĂ©er/arrĂȘter/supprimer des **workspaces via l’UI** (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 d’ensemble ### 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 d’admin (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__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-.local/?token=...` 4. Pod valide JWT (clĂ© publique Hub), vĂ©rifie `workspace_id` 5. L’app 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 s’il n’existe 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**: 5–15 minutes * **Claims** (ex.) : ```json { "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) ```sql 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 d’invitation expires_at TEXT, accepted_by_user_id TEXT NULL ); ``` ### Pod (SQLite locale `/app/db/app.sqlite`) ```sql 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) ```json { "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=` → `{ 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: ` → 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//vault:/app/vault /data/workspaces//assets:/app/assets /data/workspaces//db:/app/db /srv/hub/keys:/keys:ro # hub_rsa.pub ``` ### Variables d’environnement Pod ``` PORT=4000 NODE_ENV=production TZ=America/Montreal WORKSPACE_ID= REQUIRE_JWT=true HUB_JWT_PUBLIC_KEY_FILE=/keys/hub_rsa.pub ``` ### Labels Traefik (ex.) ``` "traefik.enable": "true" "traefik.http.routers.obsiviewer-.rule": "Host(`obsiviewer-.local`)" "traefik.http.services.obsiviewer-.loadbalancer.server.port": "4000" ``` ### Exemple (Node/TS, `dockerode`) ```ts 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__notes` * Le Pod connaĂźt `WORKSPACE_ID`, donc paramĂštre l’index au dĂ©marrage. * PrĂ©fĂ©rences de recherche (regex/case
) = cĂŽtĂ© client (user_prefs) + params de requĂȘte. --- ## 10) Panneau d’administration (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 d’email), 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 d’un 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 l’API 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 ```yaml 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) ```yaml 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 d’administration. * **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** d’installation Traefik (labels, wildcard DNS).