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

442 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 📘 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_<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. 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=<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 d’environnement 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`)
```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_<workspaceId>_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).