feat: intégration TikTak #197-200 + scoped tokens, webhooks filtrés, listes mtime/size ; fix e2e-server BUG-112
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
# Intégration TikTak ↔ ObsiGate — socle côté ObsiGate (#197 → #200)
|
||||
|
||||
> **Statut :** ✅ livré — **IDs stables :** #197, #198, #199, #200
|
||||
> **Issues Gitea :** Projets/ObsiGate #4, #3, #2, #1 (même ordre)
|
||||
> **Référence d'analyse :** `Projets/ticktick-clone`, `docs/SYNCHRO-OBSIGATE.md` v1.1
|
||||
> (décisions §8 : vault dédié « TikTak », transport API REST, TikTak maître en conflit)
|
||||
|
||||
## Contexte
|
||||
|
||||
TikTak projette ses tâches/notes en fichiers Markdown dans un vault ObsiGate dédié
|
||||
via l'API REST fichiers. Côté ObsiGate, quatre manques bloquaient une synchronisation
|
||||
propre : réconciliation sans relire chaque fichier (#197), webhooks filtrables (#198),
|
||||
clé API à moindre privilège (#199), procédure de provisionnement (#200).
|
||||
|
||||
## #197 — Listes de fichiers : `mtime` + `size` (Gitea #4)
|
||||
|
||||
**Constat.** `GET /api/vault/{vault}/files` et l'outil MCP `list_all_files` exposaient
|
||||
déjà `size`/`modified`, mais pas `GET /api/vault/{vault}/paths` (seulement
|
||||
`vault/path/name/type`), ni `GET /api/browse/{vault}`, ni l'outil `list_directory`.
|
||||
|
||||
**Livré.**
|
||||
- `GET /api/vault/{vault}/paths` : chaque entrée fichier expose `size`, `mtime`
|
||||
(epoch) et `mtime_iso` (issus de l'index — source unique, même contrat que
|
||||
`list_all_files`) ; les répertoires portent `None`.
|
||||
- `GET /api/browse/{vault}` : `modified`/`modified_iso` sur les fichiers.
|
||||
- Outil MCP `list_directory` : `size`/`mtime`/`mtime_iso` sur les fichiers.
|
||||
- Cohérence read-after-write : `PUT /api/file/{vault}/save` rafraîchit l'index de
|
||||
façon synchrone (`update_single_file`), au lieu de dépendre du watcher.
|
||||
- **Bug latent corrigé au passage** : `update_single_file`/`remove_single_file`
|
||||
exigeaient un chemin absolu alors que toutes les routes leur passent un chemin
|
||||
relatif au vault — le rafraîchissement post-écriture était un no-op silencieux
|
||||
(masqué en prod par le watcher). Les deux fonctions acceptent désormais les deux
|
||||
formes.
|
||||
|
||||
**Réconciliation type (un seul appel) :** snapshot `{path: (mtime, size)}`, puis
|
||||
re-liste et comparaison — démontré par
|
||||
`tests/test_sync_lists.py::TestReconciliationScenario`.
|
||||
|
||||
## #198 — Webhooks : `vault` + filtre `vault` / `path_prefix` (Gitea #3)
|
||||
|
||||
**Constat.** Les charges utiles contenaient déjà `vault`, mais sans filtre un
|
||||
consommateur recevait tous les vaults. De plus `file_modified` (événement souscrit
|
||||
mais jamais émis) manquait pour tirer les retours de synchro.
|
||||
|
||||
**Livré.**
|
||||
- Filtres optionnels par webhook : `vault` (égalité exacte) et `path_prefix`
|
||||
(égalité ou sous-arbre — `Maison` matche `Maison/a.md`, pas `Maison2/a.md` ;
|
||||
sur renommage/déplacement, `old_path` **ou** `new_path` suffit pour être prévenu).
|
||||
Sans filtre : reçoit tout, comme avant (compatibilité).
|
||||
- `POST /api/webhooks` accepte `vault`/`path_prefix` ; `PATCH` les modifie
|
||||
(`""` réinitialise) ; `GET` les expose (`WebhookModel.vault/path_prefix`).
|
||||
- `PUT /api/file/{vault}/save` émet désormais `file_modified`
|
||||
(`{"vault", "path"}`) — la sauvegarde tableur/IA reste hors périmètre (documenté).
|
||||
- UI : champs filtre à la création + badge dans la liste ; aide intégrée FR/EN
|
||||
mise à jour (`file_modified` émis, filtrage documenté).
|
||||
- Filtrage pur et testable : `backend/webhooks.py::webhook_matches` ;
|
||||
`tests/test_webhook_filters.py` (matching, persistance, dispatch simulé :
|
||||
le webhook filtré sur « TikTak » ne reçoit que ses événements, avec
|
||||
`vault: "TikTak"` dans la charge).
|
||||
|
||||
## #199 — Clés API à portée limitée (Gitea #2)
|
||||
|
||||
**Constat.** Les clés longue durée portaient toutes les permissions du compte.
|
||||
|
||||
**Livré.**
|
||||
- Création : `POST /api/auth/tokens` accepte `vaults` (sous-ensemble des vaults
|
||||
du créateur, défaut = tous — `400` si inconnu, `403` si hors d'accès) et
|
||||
`scope` (`"full"` défaut | `"files"`).
|
||||
- Restriction vault : le JWT embarque les vaults autorisés → `check_vault_access`
|
||||
(REST) et `require_vault_access` (MCP/outils) refusent le reste en 403.
|
||||
- Périmètre `files` : fichiers (CRUD, listes, recherche, backups lecture/restauration),
|
||||
`GET /api/vaults`, `/api/search*`, `/mcp`. Tout le reste (admin, config, IA,
|
||||
webhooks, partages, **gestion des clés elle-même** — anti-escalade) → **403**
|
||||
via `TokenScopeMiddleware` (`backend/auth/scope.py`, stateless : claim lu sans
|
||||
vérification pour le refus, signature toujours vérifiée en aval).
|
||||
- MCP : `call_tool` + `propose_*` refusent les outils hors `FILES_SCOPE_TOOLS`
|
||||
(`token_scope_denied`), `list_tools` filtré pour ces tokens, ressources
|
||||
`vault://` couvertes.
|
||||
- UI : sélecteur de portée + champ vaults à la création, badge de portée et
|
||||
vaults dans la liste (i18n FR/EN).
|
||||
- Compatibilité : clés existantes (sans `scope`) = `full`, comportement inchangé.
|
||||
- Tests : `tests/test_scoped_tokens.py` (13 cas : création/validation, lecture/
|
||||
écriture OK, 403 autre vault, 403 hors périmètre, non-régression full, outils).
|
||||
|
||||
## #200 — Provisionnement du vault « TikTak » (Gitea #1, exploitation)
|
||||
|
||||
Opération unique côté hébergement (aucun code requis — endpoints déjà testés) :
|
||||
|
||||
```bash
|
||||
# 1. Dossier sur l'hôte
|
||||
mkdir -p /NFS/OBSIDIAN_DOC/TikTak
|
||||
|
||||
# 2. Montage conteneur (docker-compose.yml, puis recreate)
|
||||
# volumes:
|
||||
# - /NFS/OBSIDIAN_DOC/TikTak:/vaults/TikTak
|
||||
|
||||
# 3. Déclarer le vault SANS redémarrage (admin, persisté via data/vaults.json)
|
||||
curl -X POST $BASE/api/vaults/add -H "Authorization: Bearer $ADMIN" \
|
||||
-H 'Content-Type: application/json' -d '{"name":"TikTak","path":"/vaults/TikTak"}'
|
||||
|
||||
# 4. Octroyer le vault au compte de synchro (ACL par vault, UI admin ou API)
|
||||
|
||||
# 5. Clé à portée limitée pour TikTak (cf. #199)
|
||||
curl -X POST $BASE/api/auth/tokens -H "Authorization: Bearer $ADMIN" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"name":"TikTak sync","expiry":"never","vaults":["TikTak"],"scope":"files"}'
|
||||
|
||||
# 6. Webhook retour filtré (optionnel, phase 3 TikTak)
|
||||
curl -X POST $BASE/api/webhooks -H "Authorization: Bearer $ADMIN" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"name":"TikTak retour","url":"https://tiktak…/hooks/obsigate","events":["file_created","file_modified","file_deleted","file_renamed"],"vault":"TikTak"}'
|
||||
```
|
||||
|
||||
**Acceptation :** le vault « TikTak » est visible ; un `.md` créé via
|
||||
`POST /api/file/TikTak` (clé limitée, pas admin) est lu, cherché et édité sans erreur :
|
||||
|
||||
```bash
|
||||
KEY=<clé de l'étape 5>
|
||||
curl -X POST $BASE/api/file/TikTak -H "Authorization: Bearer $KEY" \
|
||||
-H 'Content-Type: application/json' -d '{"path":"Inbox/sonde.md","content":"# sonde\n"}'
|
||||
curl -s "$BASE/api/file/TikTak/raw?path=Inbox/sonde.md" -H "Authorization: Bearer $KEY"
|
||||
curl -s "$BASE/api/search?q=sonde&vault=TikTak" -H "Authorization: Bearer $KEY"
|
||||
```
|
||||
|
||||
## Fichiers touchés (rappel)
|
||||
|
||||
| # | Backend | Frontend | Tests |
|
||||
|---|---|---|---|
|
||||
| 197 | `services/search.py`, `services/vaults.py`, `schemas.py`, `indexer.py`, `routers/files_write.py`, `tools/service.py` | — | `tests/test_sync_lists.py` |
|
||||
| 198 | `webhooks.py`, `schemas.py`, `routers/webhooks.py`, `routers/files_write.py` | `js/config.js`, `index.html`, `locales/{fr,en}.json` | `tests/test_webhook_filters.py` |
|
||||
| 199 | `auth/jwt_handler.py`, `auth/middleware.py`, `auth/scope.py` (nouveau), `auth/router.py`, `main.py`, `tools/context.py`, `tools/registry.py`, `tools/api.py`, `mcp/server.py`, `openapi_docs.py` | `js/config.js`, `index.html`, `locales/{fr,en}.json` | `tests/test_scoped_tokens.py` |
|
||||
| 200 | — (runbook) | — | — (recette manuelle ci-dessus) |
|
||||
Reference in New Issue
Block a user