Compare commits

...
6 Commits
Author SHA1 Message Date
bruno 705f755b6b docs: guides d'utilisation, capture reelle et README ameliores
CI / lint (push) Successful in 2m2s
CI / security (push) Successful in 1m25s
CI / test (push) Successful in 4m13s
CI / build (push) Successful in 1m16s
CI / e2e (push) Successful in 11m56s
2026-09-22 22:40:51 -04:00
bruno 8ad8eaac71 test: spec E2E mobile pour la page Configurations BUG-071
CI / lint (push) Successful in 1m54s
CI / security (push) Successful in 1m21s
CI / test (push) Successful in 3m58s
CI / build (push) Failing after 1m22s
CI / e2e (push) Skipped
2026-09-22 22:03:42 -04:00
bruno dd9224e685 fix: page Configurations inutilisable en mode mobile BUG-071
CI / lint (push) Successful in 1m57s
CI / security (push) Successful in 1m20s
CI / test (push) Successful in 4m20s
CI / build (push) Successful in 1m20s
CI / e2e (push) Successful in 11m51s
2026-09-22 21:24:56 -04:00
bruno aeb7516445 fix: activation WebAuthn impossible BUG-070 (rp_id/origines derives requete, challenges multiples)
CI / lint (push) Successful in 1m59s
CI / security (push) Successful in 1m35s
CI / test (push) Successful in 4m7s
CI / build (push) Successful in 1m16s
CI / e2e (push) Successful in 12m12s
2026-09-22 20:54:01 -04:00
bruno bca0fdd941 fix: login 2FA bloque sans erreur BUG-069 (challenge montait dans .login-box inexistant -> .login-card + erreur visible)
CI / lint (push) Successful in 1m56s
CI / security (push) Successful in 1m20s
CI / test (push) Successful in 4m17s
CI / build (push) Successful in 1m16s
CI / e2e (push) Successful in 12m35s
2026-09-22 20:38:37 -04:00
bruno 60da957f13 fix: section Securite du compte incomplete BUG-068 (boutons theme, QR local, mot de passe, recovery WebAuthn)
CI / lint (push) Successful in 2m25s
CI / security (push) Successful in 1m20s
CI / test (push) Successful in 3m43s
CI / build (push) Successful in 2m9s
CI / e2e (push) Successful in 12m7s
2026-09-22 20:07:25 -04:00
43 changed files with 3437 additions and 333 deletions
+4 -1
View File
@@ -51,7 +51,10 @@ OBSIGATE_ADMIN_PASSWORD=chab30
# OBSIGATE_PDF_MAX_SIZE_MB=50 # PDFs plus volumineux = texte non indexé
# OBSIGATE_PDF_EXTRACT_TIMEOUT=30 # secondes avant abandon de l'extraction
# WebAuthn / MFA (ROADMAP #64) — nécessaire hors localhost
# WebAuthn / MFA (ROADMAP #64) — par défaut rp_id/origines sont dérivés de la
# requête (hôte exact, port inclus) : rien à configurer en accès direct.
# À renseigner uniquement pour un accès via reverse-proxy sous un autre nom
# (avec OBSIGATE_TRUST_PROXY=true pour X-Forwarded-Host/Proto) :
# OBSIGATE_WEBAUTHN_RP_ID=obsigate.example.com
# OBSIGATE_WEBAUTHN_RP_NAME=ObsiGate
# OBSIGATE_WEBAUTHN_ORIGINS=https://obsigate.example.com
+1
View File
@@ -40,6 +40,7 @@ jobs:
node tests/frontend/unit.test.mjs
node tests/frontend/pdf-viewer.test.mjs
node tests/frontend/forge-completion.test.mjs
node tests/frontend/config-mobile.test.mjs
- name: Frontend JSDOM tests (PaneManager + Excalidraw + Plugins + AI + SW + Collab + Mobile + Semantic + Desktop + Inline edition)
run: |
+1
View File
@@ -91,6 +91,7 @@ bash scripts/run-e2e-local.sh -g "nom du test" # filtre / --headed
| Travail à venir + index | `docs/ROADMAP.md` |
| Historique des versions | `CHANGELOG.md` |
| Conception par feature | `docs/features/<slug>.md` |
| Guides d'utilisation | `docs/GUIDES/` |
| Archive du complété | `docs/archive/COMPLETED_v1-v2.md` |
| Bugs / TODO | `docs/ISSUES_TODOLIST.md` |
| Build & releases | `docs/DEVELOPMENT_AND_RELEASES.md` |
+120 -1
View File
@@ -6,7 +6,7 @@ Format basé sur [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/),
et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
> **En cours de développement** : les changements à venir sont listés dans la section
> [Unreleased](#unreleased). La dernière version livrée est **2.16.0**.
> [Unreleased](#unreleased). La dernière version livrée est **2.16.6**.
---
@@ -14,6 +14,125 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
---
## [2.16.6] — 2026-09-22
### Ajouté
- **Guides d'utilisation `docs/GUIDES/`** : nouvel index + 10 guides FR
(prise en main, recherche/PDF/Excalidraw, assistant IA & Forge,
collaboration temps réel, PWA & hors-ligne, API REST, serveur MCP,
authentification & sécurité, déploiement Docker, desktop Tauri). Le guide MCP
est déplacé dans `docs/GUIDES/MCP.md` ; `docs/MCP_GUIDE.md` devient une page
de redirection.
### Modifié
- **README.md / README.fr.md** : capture d'écran réelle de l'application en tête
(remplace l'illustration ASCII) ; un emoji sur chaque entrée de la table des
matières ; nouvelle section « Guides » avec liens vers `docs/GUIDES/` ;
renvois vers les guides depuis les sections API, Recherche, Sécurité, Desktop
et Collaboration.
---
## [2.16.5] — 2026-09-22
### Corrigé
- **BUG-071 (complément) - spec E2E mobile de la page Configurations** :
`tests/e2e/config-mobile.spec.js` (nouveau, projet `chromium-mobile`,
ignoré en `chromium-desktop` comme `mobile-editor.spec.js`) : le hamburger
révèle le sommaire, le choix d'une section y défile + lien actif + repli
auto, aucun débordement horizontal à 393px. Vérifié en local contre
l'instance de test (port 2029, auth désactivée) : 3/3.
---
## [2.16.4] — 2026-09-22
### Corrigé
- **BUG-071 - Page « Configurations » inutilisable en mode mobile** : trois
causes. (1) Le sommaire (`#config-nav`) partageait la règle `.help-nav`
qui le masque sous 768px, mais — contrairement au Guide — la modale
n'avait aucun bouton pour l'afficher : aucun moyen d'atteindre une section.
Nouvel hamburger `#config-hamburger` dans l'en-tête (même traitement
`.help-hamburger` que le Guide, libellé traduit `config.toc_toggle`
FR/EN). (2) Les liens du sommaire étaient des ancres brutes sans JS :
`config.js` les intercepte désormais (défilement doux vers la section dans
la modale, lien actif, repli automatique du sommaire sur mobile, réinit à
l'ouverture). (3) Les grilles 2 colonnes (fournisseur/modèle IA, clé/modèle
par fournisseur), les rangées d'ajout à largeurs fixes (jetons, webhooks)
et les lignes webhook/jeton/partage en flex une ligne débordaient en
360px : bloc CSS mobile scopé `#config-modal` (1 colonne, wrap, largeurs
inline neutralisées, cibles tactiles 44px, sommaire plafonné à 46vh).
`data-i18n-attr` accepte désormais plusieurs paires `attr:clé` séparées
par `;` (titre + aria-label traduits). Tests :
`tests/frontend/config-mobile.test.mjs` (nouveau, 11 — hamburger, i18n,
câblage JS, CSS mobile, garde-fou ancres mortes façon BUG-067),
enregistré dans le CI.
---
## [2.16.3] — 2026-09-22
### Corrigé
- **BUG-070 - Activation clé physique WebAuthn impossible (« Validation du
credential WebAuthn échouée »)** : deux causes. (1) Les valeurs par défaut
(`rp_id localhost`, origines `http://localhost` sans port) rejetaient toute
URL réelle — logs : `Unexpected client data origin "http://localhost:2020",
expected one of ['http://localhost']`. `rp_id`/origines sont désormais
dérivés de la requête (hôte exact, port inclus ; `X-Forwarded-Host/Proto`
si `OBSIGATE_TRUST_PROXY=true`), la config explicite restant prioritaire
(`backend/auth/webauthn_mfa.py::resolve_relying_party`, appliqué aux 4
endpoints d'enregistrement et de login). (2) Challenge à usage unique
fragile au double-clic/retry (`challenge was not expected`) : les 5
derniers challenges sont conservés et la vérification accepte le challenge
correspondant à la cérémonie en cours. `.env.example` documente le nouveau
comportement. Vérifié au navigateur avec authentificateur virtuel
(Playwright CDP, instance Docker) : enregistrement 200 + clé listée, puis
clé de test retirée. Tests : `tests/test_webauthn.py` (+8 : résolution RP,
forwarded, retry, roundtrip sans config).
---
## [2.16.2] — 2026-09-22
### Corrigé
- **BUG-069 - Login 2FA bloqué sans erreur** : après user+mot de passe corrects
sur un compte avec 2FA, la page de login restait affichée sans erreur et le
challenge MFA n'apparaissait jamais. Cause : `showMfaChallenge`
(`frontend/js/auth.js`) montait le challenge dans `.login-box`, inexistant
dans `index.html` (marquage réel : `#login-screen > .login-card`) →
`return` silencieux. Correctif : montage dans `.login-card` (repli
`#login-screen`) + erreur visible (`mfa.challenge_unavailable`, FR/EN) au
lieu d'un retour silencieux si le point de montage manque. Vérifié de bout
en bout au navigateur (Playwright, instance Docker) : challenge affiché,
code erroné → erreur, code valide → connecté. Tests :
`tests/frontend/mfa-settings.test.mjs` (+2 contrôles d'ancrage DOM).
---
## [2.16.1] — 2026-09-22
### Corrigé
- **BUG-068 - Configuration : section « 🔒 Sécurité du compte » inachevée** :
boutons `config-btn-primary` / `config-btn-danger` définis depuis les
variables du thème (`frontend/style.css`) ; QR code TOTP généré en local par
le backend (`POST /api/auth/mfa/totp/setup` → `qr_data_url`, SVG `data:`
via `segno`, `backend/requirements.txt`) au lieu de l'image tierce bloquée
par la CSP (`img-src 'self' data: blob:`, secret TOTP exposé) ; codes de
récupération affichés aussi à la première activation WebAuthn ; carte
« Mot de passe » (changement via `POST /api/auth/change-password`) et
échappement des libellés de clés WebAuthn. Tests :
`tests/test_mfa.py::test_mfa_setup_returns_local_qr_data_url`,
`tests/frontend/mfa-settings.test.mjs` (nouveau, 9 contrôles).
---
## [2.16.0] — 2026-09-22
### Modifié
+63 -39
View File
@@ -1,61 +1,75 @@
# ObsiGate
> **Version française** — ce document est le miroir synchronisé de [README.md](README.md) (référence complète). Dernière synchronisation : juin 2026.
> **Version française** — ce document est le miroir synchronisé de [README.md](README.md) (référence complète). Dernière synchronisation : septembre 2026.
**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.
[![Version](https://img.shields.io/badge/Version-2.16.0-blue.svg)]()
[![Version](https://img.shields.io/badge/Version-2.16.6-blue.svg)]()
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Docker](https://img.shields.io/badge/Docker-Ready-blue.svg)](https://www.docker.com/)
[![Python](https://img.shields.io/badge/Python-3.11+-green.svg)](https://www.python.org/)
[![CI/CD](https://img.shields.io/badge/CI%2FCD-Gitea_Actions-green.svg)](https://git.dracodev.net/Projets/ObsiGate/actions)
```
┌─────────────────────────────────────────────────────────┐
│ [🔍 Recherche...] [☀/🌙 Thème] ObsiGate │
├──────────────┬──────────────────────────────────────────┤
│ SIDEBAR │ CONTENT AREA │
│ ▼ Recettes │ 📄 Titre du fichier │
│ 📁 Soupes │ Tags: #recette #rapide │
│ 📄 Pizza │ [Contenu Markdown rendu] │
│ ▼ IT │ │
│ 📁 Docker │ │
│ Tags Cloud │ │
└──────────────┴──────────────────────────────────────────┘
```
![Interface ObsiGate — tableau de bord Statistiques avec vaults, tags et raccourcis clavier](docs/images/obsigate-home.png)
> Interface web d'ObsiGate : sidebar multi-vault, recherche globale, statistiques et raccourcis.
---
## 📚 Guides
Les **guides d'utilisation** pas à pas se trouvent dans [`docs/GUIDES/`](docs/GUIDES/) :
| Guide | Contenu |
|---|---|
| 🚀 [Prise en main](docs/GUIDES/PRISE_EN_MAIN.md) | Premier lancement, interface, navigation, vaults, raccourcis |
| 🔍 [Recherche, PDF & Excalidraw](docs/GUIDES/RECHERCHE_PDF_EXCALIDRAW.md) | Syntaxe de requête, recherche sémantique, lecteur PDF, diagrammes |
| 🤖 [Assistant IA & Forge](docs/GUIDES/ASSISTANT_IA_FORGE.md) | Fournisseurs, éditeur IA, BooksLM, Forge, commandes `@` / `/` |
| 📝 [Édition & collaboration](docs/GUIDES/COLLABORATION.md) | Édition simultanée, curseurs distants, persistance |
| 📱 [PWA & hors-ligne](docs/GUIDES/PWA_HORS_LIGNE.md) | Installation, cache hors-ligne, file de synchro, notifications |
| 🔌 [API REST](docs/GUIDES/API_REST.md) | Authentification, clés API, endpoints, exemples `curl`, SSE |
| 🧩 [Serveur MCP](docs/GUIDES/MCP.md) | Brancher Claude Desktop, Cursor, Cline… sur vos vaults |
| 🔒 [Authentification & sécurité](docs/GUIDES/AUTHENTIFICATION_SECURITE.md) | Utilisateurs, MFA, permissions par vault, durcissement |
| 🐳 [Déploiement Docker](docs/GUIDES/DEPLOIEMENT_DOCKER.md) | `docker-compose`, volumes, reverse proxy, mises à jour |
| 🖥️ [Desktop (Tauri)](docs/GUIDES/DESKTOP.md) | Installation, premier lancement, build depuis les sources, dépannage |
> Index complet : [`docs/GUIDES/README.md`](docs/GUIDES/README.md).
---
## 📋 Table des matières
- [Fonctionnalités](#fonctionnalites)
- [Prérequis](#prerequis)
- [Installation rapide](#installation-rapide)
- [Configuration détaillée](#configuration-detaillee)
- [Variables d'environnement](#variables-denvironnement)
- [🔒 Authentification](#authentification)
- [Ajouter une nouvelle vault](#ajouter-une-nouvelle-vault)
- [Build & déploiement avec build.sh](#build-deploiement-avec-buildsh)
- [Rendu d'images Obsidian](#rendu-dimages-obsidian)
- [Desktop (Tauri) — Application native](#desktop-tauri-application-native)
- [Utilisation](#utilisation)
- [API](#api)
- [Recherche avancée](#recherche-avancee)
- [Dépannage](#depannage)
- [Performance](#performance)
- [Sécurité](#securite)
- [Stack technique](#stack-technique)
- [Architecture](#architecture)
- [Développement](#developpement)
- [Licence](#licence)
- [Changelog](#changelog)
- ✨ [Fonctionnalités](#fonctionnalites)
- 📚 [Guides](#guides)
- 🚀 [Prérequis](#prerequis)
- ⚡ [Installation rapide](#installation-rapide)
- ⚙️ [Configuration détaillée](#configuration-detaillee)
- 🌍 [Variables d'environnement](#variables-denvironnement)
- 🔒 [Authentification](#authentification)
- ➕ [Ajouter une nouvelle vault](#ajouter-une-nouvelle-vault)
- 🔨 [Build & déploiement avec build.sh](#build-deploiement-avec-buildsh)
- 🖼️ [Rendu d'images Obsidian](#rendu-dimages-obsidian)
- 🖥️ [Desktop (Tauri) — Application native](#desktop-tauri-application-native)
- 📖 [Utilisation](#utilisation)
- 👥 [Collaboration temps réel](#collaboration-temps-reel)
- 🔌 [API](#api)
- 🔍 [Recherche avancée](#recherche-avancee)
- 🔧 [Dépannage](#depannage)
- ⚡ [Performance](#performance)
- 🛡️ [Sécurité](#securite)
- 🏗️ [Stack technique](#stack-technique)
- 🏠 [Architecture](#architecture)
- 📝 [Développement](#developpement)
- 📄 [Licence](#licence)
- 🤝 [Support](#support)
- 📝 [Changelog](#changelog)
---
## ✨ Fonctionnalités
- **🤖 AI Editor intégré** — Éditeur CodeMirror 6 avec toolbar IA : amélioration, correction, traduction, génération, réécriture personnalisée, toolbox (liste, tableau, frontmatter, canvas) — multi-provider DeepSeek/OpenRouter/Gemini
- **🧩 Serveur MCP & agent IA** — Serveur Model Context Protocol intégré (`/mcp`) et assistant avec function calling : lisez, cherchez et modifiez vos vaults depuis Claude Desktop, Cursor… avec confirmations two-step, permissions par vault, rate limiting et redaction des secrets ([guide](docs/MCP_GUIDE.md))
- **🧩 Serveur MCP & agent IA** — Serveur Model Context Protocol intégré (`/mcp`) et assistant avec function calling : lisez, cherchez et modifiez vos vaults depuis Claude Desktop, Cursor… avec confirmations two-step, permissions par vault, rate limiting et redaction des secrets ([guide](docs/GUIDES/MCP.md))
- **👥 Collaboration temps réel** — Édition simultanée d'un même document (Yjs/CRDT) : curseurs distants colorés, indicateur de présence, fusion sans conflit, reconnexion automatique et persistance serveur ([détail](docs/features/collaboration.md))
- **📖 Guide d'utilisation intégré** — Aide complète en FR/EN accessible depuis le menu Options : interface, navigation, recherche, fichiers, IA, sécurité, API & intégrations (OpenAPI, MCP), hors-ligne, collaboration, desktop, plus une section **Architecture** avec diagramme Mermaid ; téléchargeable en **Markdown** et **PDF** dans la langue courante ([détail](docs/features/guide-coverage-105.md))
- **📱 Éditeur mobile natif** — Édition optimisée pour le tactile : barre d'outils Markdown flottante (gras/italique/code/liste/lien), bouton « Coller » persistant (contournement iOS), zoom par pincement et hauteur ajustable, raccourcis swipe (liens entrants / table des matières) et mode lecture plein écran avec navigation entre fichiers ([détail](docs/features/mobile-editor.md))
@@ -412,6 +426,8 @@ curl -X POST http://localhost:2020/api/attachments/rescan/MonVault
## 🖥️ Desktop (Tauri) — Application native
> 📖 Guide complet : [Desktop (Tauri)](docs/GUIDES/DESKTOP.md)
ObsiGate Desktop est une application native construite avec [Tauri](https://tauri.app/) (Rust + webview système). Elle embarque le backend Python et le frontend dans un exécutable standalone — zéro Docker, zéro ligne de commande.
> 🚧 **Version 2.0.0 — binaires en cours de stabilisation.** Pour l'instant, le build depuis les sources est recommandé.
@@ -571,6 +587,8 @@ Cycle de vie : Tauri spawn le backend Python → health check → splash de dém
## 👥 Collaboration temps réel
> 📖 Guide complet : [Édition & collaboration](docs/GUIDES/COLLABORATION.md)
Plusieurs utilisateurs peuvent éditer le même document markdown simultanément (façon Google Docs) :
- **Fusion sans conflit** grâce à Yjs (CRDT) : deux personnes peuvent taper au même endroit, aucune
@@ -590,6 +608,8 @@ fenêtres) pour voir la collaboration en action.
## 🔌 API
> 📖 Guide complet : [API REST](docs/GUIDES/API_REST.md) · [Serveur MCP](docs/GUIDES/MCP.md)
ObsiGate expose une API REST complète :
| Endpoint | Description | Méthode | Auth |
@@ -637,6 +657,8 @@ curl "http://localhost:2020/api/file/Recettes?path=pizza.md"
## 🔍 Recherche avancée
> 📖 Guide complet : [Recherche, PDF & Excalidraw](docs/GUIDES/RECHERCHE_PDF_EXCALIDRAW.md)
### Syntaxe de requête
| Opérateur | Description | Exemple |
@@ -758,6 +780,8 @@ Configurables via l'interface (Settings) ou l'API `/api/config`.
## 🛡️ Sécurité
> 📖 Guide complet : [Authentification & sécurité](docs/GUIDES/AUTHENTIFICATION_SECURITE.md)
- **Path traversal** : tous les endpoints fichier valident que le chemin résolu reste dans la vault
- **Rate limiting** : 10 tentatives de login max par IP sur 15 minutes + lockout par compte (5 tentatives)
- **Audit log** : écritures/suppressions/config journalisées dans `data/audit.log` (JSON lines, rotation 10 MB)
@@ -927,8 +951,8 @@ Ce projet est sous licence **MIT** — voir le fichier [LICENSE](LICENSE) pour l
## 📝 Changelog
Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.16.0).
Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.16.6).
---
*Projet : ObsiGate | Version : 2.16.0 | Dernière mise à jour : Juin 2026*
*Projet : ObsiGate | Version : 2.16.6 | Dernière mise à jour : Septembre 2026*
+66 -34
View File
@@ -2,53 +2,73 @@
**Ultra-light web gateway for your Obsidian vaults** — Access, browse, and search all your Obsidian notes from any device via a modern, responsive web interface.
[![Version](https://img.shields.io/badge/Version-2.16.0-blue.svg)]()
[![Version](https://img.shields.io/badge/Version-2.16.6-blue.svg)]()
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Docker](https://img.shields.io/badge/Docker-Ready-blue.svg)](https://www.docker.com/)
[![Python](https://img.shields.io/badge/Python-3.11+-green.svg)](https://www.python.org/)
[![CI/CD](https://img.shields.io/badge/CI%2FCD-Gitea_Actions-green.svg)](https://git.dracodev.net/Projets/ObsiGate/actions)
```
┌─────────────────────────────────────────────────────────┐
│ [🔍 Search...] [☀/🌙 Theme] ObsiGate │
├──────────────┬──────────────────────────────────────────┤
│ SIDEBAR │ CONTENT AREA │
│ ▼ Recipes │ 📄 File Title │
│ 📁 Soups │ Tags: #recipe #quick │
│ 📄 Pizza │ [Rendered Markdown Content] │
│ ▼ IT │ │
│ 📁 Docker │ │
│ Tags Cloud │ │
└──────────────┴──────────────────────────────────────────┘
```
![ObsiGate interface — statistics dashboard with vaults, tags and keyboard shortcuts](docs/images/obsigate-home.png)
> ObsiGate web interface: multi-vault sidebar, global search, dashboard stats and shortcuts.
---
## 📚 Guides
Step-by-step **user guides** live in [`docs/GUIDES/`](docs/GUIDES/):
| Guide | What it covers |
|---|---|
| 🚀 [Getting Started](docs/GUIDES/PRISE_EN_MAIN.md) | First run, interface, navigation, vaults, shortcuts |
| 🔍 [Search, PDF & Excalidraw](docs/GUIDES/RECHERCHE_PDF_EXCALIDRAW.md) | Query syntax, semantic search, PDF viewer, diagrams |
| 🤖 [AI Assistant & Forge](docs/GUIDES/ASSISTANT_IA_FORGE.md) | Providers, AI editor, BooksLM, Forge, `@` / `/` commands |
| 📝 [Editing & Collaboration](docs/GUIDES/COLLABORATION.md) | Simultaneous editing, remote cursors, persistence |
| 📱 [PWA & Offline](docs/GUIDES/PWA_HORS_LIGNE.md) | Install as an app, offline cache, sync queue, push |
| 🔌 [REST API](docs/GUIDES/API_REST.md) | Authentication, API keys, endpoints, `curl` examples, SSE |
| 🧩 [MCP Server](docs/GUIDES/MCP.md) | Connect Claude Desktop, Cursor, Cline… to your vaults |
| 🔒 [Auth & Security](docs/GUIDES/AUTHENTIFICATION_SECURITE.md) | Users, MFA, per-vault permissions, hardening |
| 🐳 [Docker Deployment](docs/GUIDES/DEPLOIEMENT_DOCKER.md) | `docker-compose`, volumes, reverse proxy, updates |
| 🖥️ [Desktop (Tauri)](docs/GUIDES/DESKTOP.md) | Install, first run, build from source, troubleshooting |
> All guides are currently written in **French**. See the full index:
> [`docs/GUIDES/README.md`](docs/GUIDES/README.md).
---
## 📋 Table of Contents
- [Features](#features)
- [Architecture](#architecture)
- [Prerequisites](#prerequisites)
- [Quick Installation](#quick-installation)
- [Detailed Configuration](#detailed-configuration)
- [Environment Variables](#environment-variables)
- [🔒 Authentication](#authentication)
- [Adding a New Vault](#adding-a-new-vault)
- [Build & Deployment with build.sh](#build--deployment-with-buildsh)
- [Desktop (Tauri) — Native Application](#desktop-tauri--native-application)
- [Usage](#usage)
- [API](#api)
- [Performance](#performance)
- [Troubleshooting](#troubleshooting)
- [Tech Stack](#tech-stack)
- [Changelog](#changelog)
- ✨ [Features](#features)
- 📚 [Guides](#guides)
- 🚀 [Prerequisites](#prerequisites)
- ⚡ [Quick Installation](#quick-installation)
- ⚙️ [Detailed Configuration](#detailed-configuration)
- 🌍 [Environment Variables](#environment-variables)
- 🔒 [Authentication](#authentication)
- ➕ [Adding a New Vault](#adding-a-new-vault)
- 🔨 [Build & Deployment with build.sh](#build--deployment-with-buildsh)
- 🖼️ [Obsidian Image Rendering](#obsidian-image-rendering)
- 🖥️ [Desktop (Tauri) — Native Application](#desktop-tauri--native-application)
- 📖 [Usage](#usage)
- 👥 [Real-time Collaboration](#real-time-collaboration)
- 🔌 [API](#api)
- 🔍 [Advanced Search](#advanced-search)
- 🛡️ [Security](#security)
- ⚡ [Performance](#performance)
- 🔧 [Troubleshooting](#troubleshooting)
- 🏗️ [Tech Stack](#tech-stack)
- 🏠 [Architecture](#architecture)
- 📝 [Development](#development)
- 📄 [License](#license)
- 🤝 [Support](#support)
- 📝 [Changelog](#changelog)
---
## ✨ Features
- **🤖 Integrated AI Editor** — CodeMirror 6 editor with AI toolbar: improve, correct, translate, generate, custom rewrite, toolbox (list, table, frontmatter, canvas) — multi-provider DeepSeek/OpenRouter/Gemini
- **🧩 MCP Server & AI Agent** — Built-in Model Context Protocol server (`/mcp`) and tool-calling assistant: read, search and edit your vaults from Claude Desktop, Cursor… with two-step confirmations, per-vault permissions, rate limiting and secret redaction ([guide](docs/MCP_GUIDE.md))
- **🧩 MCP Server & AI Agent** — Built-in Model Context Protocol server (`/mcp`) and tool-calling assistant: read, search and edit your vaults from Claude Desktop, Cursor… with two-step confirmations, per-vault permissions, rate limiting and secret redaction ([guide](docs/GUIDES/MCP.md))
- **👥 Real-time Collaboration** — Simultaneous editing of the same document (Yjs/CRDT): colored remote cursors, presence indicator, conflict-free merge, automatic reconnection and server-side persistence ([details](docs/features/collaboration.md))
- **📖 Built-in User Guide** — Complete FR/EN help from the Options menu: interface, navigation, search, files, AI, security, API & integrations (OpenAPI, MCP), offline, collaboration, desktop, plus an **Architecture** section with a Mermaid diagram; downloadable as **Markdown** and **PDF** in the current language ([details](docs/features/guide-coverage-105.md))
- **📱 Native Mobile Editor** — Touch-optimised editing: floating Markdown toolbar (bold/italic/code/list/link), persistent Paste button (iOS workaround), pinch-zoom font & adjustable height, swipe shortcuts (backlinks / table of contents) and a full-screen reading mode with page navigation ([details](docs/features/mobile-editor.md))
@@ -520,6 +540,8 @@ curl -X POST http://localhost:2020/api/attachments/rescan/MyVault
## 🖥️ Desktop (Tauri) — Native Application
> 📖 Full guide: [Desktop (Tauri)](docs/GUIDES/DESKTOP.md)
ObsiGate Desktop is a native application built with [Tauri](https://tauri.app/) (Rust + system webview). It embeds the Python backend and frontend in a standalone executable — zero Docker, zero command line.
> 🚧 **Version 2.0.0 — binaries are being stabilized.** For now, building from source is recommended.
@@ -687,6 +709,8 @@ Lifecycle: Tauri spawns the Python backend → health check → opens the webvie
## 👥 Real-time Collaboration
> 📖 Full guide: [Editing & Collaboration](docs/GUIDES/COLLABORATION.md)
Multiple users can edit the same markdown document simultaneously (Google Docs style):
- **Conflict-free merge** via Yjs (CRDT): two people can type in the same place, no change is lost.
@@ -703,6 +727,8 @@ No configuration is required: open the same file in two browsers (or two windows
## 🔌 API
> 📖 Full guide: [REST API](docs/GUIDES/API_REST.md) · [MCP Server](docs/GUIDES/MCP.md)
ObsiGate exposes a complete REST API :
| Endpoint | Description | Method | Auth |
@@ -763,6 +789,8 @@ curl "http://localhost:2020/api/file/Recipes?path=pizza.md"
## 🔍 Advanced Search
> 📖 Full guide: [Search, PDF & Excalidraw](docs/GUIDES/RECHERCHE_PDF_EXCALIDRAW.md)
### Query Syntax
| Operator | Description | Example |
@@ -915,6 +943,8 @@ These parameters are configurable via the interface (Settings) or the `/api/conf
## 🛡️ Security
> 📖 Full guide: [Auth & Security](docs/GUIDES/AUTHENTIFICATION_SECURITE.md)
- **Path traversal** : All file endpoints validate that the resolved path stays within the vault
- **Rate limiting** : 10 login attempts max per IP over 15 minutes + per-account lockout (5 attempts)
- **Audit log** : All writes, deletions, and config changes are logged in `data/audit.log` (JSON lines, 10 MB rotation)
@@ -1070,7 +1100,9 @@ ObsiGate/
├── Dockerfile # Multi-stage, healthcheck, non-root
├── docker-compose.yml # Deployment with healthcheck and auth env vars
├── build.sh # Automated build & deployment (docker compose build + up)
└── docs/CONTRIBUTING.md # Contribution guide
└── docs/
├── GUIDES/ # User guides (getting started, API, MCP, desktop…)
└── CONTRIBUTING.md # Contribution guide
```
### Contributing
@@ -1096,8 +1128,8 @@ This project is licensed under the **MIT License** - see the [LICENSE](LICENSE)
## 📝 Changelog
See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.16.0).
See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.16.6).
---
*Project: ObsiGate | Version: 2.16.0 | Last updated: May 2026*
*Project: ObsiGate | Version: 2.16.6 | Last updated: September 2026*
+1 -1
View File
@@ -1 +1 @@
2.16.0
2.16.6
+38 -11
View File
@@ -448,7 +448,9 @@ class MfaEnableRequest(BaseModel):
async def mfa_totp_setup(current_user=Depends(require_auth)):
"""Generate a TOTP secret and QR URI for MFA setup.
Returns the secret and otpauth URI — client displays QR code.
Returns the secret, the otpauth URI and a ready-to-display QR code
(`qr_data_url`, SVG `data:` URI — no third-party service, CSP-safe).
Does NOT enable MFA yet; call /mfa/totp/enable after first successful verify.
"""
from .user_store import update_user
@@ -458,10 +460,21 @@ async def mfa_totp_setup(current_user=Depends(require_auth)):
update_user(current_user["username"], {
"mfa_secret_pending": secret,
})
# BUG-068: the QR code is generated locally (segno, stdlib-free SVG data
# URI). The previous client-side https://api.qrserver.com image was blocked
# by the CSP (img-src 'self' data: blob:) and leaked the otpauth URI —
# including the TOTP secret — to a third party.
qr_data_url: str | None = None
try:
import segno
qr_data_url = segno.make(qr_uri).svg_data_uri(scale=5)
except Exception:
qr_data_url = None
return {
"secret": secret,
"qr_uri": qr_uri,
"otpauth_uri": qr_uri,
"qr_data_url": qr_data_url,
}
@@ -563,18 +576,25 @@ class WebauthnRemoveRequest(BaseModel):
@router.post("/mfa/webauthn/register/options")
async def mfa_webauthn_register_options(current_user=Depends(require_auth)):
async def mfa_webauthn_register_options(request: Request,
current_user=Depends(require_auth)):
"""Start WebAuthn key enrolment — returns publicKey creation options for the browser."""
from .webauthn_mfa import begin_registration
from .webauthn_mfa import begin_registration, resolve_relying_party
# BUG-070: rp_id/origins derive from the request (exact host incl. port)
# unless explicitly configured — the old localhost defaults rejected
# every real access URL ("Unexpected client data origin").
rp, _ = resolve_relying_party(request)
options = begin_registration(current_user["username"],
current_user.get("display_name", ""))
current_user.get("display_name", ""),
rp_id_override=rp)
return {"options": options}
@router.post("/mfa/webauthn/register")
async def mfa_webauthn_register(
req: WebauthnRegisterRequest,
request: Request,
current_user=Depends(require_auth),
):
"""Verify the created credential, store it, and enable MFA if not already on.
@@ -584,14 +604,17 @@ async def mfa_webauthn_register(
from datetime import datetime, timezone
from .user_store import get_user, update_user
from .webauthn_mfa import complete_registration
from .webauthn_mfa import complete_registration, resolve_relying_party
user = get_user(current_user["username"])
if user is None:
raise HTTPException(404, "Utilisateur introuvable")
rp, origins = resolve_relying_party(request)
try:
record = complete_registration(current_user["username"], req.credential,
label=req.label)
label=req.label,
rp_id_override=rp,
origins_override=origins)
except ValueError as e:
raise HTTPException(400, str(e))
except Exception as e:
@@ -670,7 +693,7 @@ async def mfa_webauthn_remove(
@router.post("/mfa/webauthn/options")
async def mfa_webauthn_login_options(body: dict = Body(...)):
async def mfa_webauthn_login_options(request: Request, body: dict = Body(...)):
"""Unauthenticated: begin the login assertion for a user with registered keys.
Enumeration-safe: always 200 — returns null options (caller falls back to
@@ -682,8 +705,9 @@ async def mfa_webauthn_login_options(body: dict = Body(...)):
if not user or not user.get("mfa_enabled") or not creds:
return {"mfa_method": "totp", "options": None}
from .webauthn_mfa import begin_authentication
options = begin_authentication(username, creds)
from .webauthn_mfa import begin_authentication, resolve_relying_party
rp, _ = resolve_relying_party(request)
options = begin_authentication(username, creds, rp_id_override=rp)
if options is None:
return {"mfa_method": "totp", "options": None}
return {"mfa_method": "webauthn", "options": options}
@@ -697,7 +721,7 @@ async def mfa_webauthn_verify(
):
"""Unauthenticated: verify the WebAuthn assertion and issue JWT tokens."""
from .user_store import get_user, update_user
from .webauthn_mfa import complete_authentication
from .webauthn_mfa import complete_authentication, resolve_relying_party
client_ip = _enforce_mfa_rate_limit(request, body.username)
@@ -708,13 +732,16 @@ async def mfa_webauthn_verify(
if not user.get("mfa_enabled"):
raise HTTPException(400, "MFA non activé pour cet utilisateur")
rp, origins = resolve_relying_party(request)
creds = user.get("webauthn_credentials", [])
try:
credential_id = body.credential.get("id", "")
stored = next((c for c in creds if c.get("credential_id") == credential_id), None)
if stored is None:
raise ValueError("Credential non enregistré")
new_count = complete_authentication(body.username, body.credential, stored)
new_count = complete_authentication(body.username, body.credential, stored,
rp_id_override=rp,
origins_override=origins)
except ValueError as e:
_record_mfa_failure(client_ip, body.username)
raise HTTPException(401, str(e))
+157 -36
View File
@@ -38,8 +38,16 @@ logger = logging.getLogger("obsigate.auth.webauthn")
# Challenge lifetime: clients have 3 minutes to complete the ceremony.
CHALLENGE_TTL_SECONDS = 180
# In-memory pending challenges: key -> (challenge_bytes, expires_at)
_pending: dict[str, tuple[bytes, float]] = {}
# How many outstanding challenges to keep per key. BUG-070: a single slot made
# the flow fragile — a double-click on "add key" (or any retry) overwrote the
# pending challenge and the in-flight ceremony failed with
# "Client data challenge was not expected challenge". The verifier now accepts
# any recent challenge for the key.
MAX_PENDING_PER_KEY = 5
# In-memory pending challenges: key -> [(challenge_bytes, expires_at), ...]
# (newest last)
_pending: dict[str, list[tuple[bytes, float]]] = {}
def rp_id() -> str:
@@ -55,24 +63,100 @@ def expected_origins() -> list[str]:
return [o.strip() for o in raw.split(",") if o.strip()]
def resolve_relying_party(request: Any = None) -> tuple[str, list[str]]:
"""Resolve the WebAuthn (rp_id, expected_origins) for a ceremony.
BUG-070: the previous defaults (rp_id ``localhost``, origins
``http://localhost``) rejected every real-world access URL — any port
(``http://localhost:2020``), ``127.0.0.1``, a LAN host or a public domain
failed verification with "Unexpected client data origin".
Explicit configuration still wins: when ``OBSIGATE_WEBAUTHN_RP_ID`` /
``OBSIGATE_WEBAUTHN_ORIGINS`` are set they are used unchanged. Otherwise
the values are derived from the incoming request (exact ``Host``, port
included, since the browser origin carries non-default ports).
Behind a reverse proxy the external host/proto come from
``X-Forwarded-Host`` / ``X-Forwarded-Proto``, honored only when
``OBSIGATE_TRUST_PROXY=true`` (same rule as ``get_client_ip``).
"""
env_rp = os.environ.get("OBSIGATE_WEBAUTHN_RP_ID")
env_raw = os.environ.get("OBSIGATE_WEBAUTHN_ORIGINS")
if request is None:
return (env_rp or "localhost",
[o.strip() for o in env_raw.split(",") if o.strip()]
if env_raw else ["http://localhost"])
from backend.services.net import is_trusted_proxy
if is_trusted_proxy():
fwd_host = request.headers.get("x-forwarded-host", "")
host = fwd_host.split(",")[0].strip() or request.headers.get("host", "")
fwd_proto = request.headers.get("x-forwarded-proto", "")
scheme = fwd_proto.split(",")[0].strip() or request.url.scheme
else:
host = request.headers.get("host", "")
scheme = request.url.scheme
if not host:
url = request.url
host = url.netloc or url.hostname or ""
scheme = scheme or url.scheme or "http"
rp = env_rp or _hostname_only(host) or "localhost"
if env_raw:
origins = [o.strip() for o in env_raw.split(",") if o.strip()]
else:
origins = [f"{scheme or 'http'}://{host}"] if host else ["http://localhost"]
return rp, origins
def _hostname_only(host: str) -> str:
"""Strip the port (and IPv6 brackets) from a Host header value."""
host = host.strip()
if host.startswith("["): # [::1]:8080 or [::1]
end = host.find("]")
return host[1:end] if end > 0 else host
if host.count(":") == 1:
name, _, port = host.partition(":")
return name if port.isdigit() else host
return host
def _prune_expired() -> None:
now = time.time()
for key in [k for k, (_, exp) in _pending.items() if exp < now]:
_pending.pop(key, None)
for key in list(_pending):
remaining = [(c, exp) for c, exp in _pending[key] if exp >= now]
if remaining:
_pending[key] = remaining
else:
_pending.pop(key, None)
def _store_challenge(key: str) -> bytes:
_prune_expired()
challenge = secrets.token_bytes(32)
_pending[key] = (challenge, time.time() + CHALLENGE_TTL_SECONDS)
slot = _pending.setdefault(key, [])
slot.append((challenge, time.time() + CHALLENGE_TTL_SECONDS))
del slot[:-MAX_PENDING_PER_KEY] # keep only the most recent ones
return challenge
def _take_challenge(key: str) -> bytes | None:
"""Pop a challenge (single-use). Returns None if missing/expired."""
"""Pop the newest challenge (single-use). Returns None if missing/expired."""
_prune_expired()
entry = _pending.pop(key, None)
return entry[0] if entry else None
slot = _pending.get(key)
if not slot:
return None
challenge, _ = slot.pop()
if not slot:
_pending.pop(key, None)
return challenge
def _take_all_challenges(key: str) -> list[bytes]:
"""Pop every outstanding challenge for *key* (newest last)."""
_prune_expired()
slot = _pending.pop(key, None)
return [c for c, _ in slot] if slot else []
def clear_pending(username: str) -> None:
@@ -83,9 +167,12 @@ def clear_pending(username: str) -> None:
# ── Registration (enrol a key in settings) ─────────────────────────────
def begin_registration(username: str, display_name: str) -> dict:
def begin_registration(username: str, display_name: str,
rp_id_override: str | None = None,
origins_override: list[str] | None = None) -> dict:
_ = origins_override # origins only matter at verification time
options = generate_registration_options(
rp_id=rp_id(),
rp_id=rp_id_override or rp_id(),
rp_name=rp_name(),
user_name=username,
user_display_name=display_name or username,
@@ -98,19 +185,44 @@ def begin_registration(username: str, display_name: str) -> dict:
return _finalize_options(options)
def complete_registration(username: str, credential_json: dict[str, Any],
label: str = "") -> dict:
challenge = _take_challenge(f"{username}:register")
if challenge is None:
raise ValueError("Session d'enregistrement expirée — recommencez")
def _verify_with_any_challenge(key: str, verify_one: Any, empty_message: str) -> Any:
"""Run *verify_one(challenge)* against every outstanding challenge.
Returns the first success; re-raises the last error when all fail.
BUG-070: lets an in-flight ceremony survive a re-requested options call
(double-click / retry) that stored a newer challenge afterwards.
"""
challenges = _take_all_challenges(key)
if not challenges:
raise ValueError(empty_message)
last_error: Exception | None = None
for challenge in challenges:
try:
return verify_one(challenge)
except Exception as e: # try the next candidate challenge
last_error = e
assert last_error is not None
raise last_error
def complete_registration(username: str, credential_json: dict[str, Any],
label: str = "", rp_id_override: str | None = None,
origins_override: list[str] | None = None) -> dict:
credential = parse_registration_credential_json(credential_json)
verification = verify_registration_response(
credential=credential,
expected_challenge=challenge,
expected_rp_id=rp_id(),
expected_origin=expected_origins(),
)
effective_rp = rp_id_override or rp_id()
effective_origins = origins_override or expected_origins()
def _verify(challenge: bytes) -> Any:
return verify_registration_response(
credential=credential,
expected_challenge=challenge,
expected_rp_id=effective_rp,
expected_origin=effective_origins,
)
verification = _verify_with_any_challenge(
f"{username}:register", _verify,
"Session d'enregistrement expirée — recommencez")
transports = credential.response.transports or []
label = (label or str(credential_json.get("label") or "")).strip() or "Security key"
@@ -126,9 +238,12 @@ def complete_registration(username: str, credential_json: dict[str, Any],
# ── Authentication (assertion at login) ────────────────────────────────
def begin_authentication(username: str, credentials: list[dict]) -> dict | None:
def begin_authentication(username: str, credentials: list[dict],
rp_id_override: str | None = None,
origins_override: list[str] | None = None) -> dict | None:
if not credentials:
return None
_ = origins_override # origins only matter at verification time
from webauthn.helpers.structs import PublicKeyCredentialDescriptor
allow = [
@@ -136,7 +251,7 @@ def begin_authentication(username: str, credentials: list[dict]) -> dict | None:
for c in credentials
]
options = generate_authentication_options(
rp_id=rp_id(),
rp_id=rp_id_override or rp_id(),
challenge=_store_challenge(f"{username}:login"),
allow_credentials=allow,
)
@@ -147,21 +262,27 @@ def complete_authentication(
username: str,
credential_json: dict[str, Any],
stored: dict,
rp_id_override: str | None = None,
origins_override: list[str] | None = None,
) -> int:
"""Verify an assertion. Returns the new sign_count. Raises ValueError on failure."""
challenge = _take_challenge(f"{username}:login")
if challenge is None:
raise ValueError("Session expirée — rechargez la page")
"""Verify an assertion. Returns the new sign_count. Raises on failure."""
credential = parse_authentication_credential_json(credential_json)
verification = verify_authentication_response(
credential=credential,
expected_challenge=challenge,
expected_rp_id=rp_id(),
expected_origin=expected_origins(),
credential_public_key=base64url_to_bytes(stored["public_key"]),
credential_current_sign_count=int(stored.get("sign_count", 0)),
)
effective_rp = rp_id_override or rp_id()
effective_origins = origins_override or expected_origins()
def _verify(challenge: bytes) -> Any:
return verify_authentication_response(
credential=credential,
expected_challenge=challenge,
expected_rp_id=effective_rp,
expected_origin=effective_origins,
credential_public_key=base64url_to_bytes(stored["public_key"]),
credential_current_sign_count=int(stored.get("sign_count", 0)),
)
verification = _verify_with_any_challenge(
f"{username}:login", _verify,
"Session expirée — rechargez la page")
return int(verification.new_sign_count)
+1
View File
@@ -15,6 +15,7 @@ weasyprint>=60.0
httpx>=0.27.0
pypdf>=4.0
pyotp>=2.10.0
segno>=1.5.0
webauthn==2.6.0
psutil>=5.9
pywebpush>=2.3.0
+1 -1
View File
@@ -2626,7 +2626,7 @@ dependencies = [
[[package]]
name = "obsigate-desktop"
version = "2.16.0"
version = "2.16.6"
dependencies = [
"chrono",
"env_logger",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "obsigate-desktop"
version = "2.16.0"
version = "2.16.6"
description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian"
authors = ["Bruno Charest"]
edition = "2021"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json",
"productName": "ObsiGate",
"version": "2.16.0",
"version": "2.16.6",
"identifier": "com.obsigate.desktop",
"build": {
"frontendDist": "../frontend",
+2 -2
View File
@@ -347,7 +347,7 @@ Pour répondre au besoin de cibler un fournisseur/modèle sans dépendre uniquem
| **0 — Fondations** | `backend/tools/` (registry, context, service, audit) + extraction des services métier + tests unitaires | Couche d'outils testable sans IA |
| **1 — Function calling in-app** | Abstraction tool-calling multi-provider, agent loop, confirmations UI, SSE réel, outils de navigation | Assistant qui lit/cherche/lit/ouvre/modifie avec confirmation |
| **2 — Serveur MCP** | `backend/mcp/server.py` (tools + resources + prompts), **Streamable HTTP** (`/mcp`, auth JWT), confirmation two-step | ObsiGate accessible comme serveur MCP (local + distant, multi-utilisateur) |
| **3 — Durcissement** ✅ | Rate limiting (`backend/tools/ratelimit.py`), quotas `BOOKSLM_MAX_*`, redaction systématique des résultats (`backend/tools/redaction.py`), doc OpenAPI (tag/path MCP) + [guide MCP](./MCP_GUIDE.md), tests E2E | Observabilité et sécurité complètes |
| **3 — Durcissement** ✅ | Rate limiting (`backend/tools/ratelimit.py`), quotas `BOOKSLM_MAX_*`, redaction systématique des résultats (`backend/tools/redaction.py`), doc OpenAPI (tag/path MCP) + [guide MCP](./GUIDES/MCP.md), tests E2E | Observabilité et sécurité complètes |
Voir `docs/ROADMAP.md` (item dédié) pour le détail des activités.
@@ -380,7 +380,7 @@ Voir `docs/ROADMAP.md` (item dédié) pour le détail des activités.
- `backend/mcp/confirmations.py` — jetons de confirmation signés (two-step, anti-rejeu)
- `backend/tools/ratelimit.py` — rate limiting par jeton/outil (phase F)
- `backend/tools/redaction.py` — redaction récursive des résultats d'outils (phase F)
- `docs/MCP_GUIDE.md` — guide d'installation et d'utilisation des clients MCP
- `docs/GUIDES/MCP.md` — guide d'installation et d'utilisation des clients MCP
- `backend/bookslm.py`, `backend/bookslm_routes.py` — assistant contextuel (+ endpoint `/agent`)
- `frontend/js/ai.js`, `frontend/js/bookslm.js` — UI IA
- `backend/auth/middleware.py` — permissions
+326
View File
@@ -0,0 +1,326 @@
# 🔌 Guide de l'API REST
ObsiGate expose une **API REST complète** couvrant toute l'application :
vaults, fichiers, recherche, sauvegardes, exports, IA, partage, webhooks et
administration. Ce guide explique l'authentification, la création de clés et
donne des exemples prêts à l'emploi.
> **Public :** développeurs, intégrateurs, scripts d'automatisation
> **Doc interactive :** `/docs` (Swagger UI) · `/redoc` (ReDoc) · `/openapi.json`
> **Voir aussi :** [Serveur MCP](./MCP.md) · [Authentification & sécurité](./AUTHENTIFICATION_SECURITE.md)
---
## 1. Base et conventions
| Élément | Valeur |
|---|---|
| URL de base | `http://<hôte>:2020` (Docker) ou `http://127.0.0.1:17890` (desktop) |
| Préfixe API | `/api` |
| Format | JSON (`application/json`) |
| Version | suit la version d'ObsiGate (header `X-…`, `/api/health`) |
| Erreurs | `{"detail": "..."}` + code HTTP (`400`, `401`, `403`, `404`, `409`, `422`, `500`) |
Quand l'authentification est **désactivée** (`OBSIGATE_AUTH_ENABLED=false`), tous
les endpoints sont accessibles sans jeton (utilisateur anonyme avec accès à tous
les vaults).
---
## 2. Authentification
### 2.1 Jeton de session (JWT)
Obtenu via `POST /api/auth/login`. Le jeton d'accès a une durée de vie courte
(`OBSIGATE_ACCESS_TOKEN_TTL`, défaut 3600 s) et un refresh token longue durée est
posé en cookie HTTP-only.
```bash
curl -s -X POST http://localhost:2020/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"votre_mot_de_passe"}'
```
Réponse (extrait) :
```json
{
"access_token": "eyJ...",
"token_type": "bearer",
"expires_in": 3600,
"user": { "username": "admin", "role": "admin", "vaults": ["*"] }
}
```
Deux façons de présenter le jeton :
```http
Authorization: Bearer <access_token>
```
ou, pour un client navigateur, le cookie HTTP-only avec
`credentials: "include"` (le login pose aussi un cookie `access_token`).
### 2.2 Clés API longue durée (recommandé pour scripts & MCP)
Une **seule clé** authentifie **l'API REST et le serveur MCP**. Créez-la depuis
l'interface (Configurations → **🔑 Clés API & MCP**) ou par API :
```bash
# 1. Se connecter, récupérer le token (section 2.1)
# 2. Créer une clé valable 30 jours
curl -s -X POST http://localhost:2020/api/auth/tokens \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"Script backup","expiry":"30d"}'
```
Réponse (`token` affiché **une seule fois**) :
```json
{
"token": "eyJ...",
"jti": "…",
"name": "Script backup",
"created_at": 1790000000,
"expires_at": 1792592000,
"expiry_key": "30d"
}
```
| `expiry` | Durée |
|---|---|
| `1d` | 1 jour |
| `30d` | 1 mois |
| `180d` | 6 mois |
| `365d` | 1 an |
| `never` | sans expiration |
Gestion :
| Endpoint | Rôle |
|---|---|
| `GET /api/auth/tokens` | Lister vos clés (`last_used_at`, statut) |
| `POST /api/auth/tokens` | Créer (`{name, expiry}`) |
| `DELETE /api/auth/tokens/{jti}` | Révoquer immédiatement (API **et** MCP) |
> Le JWT brut n'est **jamais persisté** : copiez-le à la création. Plafond :
> 50 clés actives par utilisateur.
---
## 3. Référence des endpoints
> Liste non exhaustive — la référence faisant foi est `/openapi.json`. Les
> colonnes **Auth** indiquent le niveau requis (`—`, `Oui`, `Admin`).
### 3.1 Système
| Endpoint | Description | Méthode | Auth |
|---|---|---|---|
| `/api/health` | Santé (statut, version, stats) | GET | — |
| `/api/health/detailed` | Santé détaillée | GET | — |
| `/api/config` | Lire / écrire la configuration | GET/POST | Oui/Admin |
| `/api/diagnostics` | Statistiques index & mémoire | GET | Admin |
| `/api/dashboard` | Statistiques du tableau de bord | GET | Oui |
| `/api/events` | Flux SSE temps réel | GET | Oui |
### 3.2 Vaults
| Endpoint | Description | Méthode | Auth |
|---|---|---|---|
| `/api/vaults` | Liste (filtrée par permissions) | GET | Oui |
| `/api/vaults/status` | Statut de toutes les vaults | GET | Oui |
| `/api/vaults/add` | Ajouter une vault (volume déjà monté) | POST | Admin |
| `/api/vaults/{name}` | Supprimer une vault | DELETE | Admin |
| `/api/index/reload` | Réindexation complète | GET | Admin |
| `/api/index/reload/{vault}` | Réindexer une vault | GET | Oui |
| `/api/vaults/{vault}/settings` | Lire / écrire les réglages | GET/POST | Oui |
| `/api/attachments/rescan/{vault}` | Rescanner les attachements | POST | Oui |
### 3.3 Fichiers
| Endpoint | Description | Méthode | Auth |
|---|---|---|---|
| `/api/browse/{vault}?path=` | Naviguer dans les dossiers | GET | Oui |
| `/api/file/{vault}?path=` | Contenu rendu (Markdown) | GET | Oui |
| `/api/file/{vault}/raw?path=` | Contenu brut | GET | Oui |
| `/api/file/{vault}/download?path=` | Télécharger | GET | Oui |
| `/api/file/{vault}/save?path=` | Enregistrer | PUT | Oui |
| `/api/file/{vault}` | Créer | POST | Oui |
| `/api/file/{vault}` | Renommer | PATCH | Oui |
| `/api/file/{vault}` | Supprimer | DELETE | Oui |
| `/api/directory/{vault}` | Créer / renommer / supprimer un dossier | POST/PATCH/DELETE | Oui |
| `/api/move/{vault}` | Déplacer un fichier/dossier | POST | Oui |
| `/api/vault/{vault}/batch-upload` | Upload multiple (multipart) | POST | Oui |
| `/api/image/{vault}?path=` | Servir une image | GET | Oui |
### 3.4 Recherche & graphe
| Endpoint | Description | Méthode | Auth |
|---|---|---|---|
| `/api/search` | Recherche simple (legacy) | GET | Oui |
| `/api/search/advanced` | Recherche TF-IDF avancée (facettes, tri, pagination, `semantic=`) | GET | Oui |
| `/api/search/replace` | Recherche/remplacement multi-fichiers | POST | Oui |
| `/api/tags?vault=` | Tags uniques avec compteurs | GET | Oui |
| `/api/suggest?q=` | Autocomplétion de titres | GET | Oui |
| `/api/tags/suggest?q=` | Autocomplétion de tags | GET | Oui |
| `/api/tree-search` | Recherche de fichiers/dossiers | GET | Oui |
| `/api/vault/{vault}/paths` | Liste de chemins | GET | Oui |
| `/api/graph/{vault}` | Graphe de liens | GET | Oui |
### 3.5 Sauvegardes
| Endpoint | Description | Méthode | Auth |
|---|---|---|---|
| `/api/file/{vault}/backups` | Backups d'un fichier | GET | Oui |
| `/api/file/{vault}/diff` | Diff avec une version | GET | Oui |
| `/api/file/{vault}/restore` | Restaurer une version | POST | Oui |
| `/api/backups` | Lister les backups | GET | Oui |
| `/api/backups/content` | Contenu d'un backup | GET | Oui |
| `/api/backups/delete` / `/purge` / `/compress` / `/auto` | Gestion & purge | POST | Oui |
### 3.6 Exports
| Endpoint | Description | Méthode |
|---|---|---|
| `/api/export/html` | Exporter en HTML | GET |
| `/api/export/md-bundle` | Exporter en bundle Markdown (ZIP) | GET |
| `/api/export/epub` | Exporter en ePub | GET |
| `/api/guide/download?format=md\|pdf&lang=fr\|en` | Télécharger le guide intégré | GET |
### 3.7 PDF
| Endpoint | Description | Méthode |
|---|---|---|
| `/api/file/{vault}/pdf/info` | Métadonnées sans transfert | GET |
| `/api/file/{vault}/pdf/stream` | Streaming (HTTP Range, 206) | GET |
### 3.8 IA
| Endpoint | Description | Méthode |
|---|---|---|
| `/api/ai/status` | Statut des fournisseurs | GET |
| `/api/ai/improve`, `/fix-spelling`, `/summarize`, `/translate`, `/rewrite`, `/to-list`, `/to-table`, `/frontmatter`, `/inline-complete`, `/to-canvas`… | Actions éditeur IA | POST |
| `/api/ai/model-capabilities?provider=&model=` | Capacités d'un modèle | GET |
| `/api/ai/bookslm/*` | Console IA par répertoire | POST/GET |
| `/api/ai/skills` | Lister / créer / supprimer des skills | GET/POST/DELETE |
| `/api/config/ai-keys` · `/api/config/tool-keys` | Clés fournisseurs & sources | GET/POST/DELETE |
### 3.9 Authentification & administration
| Endpoint | Description | Méthode | Auth |
|---|---|---|---|
| `/api/auth/status` | Statut de l'auth | GET | — |
| `/api/auth/login` · `/refresh` · `/logout` | Cycle de session | POST | — / Cookie / Oui |
| `/api/auth/me` | Profil courant | GET/PATCH | Oui |
| `/api/auth/change-password` | Changer le mot de passe | POST | Oui |
| `/api/auth/mfa/*` | TOTP, WebAuthn, recovery | POST/GET | Oui |
| `/api/auth/tokens` | Clés API (voir §2.2) | GET/POST/DELETE | Oui |
| `/api/auth/admin/users` | Lister / créer des utilisateurs | GET/POST | Admin |
| `/api/auth/admin/users/{u}` | Modifier / supprimer | PATCH/DELETE | Admin |
| `/api/admin/stats` · `/audit` · `/backup-stats` · `/stream` | Monitoring admin | GET | Admin |
### 3.10 Partage, webhooks, conflits, plugins, push
| Endpoint | Description | Méthode |
|---|---|---|
| `/api/share/{vault}` | Créer un lien de partage public | POST |
| `/api/shares` | Lister / supprimer les partages | GET/DELETE |
| `/api/webhooks` | CRUD webhooks (HMAC-SHA256) | GET/POST/PATCH/DELETE |
| `/api/conflicts` · `/api/conflicts/resolve` | Conflits Syncthing | GET/POST |
| `/api/plugins` | Installer / activer / désactiver | GET/POST/DELETE |
| `/api/push/*` | Abonnement Web Push (VAPID) | GET/POST/DELETE |
---
## 4. Exemples `curl`
```bash
BASE=http://localhost:2020
TOKEN=$(curl -s -X POST $BASE/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"secret"}' | jq -r .access_token)
# Santé
curl -s $BASE/api/health
# Lister les vaults
curl -s $BASE/api/vaults -H "Authorization: Bearer $TOKEN"
# Naviguer
curl -s "$BASE/api/browse/Recettes?path=" -H "Authorization: Bearer $TOKEN"
# Lire un fichier (rendu Markdown)
curl -s "$BASE/api/file/Recettes?path=pizza.md" -H "Authorization: Bearer $TOKEN"
# Lire en brut
curl -s "$BASE/api/file/Recettes/raw?path=pizza.md" -H "Authorization: Bearer $TOKEN"
# Sauvegarder
curl -s -X PUT "$BASE/api/file/Recettes/save?path=pizza.md" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"content":"# Pizza\n\nNouvelle recette."}'
# Recherche avancée
curl -s "$BASE/api/search/advanced?q=tag:cuisine%20pizza&vault=all&limit=20&offset=0&sort=relevance" \
-H "Authorization: Bearer $TOKEN"
# Autocomplétion
curl -s "$BASE/api/suggest?q=piz&vault=all" -H "Authorization: Bearer $TOKEN"
# Forcer une réindexation
curl -s $BASE/api/index/reload -H "Authorization: Bearer $TOKEN"
```
> Le mot de passe peut aussi être fourni par une clé API dans `Authorization`.
> Quand l'auth est désactivée, omettez l'en-tête.
---
## 5. Temps réel
### 5.1 SSE — `/api/events`
Flux d'événements de changement d'index (fichiers créés/supprimés/modifiés), avec
reconnexion automatique côté client.
```bash
curl -N "$BASE/api/events"
```
### 5.2 WebSocket — collaboration
`ws(s)://<hôte>/ws/collab/{vault}/{path}` transporte les mises à jour
Yjs/CRDT et la présence (curseurs distants). Authentification par cookie
`access_token` ou paramètre `?token=`, avec contrôle d'accès par vault.
Voir [Édition & collaboration](./COLLABORATION.md).
---
## 6. Limites et bonnes pratiques
- **Rate limiting** : les endpoints de login et les outils IA sont limités ;
respectez `retry_after` en cas de `429`.
- **Permissions** : chaque endpoint fichier vérifie l'accès au vault et rejette
les chemins hors vault (path traversal).
- **Clés API** : préférez-les aux mots de passe pour les scripts ; révoquez-les
dès qu'elles ne servent plus.
- **Gros volumes** : utilisez la pagination (`limit`/`offset`) et le streaming
HTTP Range pour les PDF.
- **Exports** : `md-bundle` et `epub` renvoient un fichier binaire — utilisez
`-o` avec `curl`.
---
## 7. Dépannage
| Code | Cause probable |
|---|---|
| `401` | Jeton absent, expiré ou révoqué |
| `403` | Compte sans accès à cette vault / réservé admin |
| `404` | Vault, fichier ou chemin inexistant |
| `409` | Conflit (fichier déjà existant, etc.) |
| `422` | Corps de requête invalide (schéma Pydantic) |
| `429` | Rate limit dépassé — voir `retry_after` |
| `501` | Export PDF indisponible (WeasyPrint/GTK absent) |
+217
View File
@@ -0,0 +1,217 @@
# 🤖 Guide Assistant IA & Forge
ObsiGate intègre un **assistant IA** capable de lire, rechercher et modifier vos
notes, ainsi qu'un **éditeur IA** (CodeMirror + toolbar) et une console
contextuelle par répertoire (**BooksLM**). Ce guide explique comment les
configurer et les utiliser.
> **Fiches techniques :** [`ai-tools-mcp.md`](../features/ai-tools-mcp.md) ·
> [`ai-assistant-commands.md`](../features/ai-assistant-commands.md) ·
> [`ai-quick-actions.md`](../features/ai-quick-actions.md) ·
> [`forge-assistant.md`](../features/forge-assistant.md) ·
> [`bookslm.md`](../features/bookslm.md) ·
> [`ai-tools-roadmap.md`](../features/ai-tools-roadmap.md)
> **Voir aussi :** [Serveur MCP](./MCP.md) · [API REST](./API_REST.md)
---
## 1. Vue d'ensemble
L'IA d'ObsiGate se compose de plusieurs surfaces complémentaires :
| Surface | Rôle |
|---|---|
| **Éditeur IA** | Toolbar d'actions sur le document ouvert (CodeMirror) |
| **Assistant IA** | Panneau de discussion avec *function calling* sur vos vaults |
| **BooksLM** | Console IA contextuelle sur un **répertoire** (style NotebookLM) |
| **Forge** | Éditeur avancé avec assistant IA intégré |
| **Outils (tools)** | Lecture, recherche, écriture, opérations destructives (two-step) |
| **MCP** | Exposition des mêmes outils à Claude Desktop, Cursor, Cline… |
---
## 2. Configurer un fournisseur
### 2.1 Fournisseurs supportés
ObsiGate est **multi-fournisseur** :
- **DeepSeek**
- **OpenRouter**
- **Google Gemini**
Chaque fournisseur se configure au choix :
1. **Depuis l'interface** — menu → Configurations → **Clés API IA**. La clé saisie
est stockée dans `data/api_keys.json` et **prime** sur la variable
d'environnement.
2. **Par variable d'environnement** — voir `.env.example`.
### 2.2 Modèle et capacités
L'interface affiche les **capacités** de chaque modèle (8 indicateurs : vision,
tool calling, contexte long, etc.), via
`GET /api/ai/model-capabilities?provider=&model=`. Le picker de l'assistant
propose une recherche de modèle et une bulle d'information ⓘ.
Vous pouvez définir un **modèle par défaut** et un fournisseur par défaut dans la
configuration. Le fournisseur/modèle est **partagé** entre l'assistant et Forge.
### 2.3 Tester la configuration
`POST /api/config/ai-keys/test` vérifie qu'une clé fonctionne. En cas d'échec,
un message explicite s'affiche.
---
## 3. Éditeur IA (toolbar)
Quand un document Markdown est ouvert dans l'éditeur, une **toolbar IA** propose
des actions qui remplacent ou insèrent du contenu. Actions principales :
| Action | Effet |
|---|---|
| **Améliorer** | Relecture et amélioration générale |
| **Corriger** | Correction orthographique et grammaticale |
| **Raccourcir / Allonger** | Ajuste la longueur du texte |
| **Simplifier** | Vulgarise le contenu |
| **Ton** | Adapte le registre (formel, neutre…) |
| **Traduire** | Traduit la sélection ou le document |
| **Expliquer** | Explique un passage |
| **Résumer** | Produit un résumé |
| **Continuer** | Prolonge le texte |
| **Réécrire** | Réécriture personnalisée libre |
| **En liste / En tableau** | Convertit en liste à puces ou tableau Markdown |
| **Frontmatter** | Génère ou met à jour le frontmatter YAML |
| **Complétion inline** | `Ctrl + J` — complétion directement dans l'éditeur |
| **En canvas** | Transforme en diagramme canvas |
> Les actions sont exposées par `backend/ai_routes.py` (préfixe `/api/ai`). Le
> contexte ad-hoc (fichiers ouverts, répertoire, recherche, récents) est injecté
> automatiquement.
---
## 4. Forge et Editer
- **Editer** ouvre le document dans l'éditeur CodeMirror classique.
- **Forge** ouvre l'**éditeur avancé** : mêmes capacités d'édition, mais avec
l'**assistant IA partagé** intégré (bouton AI Panel), insertion rapide
(`Alt + I`), aide (`F1`) et mode plein écran.
Dans les deux cas, `Editer` et `Forge` **remplacent** la vue lecture ; revenez en
lecture avec `✓` / `×` ou `Échap`. Le panneau de l'assistant reste accessible à
côté.
---
## 5. Assistant IA & BooksLM
### 5.1 Discussion avec outils
L'assistant (panneau latéral) discute et **appelle des outils** pour agir sur
vos vaults : `list_vaults`, `read_file`, `search_fulltext`, `get_backlinks`,
`list_tags`, etc. Les opérations d'écriture passent par une **confirmation en
deux temps** (aperçu + jeton, puis application).
### 5.2 Contexte `@`
Tapez `@` pour attacher :
- un **fichier** (chip de contexte) ;
- un **répertoire** (chip de contexte) ;
- une **image** (pièce jointe, si le modèle gère la vision).
Le menu est alimenté par `/api/tree-search` (repli sur la liste des fichiers du
vault). Les chips sont retirables et rechargent le contexte.
### 5.3 Commandes `/` et skills
Tapez `/` pour ouvrir le **menu de commandes** (navigation `↑`/`↓`/`Entrée`/`Échap`).
**30 skills intégrés**, répartis par familles :
| Famille | Exemples |
|---|---|
| Base | `/research`, `/resume`, `/reformuler`, `/correction`, `/brainstorm`, `/plan`, `/ask`, `/meeting-note`, `/livrable` |
| Extraction & structuration | `/extract`, `/timeline`, `/glossary`, `/tag` |
| Transformation & adaptation | `/translate`, `/adapt`, `/clean`, `/summary-progressive` |
| Analyse critique & décision | `/critique`, `/compare`, `/prioritize`, `/swot`, `/debate` |
| Apprentissage & mémorisation | `/quiz`, `/reading-note`, `/qa-generator` |
| Méta-gestion & confidentialité | `/link`, `/anonymize`, `/estimate` |
Chaque skill applique un bloc de règles commun (français, notes traitées comme
données, anti-hallucination, conservation des noms/dates/chiffres).
**Skills utilisateur** : `/create-new-skill` ouvre une modale et persiste le
skill dans `data/skills.json` (par utilisateur). Ils sont listés par
`GET /api/ai/skills` et supprimables.
**Commandes admin** (exécutées localement, sans LLM) : `/help`, `/providers`,
`/provider <nom>`, `/model <nom>`, `/keys`.
### 5.4 Actions rapides
Un catalogue de **25 actions** en 6 catégories est proposé sous forme de boutons
contextuels (« Résumer en 3 points », « Checklist d'actions », « Générer le
frontmatter », « Expliquer le code », « Fusionner », « Traduire »…). Un tiroir
**« Toutes les actions »** permet de rechercher dans le catalogue.
### 5.5 Deep Research
Le mode **Deep Research** enchaîne recherche web et synthèse. Il est activé via
le panneau **« + »** de l'assistant (fichiers, contextes, skills, Deep Research).
### 5.6 Historique
Les conversations sont **persistées côté backend** et accessibles depuis la
sidebar « Historique IA », avec filtre de recherche.
---
## 6. Outils (function calling)
Les outils sont définis dans `backend/tools/` — **source unique de vérité**,
partagée par l'assistant in-app et le serveur MCP.
| Catégorie | Outils |
|---|---|
| Vaults / navigation | `list_vaults`, `list_directory`, `list_all_files` |
| Lecture | `read_file`, `read_file_raw`, `get_backlinks`, `list_backups`, `diff_backup`, `get_graph` |
| Recherche | `search_fulltext`, `search_advanced`, `search_paths`, `list_tags`, `suggest_tags`, `list_recent` |
| Écriture (propose/apply) | `create_file`, `create_directory`, `edit_file`, `append_to_file`, `restore_backup` |
| Destructif (propose/apply) | `rename_file`, `rename_directory`, `move_path`, `replace_in_files`, `delete_file`, `delete_directory` |
| Web / sources connectées | `web_search`, `fetch_url`, sources Gitea/GitHub… |
Les mutations suivent un flux **two-step** : `propose_<tool>` renvoie un aperçu
et un **jeton signé à usage unique**, puis `apply_<tool>` exécute.
---
## 7. Sécurité
- **Permissions par vault** appliquées à chaque outil.
- **Anti path-traversal** via `resolve_safe_path`.
- **Confirmation two-step** pour toute mutation.
- **Toggle `aiDestructiveTools`** par vault : le désactiver bloque
rename/move/replace/delete, sans bloquer create/edit/append.
- **Backup automatique** avant chaque opération destructive.
- **Rate limiting** par identité et par outil.
- **Redaction des secrets** dans tous les retours d'outils.
- **Audit** de chaque appel (`data/audit.log`, action `ai_tool_call`).
Détails : [Authentification & sécurité](./AUTHENTIFICATION_SECURITE.md) et
[`MCP.md`](./MCP.md) §5.
---
## 8. Dépannage
| Symptôme | Piste |
|---|---|
| « Aucun fournisseur configuré » | Saisir une clé API (Configurations → Clés API IA) et la tester |
| L'IA n'a pas accès à un fichier | Vérifier `list_vaults` et les permissions du compte |
| L'image est refusée | Le modèle ne supporte pas la vision (400) — choisir un modèle multimodal |
| Une mutation reste bloquée | Vérifier `aiDestructiveTools` et le flux `propose_` → `apply_` |
| Quota d'outils atteint | Respecter `OBSIGATE_TOOL_RATE_LIMIT` / `retry_after` |
| Réponse tronquée | Ajuster `BOOKSLM_MAX_TOOL_READ_BYTES` / le modèle |
+229
View File
@@ -0,0 +1,229 @@
# 🔒 Guide Authentification & sécurité
ObsiGate embarque un système d'authentification optionnel **JWT + Argon2id**,
un contrôle d'accès **par vault**, du MFA (TOTP, WebAuthn, codes de secours) et
des mécanismes de durcissement. Ce guide couvre l'activation, la gestion des
comptes et les bonnes pratiques.
> **Public :** administrateurs · **Voir aussi :**
> [`features/api-mcp-tokens-107.md`](../features/api-mcp-tokens-107.md) ·
> [API REST](./API_REST.md) · [MCP](./MCP.md) · [Déploiement Docker](./DEPLOIEMENT_DOCKER.md)
---
## 1. Vue d'ensemble
- **Désactivée par défaut** (`OBSIGATE_AUTH_ENABLED=false`) — compatible avec
toutes les installations existantes.
- Quand elle est activée, l'écran de connexion s'affiche et chaque endpoint
vérifie l'utilisateur et ses permissions.
- Les données d'auth (`users.json`, `secret.key`, `api_tokens.json`) vivent dans
`/app/data` — **montez ce dossier en volume** pour les persister.
---
## 2. Activer l'authentification
### 2.1 Fichier `.env`
```bash
cp .env.example .env
```
```bash
OBSIGATE_AUTH_ENABLED=true
OBSIGATE_ADMIN_USER=admin
OBSIGATE_ADMIN_PASSWORD=votre_mot_de_passe # vide = auto-généré (voir logs)
# OBSIGATE_SECURE_COOKIES=false # true si derrière HTTPS
```
### 2.2 `docker-compose.yml`
```yaml
env_file:
- .env
```
> **Ne mettez jamais de mot de passe dans `docker-compose.yml` !** Utilisez
> toujours `.env` (non committé).
### 2.3 Premier démarrage
Si aucun utilisateur n'existe, ObsiGate crée un compte admin et affiche le mot de
passe **une seule fois dans les logs** :
```bash
docker compose logs obsigate | grep -A4 "FIRST"
```
```
============================================================
FIRST STARTUP — Admin account created automatically
Username : admin
Password : xK9mQ3pLr7wN2jT5
CHANGE THIS PASSWORD on first login!
============================================================
```
Changez-le immédiatement (menu profil → *Changer le mot de passe*).
---
## 3. Gestion des utilisateurs
### 3.1 Interface d'administration
Un compte **admin** voit une icône 🛡️ dans le header. Le panneau permet de :
- lister tous les utilisateurs ;
- créer / modifier / supprimer des comptes ;
- assigner les vaults accessibles par utilisateur ;
- activer / désactiver des comptes.
### 3.2 Ligne de commande
```bash
# Créer un utilisateur
docker exec obsigate python backend/create_admin.py create alice MotDePasse --role user --vaults Recettes IT
# Créer un admin avec accès total
docker exec obsigate python backend/create_admin.py create bob SecretPass --role admin --vaults "*"
# Lister
docker exec obsigate python backend/create_admin.py list
# Supprimer
docker exec obsigate python backend/create_admin.py delete alice
```
### 3.3 Contrôle d'accès par vault
| Valeur `vaults` | Accès |
|---|---|
| `["*"]` | Toutes les vaults (y compris futures) — défaut admin |
| `["Recettes", "IT"]` | Uniquement ces vaults |
| `[]` | Aucun accès |
Les permissions sont revérifiées à chaque requête (et à chaque connexion
WebSocket de collaboration).
---
## 4. MFA (authentification multifacteur)
ObsiGate propose trois secondes facteurs, configurables par l'utilisateur.
### 4.1 TOTP (application d'authentification)
1. Menu profil → **Sécurité** → *Configurer TOTP* (`POST /api/auth/mfa/totp/setup`).
2. Scannez le QR code avec Google Authenticator, Authy, etc.
3. Validez le code (`POST /api/auth/mfa/totp/enable`).
4. Désactivation : `POST /api/auth/mfa/totp/disable` (mot de passe requis).
### 4.2 Clés de sécurité & biométrie (WebAuthn)
- Enregistrement : `POST /api/auth/mfa/webauthn/register/options` puis
`POST /api/auth/mfa/webauthn/register`.
- Connexion : `POST /api/auth/mfa/webauthn/options` puis `/verify`.
- Gestion des clés : `GET /api/auth/mfa/webauthn/credentials`,
`POST /api/auth/mfa/webauthn/credentials/remove`.
> Le *relying party* (domaine) est **dérivé de la requête** (hôte exact, port
> inclus) ; derrière un reverse proxy, activez `OBSIGATE_TRUST_PROXY=true` pour
> que `X-Forwarded-Host/Proto` soient pris en compte.
### 4.3 Codes de secours
À l'activation du MFA, des **codes de récupération** sont générés. Utilisez-en un
via `POST /api/auth/mfa/recovery` si vous perdez votre second facteur. Conservez-
les hors ligne.
### 4.4 Statut
`GET /api/auth/mfa/status` indique les facteurs actifs pour le compte courant.
---
## 5. Clés API & MCP
Pour les scripts et les clients externes, créez une **clé API longue durée**
(1 j, 1 mois, 6 mois, 1 an, sans fin) depuis Configurations → 🔑 **Clés API &
MCP**. Une seule clé authentifie l'API REST **et** le serveur MCP.
- Le secret n'est **affiché qu'une fois** (pattern GitHub) et n'est jamais persisté.
- La révocation est **immédiate** des deux côtés.
- Une colonne « dernière utilisation » (throttlée) aide à repérer les clés
dormantes.
Détails : [API REST §2.2](./API_REST.md#22-clés-api-longue-durée-recommandé-pour-scripts--mcp)
et [`features/api-mcp-tokens-107.md`](../features/api-mcp-tokens-107.md).
---
## 6. Mécanismes de durcissement
| Mécanisme | Détail |
|---|---|
| **Path traversal** | Chaque endpoint fichier valide que le chemin résolu reste dans la vault |
| **Rate limiting** | 10 tentatives de login max par IP / 15 min + lockout par compte |
| **Rate limiting MFA** | Appliqué aux endpoints TOTP/WebAuthn/recovery |
| **Audit log** | Écritures, suppressions, config dans `data/audit.log` (JSON lines, rotation 10 Mo) |
| **Backup automatique** | Avant chaque modification/suppression dans `.obsigate-backup/` |
| **Redaction** | Masquage des JWT, clés API, tokens dans les aperçus et retours d'outils |
| **CSP** | `object-src`, `base-uri`, `form-action`, `frame-ancestors` restreints |
| **Cookie HttpOnly** | Jeton retiré de `sessionStorage`, porté par cookie HTTP-only |
| **Utilisateur non-root** | Conteneur sous `obsigate` (UID 1000) |
| **Volumes read-only** | Vaults montées `:ro` par défaut |
| **Atomic writes** | `users.json`, `shares.json`, `webhooks.json` écrits en tmp+replace |
| **Symlinks ignorés** | L'index n'indexe pas les liens symboliques |
### Politique de mot de passe
Une politique minimale est validée à la création d'un compte. Choisissez des mots
de passe longs et uniques ; activez le MFA pour les comptes admin.
---
## 7. Variables d'environnement
| Variable | Description | Défaut |
|---|---|---|
| `OBSIGATE_AUTH_ENABLED` | Activer l'authentification | `false` |
| `OBSIGATE_ADMIN_USER` | Nom de l'admin auto-créé | `admin` |
| `OBSIGATE_ADMIN_PASSWORD` | Mot de passe admin (vide = auto-généré) | *(auto)* |
| `OBSIGATE_SECURE_COOKIES` | Cookie `Secure` (HTTPS uniquement) | `false` |
| `OBSIGATE_ACCESS_TOKEN_TTL` | Durée de vie du token d'accès (s) | `3600` |
| `OBSIGATE_REFRESH_TOKEN_TTL` | Durée de vie du refresh token (s) | `2592000` |
| `OBSIGATE_LOGIN_MAX_ATTEMPTS` | Tentatives de login max par IP | `10` |
| `OBSIGATE_ACCOUNT_MAX_ATTEMPTS` | Tentatives de login max par compte | `10` |
| `OBSIGATE_LOGIN_WINDOW_SECONDS` | Fenêtre de rate limiting (s) | `900` |
| `OBSIGATE_TRUST_PROXY` | Faire confiance à `X-Forwarded-For` / `Host` | `false` |
Toutes ces variables sont documentées dans `.env.example`.
---
## 8. Déploiement sécurisé (checklist)
- [ ] `OBSIGATE_AUTH_ENABLED=true` sur toute instance exposée.
- [ ] Mot de passe admin fort, changé après le premier démarrage.
- [ ] MFA activé pour les comptes admin.
- [ ] HTTPS via reverse proxy + `OBSIGATE_SECURE_COOKIES=true`.
- [ ] `OBSIGATE_TRUST_PROXY=true` **uniquement** derrière un proxy de confiance.
- [ ] Volume `./data:/app/data` monté et **sauvegardé**.
- [ ] Vaults montées en `:ro` (lecture seule) sauf besoin d'écriture.
- [ ] Clés API révoquées dès qu'elles ne servent plus.
- [ ] Accès réseau restreint (VPN / pare-feu) si possible.
---
## 9. Dépannage
| Symptôme | Piste |
|---|---|
| Login bloqué `429` | Rate limit : attendre la fenêtre (`OBSIGATE_LOGIN_WINDOW_SECONDS`) |
| WebAuthn refuse l'enregistrement | Domaine/port non dérivés — activer `OBSIGATE_TRUST_PROXY` derrière un proxy |
| TOTP « challenge inattendu » | Relancer la cérémonie ; les 5 derniers challenges sont acceptés |
| Perte du second facteur | Utiliser un code de secours (`/api/auth/mfa/recovery`) |
| Sessions perdues au redémarrage | Le volume `./data` n'est pas monté |
| Clé API `401` | Clé expirée ou révoquée — en créer une nouvelle |
+86
View File
@@ -0,0 +1,86 @@
# 📝 Guide Édition & collaboration temps réel
Plusieurs utilisateurs peuvent éditer le **même document Markdown
simultanément**, façon Google Docs, grâce à Yjs (CRDT) et à un canal WebSocket.
Ce guide explique le fonctionnement et l'utilisation.
> **Public :** tous les utilisateurs · **Fiche technique :**
> [`features/collaboration.md`](../features/collaboration.md)
> **Voir aussi :** [Prise en main](./PRISE_EN_MAIN.md) · [API REST](./API_REST.md)
---
## 1. Ce que fait la collaboration
- **Fusion sans conflit** via **Yjs (CRDT)** : deux personnes peuvent taper au
même endroit, aucune modification n'est perdue.
- **Curseurs distants colorés** et sélections visibles dans CodeMirror, étiquetés
avec le nom de chaque utilisateur.
- **Indicateur de présence** dans l'en-tête de l'éditeur (avatars + statut de
connexion).
- **Reconnexion automatique** (backoff exponentiel) : l'état est fusionné au retour.
- **Persistance serveur** : le document est écrit sur disque **2 s** après la
dernière modification.
---
## 2. Utilisation
Aucune configuration n'est nécessaire :
1. Ouvrez le même fichier dans **deux navigateurs** (ou deux fenêtres).
2. Passez en mode **Editer** (ou **Forge**) dans les deux.
3. Tapez : les modifications apparaissent en temps réel des deux côtés, avec les
curseurs de chacun.
> L'édition collaborative nécessite que la vault soit **accessible en écriture**
> (le volume Docker doit être monté **sans** `:ro` pour les vaults modifiables).
---
## 3. Transport & protocole
| Élément | Valeur |
|---|---|
| Endpoint | `ws(s)://<hôte>/ws/collab/{vault}/{chemin}` |
| Authentification | Cookie `access_token` (ou paramètre `?token=`) |
| Autorisation | Contrôle d'accès **par vault** appliqué à chaque connexion |
| Protocole | Yjs / CRDT — updates + awareness (curseurs) |
| Persistance | Écriture disque débouncée (2 s) côté serveur |
Le canal est mis à niveau à partir de la même origine que l'application. Derrière
un reverse proxy, autorisez les **upgrades WebSocket** et augmentez
`proxy_read_timeout` (voir [Déploiement Docker](./DEPLOIEMENT_DOCKER.md)).
---
## 4. Sécurité
- L'accès au document est **revérifié à la connexion** (permissions du compte).
- Un utilisateur sans droit sur la vault ne peut pas rejoindre la session.
- Les échanges passent par le même domaine que l'application (pas de serveur
tiers).
---
## 5. Limitations & bonnes pratiques
- La collaboration vise les fichiers **Markdown**.
- Évitez d'éditer le même fichier simultanément depuis ObsiGate **et** une
application de synchronisation externe (risque de conflits au niveau fichier).
- Le document est écrit après un court délai ; attendez la fin de la sauvegarde
avant de fermer brutalement l'onglet.
- En cas de conflit de synchronisation externe (Syncthing), l'écran
**Conflits** (`/api/conflicts`) aide à résoudre.
---
## 6. Dépannage
| Symptôme | Piste |
|---|---|
| Les curseurs des autres n'apparaissent pas | Vérifier le WebSocket (proxy sans support `Upgrade`) |
| Reconnecté sans cesse | Réseau instable ou timeout proxy trop court |
| Modifications non persistées | Vault montée en lecture seule (`:ro`) ? |
| `401` à la connexion | Session expirée — se reconnecter |
| Accès refusé | Le compte n'a pas la permission sur cette vault |
+221
View File
@@ -0,0 +1,221 @@
# 🐳 Guide de déploiement Docker
Ce guide couvre l'installation, la configuration et l'exploitation d'ObsiGate
avec Docker / Docker Compose, y compris le reverse proxy HTTPS et les mises à jour.
> **Public :** administrateurs, ops
> **Voir aussi :** [Prise en main](./PRISE_EN_MAIN.md) ·
> [Authentification & sécurité](./AUTHENTIFICATION_SECURITE.md) ·
> [`DEVELOPMENT_AND_RELEASES.md`](../DEVELOPMENT_AND_RELEASES.md)
---
## 1. Prérequis
| Composant | Version minimale |
|---|---|
| Docker | ≥ 20.10 |
| docker-compose | ≥ 2.0 |
| Espace disque | ~200 Mo pour l'image |
Systèmes supportés : Linux (Ubuntu, Debian…), macOS (Intel & Apple Silicon),
Windows (Docker Desktop), NAS compatibles Docker (Synology, QNAP…).
---
## 2. Configuration de `docker-compose.yml`
```yaml
services:
obsigate:
build:
context: .
image: obsigate:latest
container_name: obsigate
restart: unless-stopped
ports:
- "2020:8080" # port local 2020 → conteneur 8080
volumes:
- /home/user/Documents/Obsidian-Recettes:/vaults/Recettes:ro
- /home/user/Documents/Obsidian-IT:/vaults/IT:ro
- ./data:/app/data # persistance auth/config/backups
environment:
- VAULT_1_NAME=Recettes
- VAULT_1_PATH=/vaults/Recettes
- VAULT_2_NAME=IT
- VAULT_2_PATH=/vaults/IT
- OBSIGATE_AUTH_ENABLED=true
- OBSIGATE_ADMIN_USER=admin
env_file:
- .env # secrets (mot de passe admin…)
```
> **Important :** les chemins de vaults doivent être **absolus** et montés en
> **lecture seule** (`:ro`) sauf si vous voulez autoriser l'édition depuis
> ObsiGate. Le dossier `./data` doit être **persistant**.
### Variables de vault
| Variable | Description | Exemple |
|---|---|---|
| `VAULT_N_NAME` | Nom affiché | `Recettes` |
| `VAULT_N_PATH` | Chemin dans le conteneur | `/vaults/Recettes` |
| `VAULT_N_ATTACHMENTS_PATH` | Dossier d'attachements (optionnel) | `Assets/Images` |
| `VAULT_N_SCAN_ATTACHMENTS` | Scanner les images au démarrage | `true` |
**Nommage :** lettres, chiffres et tirets uniquement ; le nom doit correspondre au
chemin interne.
---
## 3. Construire et lancer
### 3.1 Script `build.sh` (recommandé)
```bash
chmod +x build.sh # une seule fois
./build.sh
```
Le script :
1. vérifie Docker et Docker Compose (versions) ;
2. valide `docker-compose.yml` (présence + syntaxe) ;
3. contrôle chaque volume monté (avertit si la source n'existe pas) ;
4. construit l'image (multi-stage, ~180 Mo) ;
5. démarre le conteneur ;
6. affiche le statut puis les logs en temps réel.
| Option | Description |
|---|---|
| `--help`, `-h` | Aide complète |
| `--build-only` | Construire sans démarrer |
| `--no-cache` | Rebuild complet sans cache **(défaut)** |
| `--cache` | Utiliser le cache Docker (plus rapide) |
| `--progress=plain` / `--progress=tty` | Sortie verbeuse / interactive |
### 3.2 Alternative manuelle
```bash
docker compose build --no-cache
docker compose up -d
```
### 3.3 Exploitation
```bash
docker compose down # arrêter
docker compose up -d # redémarrer sans rebuild
docker compose logs -f # logs temps réel
docker compose logs --tail=100 obsigate
```
> **Compatibilité Docker :** l'image utilise une variante `uvicorn` minimale et
> `fastapi 0.110.3` pour éviter des dépendances natives optionnelles
> (`watchfiles`, `uvloop`, `httptools`, `fastapi-cli`…) qui échouent sur Alpine,
> ARM ou i386.
---
## 4. Reverse proxy & HTTPS
ObsiGate sert du HTTP en clair ; placez un reverse proxy devant pour TLS.
### 4.1 Nginx (exemple)
```nginx
server {
listen 443 ssl http2;
server_name obsigate.example.com;
ssl_certificate /etc/letsencrypt/live/obsigate.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/obsigate.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:2020;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade; # WebSocket collab
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s; # SSE / WebSocket
}
}
```
### 4.2 Variables à activer derrière un proxy
```bash
OBSIGATE_SECURE_COOKIES=true # cookie Secure (HTTPS uniquement)
OBSIGATE_TRUST_PROXY=true # confiance à X-Forwarded-For / Host
```
> N'activez `OBSIGATE_TRUST_PROXY` **que** derrière un proxy de confiance, sinon
> l'adresse IP client peut être usurpée (rate limiting, audit).
Cloudflare Tunnel, Caddy et Traefik fonctionnent de la même façon (pensez au
support WebSocket et aux longs timeouts pour le SSE).
---
## 5. Healthcheck & supervision
L'image intègre un healthcheck sur `/api/health` (statut, version, stats). Vous
pouvez aussi l'interroger depuis l'hôte :
```bash
curl -s http://localhost:2020/api/health
curl -s http://localhost:2020/api/health/detailed # admin
```
`/api/admin/stream` fournit un flux d'administration (admin uniquement).
---
## 6. Mises à jour
```bash
git pull
./build.sh # reconstruit et redémarre
```
Vos données (`./data`) et vos vaults (volumes `:ro`) sont conservées. Pour un
rebuild propre sans cache : `./build.sh --no-cache`.
> **Version :** le fichier `VERSION` à la racine est la source unique de vérité ;
> l'image et l'UI affichent la même version. Voir
> [`DEVELOPMENT_AND_RELEASES.md`](../DEVELOPMENT_AND_RELEASES.md).
---
## 7. Sauvegardes
- **Données applicatives** : sauvegardez `./data` (utilisateurs, clés, partages,
webhooks, jetons).
- **Vos notes** : ObsiGate n'écrit dans les vaults que si elles sont montées en
écriture. Un backup automatique interne est créé dans `.obsigate-backup/` avant
chaque modification (rotation 10 Mo d'audit).
- **Backups desktop** : voir [Desktop](./DESKTOP.md).
---
## 8. Multi-plateforme
L'image est publiée pour `linux/amd64`, `linux/arm64`, `linux/arm/v7` et
`linux/386`. Sur un NAS ou un Raspberry Pi, choisissez la variante correspondante
(Buildx / `platform:` dans le compose).
---
## 9. Dépannage
| Symptôme | Piste |
|---|---|
| Port déjà utilisé | `sudo netstat -tulpn \| grep 2020` puis changer `ports: "2021:8080"` |
| Vault introuvable | Chemin absolu, permissions de lecture, redémarrer après modif |
| Build qui échoue | `docker system prune -f` puis `./build.sh --progress=plain` |
| Logs | `docker compose logs -f obsigate` |
| Widgets temps réel inopérants derrière un proxy | Autoriser les upgrades WebSocket et augmenter `proxy_read_timeout` |
| Login « insecure cookie » | Passer en HTTPS ou retirer `OBSIGATE_SECURE_COOKIES` |
+201
View File
@@ -0,0 +1,201 @@
# 🖥️ Guide de l'application desktop (Tauri)
ObsiGate Desktop est une application native construite avec
[Tauri](https://tauri.app/) (Rust + webview système). Elle embarque le backend
Python et le frontend dans un exécutable autonome — **zéro Docker, zéro ligne de
commande**.
> **Public :** tous les utilisateurs · **Statut :** version 2.x, binaires en
> cours de stabilisation (build depuis les sources recommandé)
> **Fiche technique :** [`features/desktop-tauri.md`](../features/desktop-tauri.md) ·
> **Checklist E2E :** [`DESKTOP_E2E_CHECKLIST.md`](../DESKTOP_E2E_CHECKLIST.md)
---
## 1. Fonctionnalités natives
| Fonctionnalité | Web | Desktop |
|---|---|---|
| Accès fichiers local | Via upload | Natif (sélecteur de dossier) |
| Thème système | Manuel | Auto (suit l'OS clair/sombre) |
| Notifications | Service Worker | Natif OS |
| Association `.md` | ❌ | ✅ « Ouvrir avec ObsiGate » |
| Icône de barre des tâches (tray) | ❌ | ✅ |
| Auto-update | ❌ | ✅ (vérifie les releases Gitea) |
| Mode hors-ligne | Limité | Complet (backend local) |
---
## 2. Téléchargement des binaires
Les releases sont publiées sur
[Gitea](https://git.dracodev.net/Projets/ObsiGate/releases) :
| Plateforme | Formats |
|---|---|
| **Linux** | `.deb` + `.AppImage` |
| **Windows** | `.msi` + `.exe` (NSIS) |
### Linux
```bash
# .deb (Debian / Ubuntu / Deepin)
sudo dpkg -i obsigate_2.0.0_amd64.deb
# Lancer : ObsiGate depuis le menu applications, ou `obsigate-desktop`
# .AppImage (toute distribution)
chmod +x ObsiGate_2.0.0_amd64.AppImage
./ObsiGate_2.0.0_amd64.AppImage
```
### Windows
```cmd
:: Double-cliquer sur ObsiGate_2.0.0_x64.msi (ou le setup NSIS)
:: Ou lancer ObsiGate depuis le menu Démarrer
```
---
## 3. Démarrage
1. **Lancez l'application** depuis le menu ou la ligne de commande.
2. Le backend Python démarre automatiquement sur `127.0.0.1:17890`
(splash « Démarrage… » pendant le boot).
3. La fenêtre s'ouvre et charge l'interface ObsiGate.
4. **Premier lancement** : sélectionnez le dossier de vos vaults Obsidian via le
sélecteur natif.
5. Pour fermer : icône tray → **Quitter** (arrêt propre du backend).
---
## 4. Construire depuis les sources
Guide détaillé : [`desktop/README.md`](../../desktop/README.md).
### 4.1 Prérequis communs
| Outil | Version | Installation |
|---|---|---|
| Rust (cargo) | ≥ 1.75 | `rustup` |
| Tauri CLI | ≥ 2.0 | `cargo install tauri-cli` |
| Git | — | — |
| Dépendances système Linux | — | `sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev` |
> **Important — staging :** `tauri.conf.json` embarque `backend/**` et
> `frontend/**` **depuis le dossier `desktop/`**. Les scripts de build copient
> automatiquement `../backend` et `../frontend` dans `desktop/` avant
> `cargo tauri build`. Sans ce staging, le build échoue avec
> « glob pattern backend/**/* path not found ».
### 4.2 Windows — `build-windows.bat`
```cmd
REM Prérequis (via Scoop) : rustup, curl, git
scoop install rustup curl git
rustup default stable
cargo install tauri-cli
cd desktop
build-windows.bat
```
Étapes du script :
1. Tue les processus Python résiduels (`taskkill /F /IM python.exe`).
2. Télécharge **Python 3.11 embed** (python.org) → `desktop\python-embed\` +
active pip (`python311._pth`).
3. `pip install -r ..\backend\requirements.txt` dans l'embed.
4. **Staging** : copie `..\backend` et `..\frontend` dans `desktop\`.
5. `cargo tauri build --target x86_64-pc-windows-msvc --bundles nsis`.
6. Copie `python-embed` à côté de l'exécutable pour le mode dev local.
7. Nettoie les dossiers stagés.
→ **Artefact :** `desktop\target\x86_64-pc-windows-msvc\release\bundle\nsis\ObsiGate_2.0.0_x64-setup.exe`
### 4.3 Linux — `build-linux.sh`
```bash
cd desktop
chmod +x build-linux.sh
./build-linux.sh
```
Étapes du script :
1. Vérifie Rust + Tauri CLI, installe les dépendances système (apt).
2. Crée un venv `desktop/python-embed/venv` + `pip install -r ../backend/requirements.txt`.
3. **Staging** : copie `../backend` et `../frontend` dans `desktop/`.
4. `cargo tauri build --target x86_64-unknown-linux-gnu --bundles deb,appimage`.
5. Copie le runtime (`python-embed/`, `backend/`, `frontend/`) à côté de l'exécutable.
→ **Artefacts :**
- `desktop/target/x86_64-unknown-linux-gnu/release/bundle/deb/obsigate_2.0.0_amd64.deb`
- `desktop/target/x86_64-unknown-linux-gnu/release/bundle/appimage/ObsiGate_2.0.0_amd64.AppImage`
---
## 5. Builds CI/CD automatiques
Le workflow [`.gitea/workflows/desktop-build.yml`](../../.gitea/workflows/desktop-build.yml)
construit les binaires desktop à chaque push sur `main` touchant `desktop/**`,
`frontend/**` ou `backend/**` (et manuellement via `workflow_dispatch`), sur des
**runners self-hosted** :
| Job | Runner | Artefacts (30 jours) |
|---|---|---|
| `build-windows` | `[self-hosted, windows, desktop]` | `desktop/target/release/bundle/msi/*.msi` |
| `build-linux` | `[self-hosted, linux, desktop]` | `*.AppImage` + `*.deb` |
Les artefacts sont téléchargeables depuis la page **Actions** du run Gitea ; la
publication en **Gitea Release** est prévue sur les tags `v*`.
---
## 6. Architecture desktop
```
┌────────────────────────────────────────────┐
│ Tauri (Rust) │
│ ├─ Webview (webview système) │
│ │ └─ Frontend (HTML/JS/CSS) │
│ └─ Sidecar Python │
│ └─ uvicorn backend.main:app │
│ └─ port 127.0.0.1:17890 │
└────────────────────────────────────────────┘
```
Cycle de vie : Tauri spawn le backend Python → health check → splash → webview.
À la fermeture : arrêt propre du backend (SIGTERM / kill).
---
## 7. Mises à jour
L'application vérifie les **releases Gitea** et propose la mise à jour (updater
Tauri signé). Le manifeste `latest.json` est généré automatiquement.
> La **signature de code Windows** n'est pas retenue (pas de certificat) : le
> binaire peut déclencher un avertissement SmartScreen. Alternatives possibles :
> SignPath.io (OSS gratuit), Certum OSS, Azure Trusted Signing, certificat EV.
---
## 8. Logs & dépannage
Les logs du backend sont écrits dans :
- **Windows** : `%APPDATA%\ObsiGate\logs\backend.log`
- **Linux** : `~/.config/obsigate/logs/backend.log`
| Symptôme | Piste |
|---|---|
| « Backend ne répond pas » | Vérifier le port `17890` (conflit) et relancer |
| Build « glob pattern backend/**/* not found » | Le staging n'a pas été fait — utiliser les scripts fournis |
| Le sélecteur de dossier ne s'ouvre pas | Permissions système / dialogue natif bloqué |
| Fenêtre blanche | Consulter `backend.log` ; le backend a peut-être échoué au boot |
| Mise à jour non proposée | Vérifier la connectivité aux releases Gitea |
Voir aussi [Prise en main](./PRISE_EN_MAIN.md) et
[Authentification & sécurité](./AUTHENTIFICATION_SECURITE.md).
+191
View File
@@ -0,0 +1,191 @@
# 🧩 Guide MCP (Model Context Protocol)
ObsiGate expose ses vaults à des **clients MCP externes** (Claude Desktop, Cursor,
Cline, tout client compatible MCP) via un serveur **Streamable HTTP** monté sur
`/mcp`. Les outils sont les **mêmes** que ceux de l'assistant in-app : la couche
`backend/tools/` est la source unique de vérité.
> **Statut :** livré (#79 phase E + F) · **Dernière mise à jour :** 2026-09
> **Voir aussi :** [`features/ai-tools-mcp.md`](../features/ai-tools-mcp.md) ·
> [`AI_ARCHITECTURE_GUIDE.md`](../AI_ARCHITECTURE_GUIDE.md) ·
> [API REST](./API_REST.md) · [Assistant IA & Forge](./ASSISTANT_IA_FORGE.md)
---
## 1. Prérequis
1. Une instance ObsiGate accessible (locale ou distante).
2. Une **clé API** (recommandé) ou un **jeton JWT** valide
(`Authorization: Bearer <token>`). Une seule clé fonctionne pour l'API REST
**et** le MCP. Créez-la depuis l'interface (Configurations → 🔑 Clés API & MCP)
ou via `POST /api/auth/tokens` — voir [API REST §2.2](./API_REST.md#22-clés-api-longue-durée-recommandé-pour-scripts--mcp).
3. Si l'authentification est désactivée (`OBSIGATE_AUTH_ENABLED=false`), le
serveur MCP accepte un utilisateur anonyme disposant de tous les vaults.
> Le transport `stdio` n'est pas encore supporté ; utilisez le transport HTTP
> (un pont local type `mcp-remote` si votre client ne gère pas nativement le
> Streamable HTTP distant).
---
## 2. Endpoint & protocole
| Élément | Valeur |
|---|---|
| URL | `https://<obsigate>/mcp` |
| Transport | Streamable HTTP (`POST` JSON-RPC 2.0, `Accept: application/json, text/event-stream`) |
| Auth | `Authorization: Bearer <JWT>` |
| Protocole MCP | `2025-03-26` (négocié à l'`initialize`) |
| Réponses | JSON (`json_response=True`) |
Handshake minimal :
```bash
curl -sS https://obsigate.example/mcp \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json, text/event-stream" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{
"protocolVersion":"2025-03-26","capabilities":{},
"clientInfo":{"name":"curl","version":"1.0"}}}'
```
La réponse contient l'en-tête `Mcp-Session-Id` à réutiliser pour les appels
suivants (`tools/list`, `tools/call`, `resources/read`, …).
---
## 3. Configuration des clients
### Claude Desktop (via pont `mcp-remote`)
```json
{
"mcpServers": {
"obsigate": {
"command": "npx",
"args": [
"-y", "mcp-remote",
"https://obsigate.example/mcp",
"--header", "Authorization: Bearer ${OBSIGATE_TOKEN}"
],
"env": { "OBSIGATE_TOKEN": "eyJ..." }
}
}
}
```
### Cursor
`.cursor/mcp.json` :
```json
{
"mcpServers": {
"obsigate": {
"url": "https://obsigate.example/mcp",
"headers": { "Authorization": "Bearer eyJ..." }
}
}
}
```
### Client générique (config raccourcie)
```json
{"mcpServers": {"obsigate": {
"url": "http://localhost:2020/mcp",
"headers": {"Authorization": "Bearer <clé API>"}
}}}
```
---
## 4. Primitives exposées
### 4.1 Tools
Les outils de **lecture/recherche** sont exposés directement. Les outils
**d'écriture/destructifs** sont exposés via une paire **two-step** :
`propose_<tool>` (aperçu + jeton de confirmation, aucune modification) puis
`apply_<tool>` (consomme le jeton et exécute).
| Catégorie | Outils |
|---|---|
| Vaults / navigation | `list_vaults`, `list_directory`, `list_all_files` |
| Lecture | `read_file`, `read_file_raw`, `get_backlinks`, `list_backups`, `diff_backup`, `get_graph` |
| Recherche | `search_fulltext`, `search_advanced`, `search_paths`, `list_tags`, `suggest_tags`, `list_recent` |
| Écriture (propose/apply) | `create_file`, `create_directory`, `edit_file`, `append_to_file`, `restore_backup` |
| Destructif (propose/apply) | `rename_file`, `rename_directory`, `move_path`, `replace_in_files`, `delete_file`, `delete_directory` |
Flux d'une mutation :
```text
1. tools/call { name: "propose_edit_file",
arguments: { vault, path, content } }
→ { tool, arguments, diff, confirmation_token, expires_in }
2. (l'utilisateur / l'agent valide)
3. tools/call { name: "apply_edit_file",
arguments: { confirmation_token } }
→ { ok: true, data: { ... } }
```
Le jeton est **signé (JWT), à usage unique et à durée de vie limitée**
(`OBSIGATE_MCP_CONFIRMATION_TTL`, défaut 300 s). Un rejeu renvoie `token_reused`.
### 4.2 Resources
| URI | Contenu |
|---|---|
| `vault://<name>` | Vault accessible (métadonnées, nombre de fichiers) |
| `vault://<name>/<path>` | Contenu d'un fichier (lecture seule, **secrets redactés**) |
### 4.3 Prompts
`summarize-directory`, `generate-note`, `find-related`.
---
## 5. Sécurité
- **Permissions par vault** : `check_vault_access` est appliqué à chaque outil
et chaque resource ; un utilisateur ne voit que ses vaults.
- **Anti path-traversal** : `resolve_safe_path` rejette tout chemin hors du vault.
- **Confirmation two-step** pour toute mutation (jeton signé, usage unique).
- **Toggle par vault** `aiDestructiveTools` (défaut : activé) : le désactiver
bloque rename/move/replace/delete tout en laissant create/edit/append.
- **Backup automatique** avant chaque opération destructive.
- **Rate limiting** : par jeton et par outil
(`OBSIGATE_TOOL_RATE_LIMIT`, `OBSIGATE_TOOL_RATE_LIMIT_PER_TOOL`,
`OBSIGATE_TOOL_RATE_WINDOW`). Une limite dépassée renvoie le code `rate_limited`.
- **Redaction des secrets** : les résultats d'outils (lectures, diffs, extraits
de recherche) sont nettoyés avant tout retour au client.
- **Audit** : chaque appel est journalisé (`data/audit.log`, action
`ai_tool_call`) avec arguments sensibles résumés.
### Variables d'environnement
| Variable | Défaut | Rôle |
|---|---|---|
| `OBSIGATE_MCP_CONFIRMATION_TTL` | `300` | Durée de vie (s) des jetons de confirmation |
| `OBSIGATE_TOOL_RATE_LIMIT` | `60` | Appels d'outils max par identité et par fenêtre |
| `OBSIGATE_TOOL_RATE_LIMIT_PER_TOOL` | = global | Appels max par outil et par fenêtre |
| `OBSIGATE_TOOL_RATE_WINDOW` | `60` | Longueur de la fenêtre (s) |
| `BOOKSLM_MAX_TOOL_CALLS` | `25` | Quota d'appels d'outils par run d'agent |
| `BOOKSLM_MAX_TOOL_READ_BYTES` | `200000` | Taille max renvoyée par `read_file` |
---
## 6. Dépannage
| Symptôme | Cause probable / remède |
|---|---|
| `401 Authentification requise` | En-tête `Authorization: Bearer` absent ou jeton expiré |
| `vault_access_denied` | Le jeton n'a pas accès à ce vault (`vaults` / `_token_vaults`) |
| `destructive_tools_disabled` | `aiDestructiveTools=false` pour ce vault |
| `confirmation_required` | Appeler d'abord `propose_<tool>` puis `apply_<tool>` |
| `token_reused` / `invalid_confirmation` | Jeton déjà consommé ou expiré → refaire un `propose_` |
| `rate_limited` | Quota dépassé ; respecter `retry_after` |
| Le client ne se connecte pas | Vérifier le transport Streamable HTTP / le pont `mcp-remote` |
+236
View File
@@ -0,0 +1,236 @@
# 🚀 Guide de prise en main
Ce guide vous fait passer d'une installation fraîche à une utilisation courante
d'ObsiGate : première connexion, découverte de l'interface, navigation dans vos
vaults Obsidian et raccourcis essentiels.
> **Public :** tous les utilisateurs · **Durée de lecture :** ~10 min
> **Voir aussi :** [Déploiement Docker](./DEPLOIEMENT_DOCKER.md) ·
> [Recherche, PDF & Excalidraw](./RECHERCHE_PDF_EXCALIDRAW.md) ·
> [API REST](./API_REST.md)
---
## 1. Qu'est-ce qu'ObsiGate ?
ObsiGate est une **porte d'entrée web ultra-légère** vers vos vaults Obsidian.
Il indexe vos notes en mémoire, les rend accessibles depuis n'importe quel
navigateur (ordinateur, tablette, téléphone) et ajoute une couche moderne :
recherche avancée, lecture Markdown, liens `[[wikilinks]]`, images, PDF,
Excalidraw, Mermaid, assistant IA, collaboration temps réel.
Points clés :
- **Aucune modification de vos vaults** : les volumes sont montés en lecture seule (`:ro`) par défaut.
- **Pas de base de données** : tout l'état tient dans des fichiers JSON sous `data/`.
- **Temps réel** : un watcher surveille le système de fichiers et met l'index à jour à chaud.
- **Multi-vault** : plusieurs vaults peuvent être affichés et recherchés simultanément.
---
## 2. Prérequis
| Composant | Version | Remarque |
|---|---|---|
| Docker | ≥ 20.10 | ou Node/`uv` pour un lancement manuel |
| docker-compose | ≥ 2.0 | inclus avec Docker Desktop |
| Navigateur | récent | Chrome, Edge, Firefox, Safari |
Vous aurez aussi besoin du **chemin absolu** de chaque vault Obsidian sur la
machine qui héberge Docker.
---
## 3. Lancer ObsiGate en 3 étapes
> La procédure complète (reverse proxy, HTTPS, mises à jour) est détaillée dans le
> [Guide de déploiement Docker](./DEPLOIEMENT_DOCKER.md).
### 3.1 Cloner le dépôt
```bash
git clone https://git.dracodev.net/Projets/ObsiGate.git
cd ObsiGate
```
### 3.2 Déclarer vos vaults
Éditez `docker-compose.yml` pour monter vos dossiers (chemins absolus, lecture seule) :
```yaml
volumes:
- /home/user/Documents/Obsidian-Recettes:/vaults/Recettes:ro
- /home/user/Documents/Obsidian-IT:/vaults/IT:ro
- ./data:/app/data # persistance auth/config
environment:
- VAULT_1_NAME=Recettes
- VAULT_1_PATH=/vaults/Recettes
- VAULT_2_NAME=IT
- VAULT_2_PATH=/vaults/IT
```
Créez le fichier de secrets à partir du modèle :
```bash
cp .env.example .env
# Éditez .env (mot de passe admin, options d'auth…)
```
### 3.3 Construire et démarrer
```bash
chmod +x build.sh # une seule fois
./build.sh
```
`build.sh` vérifie Docker, valide les volumes, construit l'image et démarre le
conteneur. Ouvrez ensuite **http://localhost:2020**.
> Options utiles : `./build.sh --help`, `./build.sh --cache` (rebuild rapide),
> `./build.sh --build-only` (construire sans démarrer).
---
## 4. Premier accès
### 4.1 Si l'authentification est désactivée (défaut)
Vous arrivez directement sur l'interface. Toutes les fonctionnalités sont
accessibles sans compte — **à réserver à un usage sur réseau de confiance**.
### 4.2 Si l'authentification est activée
L'écran de connexion s'affiche. Au **tout premier démarrage**, ObsiGate crée un
compte admin et affiche le mot de passe **une seule fois dans les logs** :
```bash
docker compose logs obsigate | grep -A4 "FIRST"
```
Changez ce mot de passe dès la première connexion (menu → profil →
*Changer le mot de passe*). La gestion complète des comptes, du MFA et des
permissions est décrite dans le
[Guide Authentification & sécurité](./AUTHENTIFICATION_SECURITE.md).
---
## 5. Découvrir l'interface
L'interface se compose de trois zones principales.
### 5.1 L'en-tête (header)
| Élément | Rôle |
|---|---|
| 🔍 **Barre de recherche globale** | Recherche dans toutes les vaults autorisées |
| Filtre | Restreint la recherche (type, tag, vault…) |
| Sélecteur de vault | Bascule l'arborescence sur une vault ou « Toutes les vaults » |
| Utilisateur | Nom du compte connecté (si auth activée) |
| Version | Version courante d'ObsiGate |
| ⚙️ **Options** | Configuration, thème, guide d'utilisation, administration |
### 5.2 La barre latérale (sidebar)
Elle regroupe les vues principales via des icônes :
- **Arborescence** — parcourt les dossiers et fichiers de la vault sélectionnée.
- **Graphe** — vue force-directed des liens entre notes.
- **Récents** — derniers fichiers ouverts.
- **Signets** — vos fichiers et recherches enregistrés.
- **Partagés** — liens de partage public que vous avez créés.
Un champ **« Filtrer fichiers… »** restreint l'arborescence en temps réel, et le
bouton **Aa** ajuste l'affichage des libellés.
### 5.3 La zone de contenu
Elle affiche l'onglet actif : tableau de bord **Statistiques**, **Bookmarks**,
**Récents**, **Partagés**, ou le document ouvert. Les documents s'ouvrent dans
des **onglets** (avec possibilité de vue multi-panneaux / split view).
---
## 6. Navigation et lecture
1. **Déployez une vault** dans la sidebar (clic sur son nom).
2. **Cliquez sur un dossier** pour l'ouvrir, sur un **fichier** pour l'afficher.
3. Le **breadcrumb** en haut du document permet de remonter rapidement.
4. Les **wikilinks** `[[note]]` sont cliquables ; les images et diagrammes
s'affichent automatiquement.
5. Utilisez **Ctrl + clic** sur un lien pour l'ouvrir en aperçu rapide selon le
contexte, ou ouvrir le graphe centré sur un nœud.
### Créer et modifier
- **Bouton « Editer »** : ouvre le document dans l'éditeur Markdown (CodeMirror).
- **Bouton « Forge »** (éditeur avancé) : ouvre la version enrichie avec
assistant IA intégré. Voir [Assistant IA & Forge](./ASSISTANT_IA_FORGE.md).
- **Nouveau fichier / dossier** : depuis les actions de la sidebar ou la palette
de commandes.
- **Sauvegarde** : `Ctrl + S` (et auto-sauvegarde dans l'éditeur IA).
> Selon le mode, la lecture et l'édition se remplacent : `Editer` et `Forge`
> prennent la place de la vue lecture ; revenez avec `✓` / `×` ou `Échap`.
---
## 7. Rechercher
La recherche est un point fort d'ObsiGate : index inversé TF-IDF, stemming
français, normalisation des accents, facettes et pagination. La syntaxe complète
(`tag:`, `#`, `vault:`, `title:`, `path:`, `ext:`, phrases exactes) est décrite
dans le [Guide Recherche, PDF & Excalidraw](./RECHERCHE_PDF_EXCALIDRAW.md).
Démarrage rapide :
- Tapez dans la barre de recherche, `Ctrl + K` pour y revenir.
- `/` focalise la recherche hors champ de saisie.
- `/` + `↑`/`↓` navigue dans les suggestions.
---
## 8. Apparence et confort
- **Thème clair/sombre** : bascule persistée en `localStorage` ; le desktop suit
aussi le thème du système.
- **Thèmes** : clair, sombre, contraste élevé, sépia — import/export possible.
- **Responsive** : l'interface s'adapte au mobile (éditeur tactile, barre
d'outils flottante).
- **PWA** : installable comme application native, mode hors-ligne partiel.
Voir [PWA & mode hors-ligne](./PWA_HORS_LIGNE.md).
---
## 9. Raccourcis clavier essentiels
| Action | Raccourci |
|---|---|
| Palette de commandes | `Ctrl + Shift + Space` |
| Palette de fichiers (navigation rapide) | `Ctrl + Alt + Space` |
| Focus barre de recherche | `Ctrl + K` |
| Recherche rapide (hors champ texte) | `/` |
| Sauvegarder le fichier ouvert | `Ctrl + S` |
| Rechercher dans le document | `Ctrl + F` |
| Completion IA inline (éditeur) | `Ctrl + J` |
| Insertion rapide (éditeur Forge) | `Alt + I` |
| Fermer l'éditeur / modale | `Échap` |
| Aide de l'éditeur Forge | `F1` |
| Naviguer dans les suggestions | `↑` / `↓` |
| Lancer la recherche / valider | `Entrée` |
> Le panneau **Raccourcis & Astuces** du tableau de bord Statistiques récapitule
> ces raccourcis directement dans l'application.
---
## 10. Et ensuite ?
| Objectif | Guide |
|---|---|
| Mieux chercher, lire PDF et Excalidraw | [Recherche, PDF & Excalidraw](./RECHERCHE_PDF_EXCALIDRAW.md) |
| Utiliser l'IA intégrée | [Assistant IA & Forge](./ASSISTANT_IA_FORGE.md) |
| Éditer à plusieurs | [Édition & collaboration](./COLLABORATION.md) |
| Sécuriser l'accès | [Authentification & sécurité](./AUTHENTIFICATION_SECURITE.md) |
| Automatiser via API/MCP | [API REST](./API_REST.md) · [MCP](./MCP.md) |
| Installer l'application native | [Desktop (Tauri)](./DESKTOP.md) |
+136
View File
@@ -0,0 +1,136 @@
# 📱 Guide PWA & mode hors-ligne
ObsiGate est une **Progressive Web App (PWA)** : installez-la comme une
application native, consultez vos notes **hors-ligne**, recevez des
notifications et synchronisez vos modifications à la reconnexion.
> **Public :** tous les utilisateurs · **Guides techniques :**
> [`PWA_GUIDE.md`](../PWA_GUIDE.md) · [`INSTALLATION_PWA.md`](../INSTALLATION_PWA.md)
> **Voir aussi :** [Prise en main](./PRISE_EN_MAIN.md) · [Édition & collaboration](./COLLABORATION.md)
---
## 1. Qu'est-ce que la PWA d'ObsiGate ?
Une PWA combine le meilleur du web et du natif :
- **Installation** sur l'écran d'accueil, sans store.
- **Mode hors-ligne** : interface et dernières données consultées mises en cache.
- **Notifications** : alertes de mise à jour et Web Push.
- **Performance** : chargement rapide via cache intelligent.
- **Multi-plateforme** : desktop, mobile, tablette.
---
## 2. Installer la PWA
### Desktop (Chrome, Edge, Brave)
1. Ouvrez ObsiGate dans le navigateur.
2. Cliquez sur l'icône d'installation dans la barre d'adresse (➕ / ⬇️).
3. Cliquez sur **Installer** dans la popup.
4. ObsiGate apparaît dans vos applications.
*Alternative :* menu ⋮ → **Installer ObsiGate…**
### Android (Chrome)
1. Ouvrez ObsiGate dans Chrome.
2. Menu ⋮ → **Ajouter à l'écran d'accueil**.
3. Confirmez.
### iOS / iPadOS (Safari)
1. Ouvrez ObsiGate dans Safari.
2. Bouton Partager 📤 → **Sur l'écran d'accueil**.
3. Nommez l'application puis **Ajouter**.
---
## 3. Mode hors-ligne
Le **Service Worker** (`frontend/sw.js`) met en cache :
- l'interface (HTML, CSS, JavaScript, manifeste) ;
- les ressources statiques (icônes, polices) ;
- les dernières données API consultées.
### Stratégies de cache
| Ressource | Stratégie |
|---|---|
| Code (HTML/JS/CSS/manifest) | **Network-first** (cache en secours hors-ligne) |
| API | **Network-first** (+ cache hors-ligne) |
| Autres assets (images, polices) | **Stale-while-revalidate** |
| Nettoyage | Purge des caches d'une version antérieure à l'activation |
> Le choix **network-first** est délibéré : les assets ne sont pas fingerprintés,
> un cache-first servirait indéfiniment un ancien build sur mobile.
### File de synchronisation & conflits
- Les modifications faites hors-ligne sont stockées (IndexedDB) et rejouées à la
reconnexion.
- Les conflits éventuels sont détectés et peuvent être résolus (écran
**Conflits**, `GET /api/conflicts`).
### Tester hors-ligne
1. DevTools (F12) → onglet **Network**.
2. Cochez **Offline**.
3. Rechargez : l'application doit fonctionner avec le cache.
---
## 4. Notifications (Web Push)
- Abonnement à partir de l'interface (permission navigateur requise).
- Endpoints : `GET /api/push/vapid-public-key`,
`POST /api/push/subscribe`, `DELETE /api/push/subscribe`,
`GET /api/push/subscriptions`.
- Les notifications sont signées **VAPID** et peuvent prévenir de changements
(collaboration, mises à jour).
---
## 5. Mises à jour
- Vérification régulière des mises à jour.
- Notification quand une nouvelle version est disponible.
- Mise à jour en un clic, **sans perte de données**.
- Le numéro `SW_VERSION` invalide l'ancien cache à chaque livraison.
### Forcer une mise à jour (console)
```javascript
navigator.serviceWorker.getRegistration().then(reg => reg.update());
```
---
## 6. Débogage
### Vérifier l'installation
Chrome DevTools → onglet **Application** :
- **Manifest** : métadonnées ;
- **Service Workers** : enregistrement ;
- **Cache Storage** : contenu du cache.
### Désinstaller le Service Worker
```javascript
navigator.serviceWorker.getRegistrations().then(regs => regs.forEach(r => r.unregister()));
```
---
## 7. Limites
- Le hors-ligne dépend des données déjà mises en cache.
- Les actions d'écriture hors-ligne s'appliquent à la reconnexion (pas en temps
réel).
- iOS applique des contraintes spécifiques (persistance, notifications).
Voir [Édition & collaboration](./COLLABORATION.md) pour le temps réel.
+54
View File
@@ -0,0 +1,54 @@
# 📚 Guides d'utilisation ObsiGate
Bienvenue dans le répertoire des **guides utilisateur** d'ObsiGate. Chaque guide est
autonome, écrit en français et illustré d'exemples concrets (commandes, configuration,
captures conceptuelles).
> **Vous découvrez ObsiGate ?** Commencez par le **[Guide de prise en main](./PRISE_EN_MAIN.md)**.
> Une aide rapide est aussi intégrée directement dans l'application (menu Options →
> **Guide d'utilisation**, FR/EN, téléchargeable en Markdown et PDF).
---
## 🗂️ Sommaire des guides
| Guide | Public | Contenu |
|---|---|---|
| 🚀 [Prise en main](./PRISE_EN_MAIN.md) | Tous | Premier lancement, interface, navigation, vaults, raccourcis |
| 🔍 [Recherche, PDF & Excalidraw](./RECHERCHE_PDF_EXCALIDRAW.md) | Tous | Syntaxe de requête, recherche sémantique, lecteur PDF, diagrammes |
| 🤖 [Assistant IA & Forge](./ASSISTANT_IA_FORGE.md) | Tous | Fournisseurs, éditeur IA, BooksLM, Forge, commandes `@` / `/` |
| 📝 [Édition & collaboration](./COLLABORATION.md) | Tous | Édition simultanée, curseurs distants, persistance |
| 📱 [PWA & mode hors-ligne](./PWA_HORS_LIGNE.md) | Tous | Installation PWA, cache, file de synchronisation, notifications |
| 🔌 [API REST](./API_REST.md) | Développeurs | Authentification, clés API, endpoints, exemples `curl`, SSE |
| 🧩 [Serveur MCP](./MCP.md) | Développeurs / IA | Brancher Claude Desktop, Cursor, Cline… sur vos vaults |
| 🔒 [Authentification & sécurité](./AUTHENTIFICATION_SECURITE.md) | Admin | Utilisateurs, MFA, permissions par vault, bonnes pratiques |
| 🐳 [Déploiement Docker](./DEPLOIEMENT_DOCKER.md) | Admin / Ops | `docker-compose`, volumes, reverse proxy, mises à jour |
| 🖥️ [Application desktop (Tauri)](./DESKTOP.md) | Tous | Installation, premier lancement, build depuis les sources |
---
## 🧭 Par où commencer ?
- **Je veux juste utiliser l'application** → [Prise en main](./PRISE_EN_MAIN.md)
- **Je veux sécuriser mon instance** → [Authentification & sécurité](./AUTHENTIFICATION_SECURITE.md)
- **Je veux brancher une IA** → [Assistant IA & Forge](./ASSISTANT_IA_FORGE.md) puis [MCP](./MCP.md)
- **Je veux scripter/automatiser** → [API REST](./API_REST.md)
- **Je veux héberger sur un serveur** → [Déploiement Docker](./DEPLOIEMENT_DOCKER.md)
---
## 📖 Documentation associée
| Type | Où |
|---|---|
| Vue d'ensemble produit | [`README.fr.md`](../../README.fr.md) · [`README.md`](../../README.md) |
| Conception détaillée par fonctionnalité | [`docs/features/`](../features/) |
| Standards de code | [`docs/CONTRIBUTING.md`](../CONTRIBUTING.md) |
| Méthode de livraison (Definition of Done) | [`docs/DELIVERY_WORKFLOW.md`](../DELIVERY_WORKFLOW.md) |
| Roadmap / travail à venir | [`docs/ROADMAP.md`](../ROADMAP.md) |
| Historique des versions | [`CHANGELOG.md`](../../CHANGELOG.md) |
| API interactive (Swagger / ReDoc) | `/docs` · `/redoc` (instance ObsiGate) |
> **Convention :** ce répertoire est la **porte d'entrée utilisateur**. Le *comment*
> (utilisation) vit ici ; le *pourquoi* (conception technique) vit dans
> [`docs/features/`](../features/). Ne jamais dupliquer le détail technique des fiches.
+193
View File
@@ -0,0 +1,193 @@
# 🔍 Guide Recherche, PDF & Excalidraw
ObsiGate va au-delà de la simple lecture : recherche puissante, rendu des
documents riches (PDF, diagrammes) et indexation de leur contenu pour que tout
soit retrouvable.
> **Public :** tous les utilisateurs
> **Fiches techniques :** [`features/semantic-search.md`](../features/semantic-search.md) ·
> [`features/pdf.md`](../features/pdf.md) · [`features/excalidraw.md`](../features/excalidraw.md)
---
## 1. Recherche plein texte (TF-IDF)
Le moteur d'ObsiGate s'appuie sur un **index inversé** et un scoring **TF-IDF**
avec :
- **Boost titre** — une correspondance dans le titre pèse 3× plus.
- **Normalisation des accents** — `resume` trouve `résumé`, `elephant` trouve `éléphant`.
- **Stemming français** — les variantes des mots sont rapprochées.
- **Snippets surlignés** — les termes trouvés sont mis en `<mark>` dans l'extrait.
- **Facettes** — compteurs par vault et par tag sur les résultats.
- **Pagination** — 50 résultats par page.
- **Tri** — par pertinence (TF-IDF) ou par date de modification.
- **Chips de filtres** — les filtres actifs apparaissent sous forme de puces retirables.
- **Historique** — les 50 dernières recherches sont conservées en `localStorage`.
La recherche s'effectue **sans I/O disque** : le contenu est déjà en mémoire.
---
## 2. Syntaxe de requête
| Opérateur | Description | Exemple |
|---|---|---|
| `tag:<nom>` | Filtre par tag | `tag:recette docker` |
| `#<nom>` | Raccourci de tag | `#linux serveur` |
| `vault:<nom>` | Filtre par vault | `vault:IT kubernetes` |
| `title:<texte>` | Filtre par titre | `title:pizza` |
| `path:<texte>` | Filtre par chemin | `path:recettes/soupes` |
| `ext:<type>` | Filtre par type de fichier | `ext:md kubernetes` |
| `"phrase exacte"` | Recherche d'une phrase | `tag:"multi mots"` |
Les opérateurs sont **combinables** :
```text
tag:linux vault:IT ext:md serveur web
```
Cette requête cherche « serveur web » dans les fichiers Markdown de la vault
`IT` portant le tag `linux`.
### Filtres par extension
| Extension | Contenu |
|---|---|
| `ext:md` | Notes Markdown |
| `ext:py`, `ext:sh`, `ext:js` | Scripts et code |
| `ext:pdf` | Documents PDF (texte extrait) |
| `ext:excalidraw` | Diagrammes Excalidraw (texte extrait) |
---
## 3. Autocomplétion et suggestions
- **`/api/suggest`** — suggère des titres de fichiers.
- **`/api/tags/suggest`** — suggère des tags.
- Navigation clavier : `↑` / `↓` puis `Entrée` ; `Échap` ferme les suggestions.
### Raccourcis de recherche
| Raccourci | Action |
|---|---|
| `Ctrl + K` / `Cmd + K` | Focaliser la barre de recherche |
| `/` | Focaliser la recherche (hors champ texte) |
| `↑` / `↓` | Naviguer dans les suggestions |
| `Entrée` | Sélectionner la suggestion active ou lancer la recherche |
| `Échap` | Fermer les suggestions / quitter la recherche |
Recherches sauvegardées et signets sont disponibles via l'API
(`/api/saved-searches`, `/api/bookmarks`).
---
## 4. Recherche sémantique (optionnelle)
Au classement TF-IDF peut s'ajouter un classement **par embeddings**, fusionné
via la méthode **RRF** (Reciprocal Rank Fusion). Activation : touche `~`
(ou `Alt + S`) dans la recherche.
Deux modes :
1. **Sans dépendance** — un *embedder* par hachage fournit une base utilisable
immédiatement.
2. **Embeddings réels** — installez `backend/requirements-semantic.txt` et/ou
renseignez les variables `OBSIGATE_EMBEDDING_*` pour utiliser
`all-MiniLM-L6-v2`.
Détails et configuration :
[`features/semantic-search.md`](../features/semantic-search.md).
---
## 5. Support PDF
### Lecture
Les fichiers PDF de vos vaults s'affichent **en ligne** dans le navigateur via le
visualiseur PDF natif (iframe + `<embed>`). Le fichier est **streamé** en HTTP
Range (`206 Partial Content`) : les gros PDF se chargent progressivement.
### Recherche
Le texte est **extrait à l'indexation** (`pypdf` / `pymupdf`), donc le contenu
des PDF est recherchable via la recherche plein texte. Utilisez `ext:pdf` pour
limiter les résultats aux PDF.
### Métadonnées
`GET /api/file/{vault}/pdf/info` renvoie les métadonnées (pages, titre, auteur)
**sans transférer** le document.
```bash
curl "http://localhost:2020/api/file/Recettes/pdf/info?path=menu.pdf"
```
### Limites
- **Pas d'OCR** : les PDF scannés (images) ne sont pas recherchables.
- Pas d'annotation ni d'édition du PDF lui-même.
---
## 6. Diagrammes Excalidraw
Les fichiers `.excalidraw` et `.excalidraw.md` (dont le format compressé du
**plugin Obsidian Excalidraw**) s'ouvrent dans un **éditeur visuel Excalidraw
complet**, dans une iframe sandboxée.
- **Dessin et édition** sans quitter ObsiGate.
- **Sauvegarde automatique** (débounce 2 s) ou `Ctrl + S`.
- **Thème** clair/sombre suivi automatiquement.
- **Texte indexé** : le texte des éléments du diagramme est extrait à
l'indexation et donc recherchable (`ext:excalidraw`).
Fiche technique : [`features/excalidraw.md`](../features/excalidraw.md).
---
## 7. Autres contenus riches
### Mermaid
Les blocs de code ` ```mermaid ` sont rendus en diagrammes interactifs (live
preview, thèmes, zoom, plein écran, pré-processeur compatible syntaxe Obsidian).
### Images Obsidian
Toutes les syntaxes d'images sont supportées avec résolution intelligente en
7 stratégies :
1. chemin absolu ;
2. dossier d'attachements configuré (`VAULT_N_ATTACHMENTS_PATH`) ;
3. index de démarrage (correspondance unique) ;
4. même répertoire que la note ;
5. racine de la vault ;
6. index de démarrage (correspondance la plus proche) ;
7. repli : `[image not found: fichier.ext]`.
Rescan manuel des attachements :
```bash
curl -X POST "http://localhost:2020/api/attachments/rescan/Recettes"
```
### Graphe et backlinks
- **Graphe** : vue force-directed (Barnes-Hut), filtres (tag, type), profondeur,
mode focus, export PNG, aperçu au survol (`Ctrl + clic`).
- **Backlinks** : `GET /api/file/{vault}/backlinks?path=…` liste les notes
pointant vers un document.
---
## 8. Dépannage
| Symptôme | Piste |
|---|---|
| Un PDF ne s'affiche pas | Vérifier la taille (`OBSIGATE_PDF_MAX_SIZE_MB`, défaut 50 Mo) |
| Le texte d'un PDF scanné n'est pas trouvé | Pas d'OCR : normal |
| Une image reste introuvable | Configurer `VAULT_N_ATTACHMENTS_PATH`, puis rescan |
| La recherche sémantique ne s'active pas | Vérifier le toggle `~` et `OBSIGATE_EMBEDDING_*` |
| Résultats obsolètes | Forcer une réindexation : `GET /api/index/reload` |
+9
View File
@@ -176,6 +176,10 @@ Avant de corriger quoi que ce soit, un agent IA doit :
| *BUG-065* | [🟡 IMPORTANT] Éditeur Excalidraw : l'auto-save recharge la page en pleine édition | 🟢 corrigé | P1 | 📱 frontend | IA | `frontend/js/excalidraw-viewer.js`, `frontend/js/utils.js`, `frontend/excalidraw-editor.html`, `tests/frontend/excalidraw-viewer.test.mjs` | Ouvrir un `.excalidraw` puis modifier un élément : au bout de 2 s la vue se recharge | Chaque modification déclenchait un `PUT save` 2 s plus tard → SSE `index_updated` → `reloadExternalWrite` → `openFile` → **recréation de l'iframe** (refresh visible). Auto-save supprimée : sauvegarde explicite (bouton 💾 / Ctrl+S). `reloadExternalWrite` ignore le fichier si un iframe Excalidraw est ouvert (`iframe[data-excalidraw-vault/path]`). Le badge « Modified » ne réagit plus aux changements d'`appState` (resize/zoom) mais à la signature des éléments. | Vérifié Playwright : plus de refresh, badge stable après bascule plein écran. Test statique (absence de `requestSave`/`saveTimer`). |
| *BUG-066* | [🔵 MINEUR] Configuration : icônes manquantes dans la table des matières (« Fichiers cachés », « Partages publics ») | 🟢 corrigé | P3 | 📱 frontend | IA | `frontend/locales/{fr,en}.json` | Ouvrir Configuration → observer le sommaire : les entrées « Fichiers cachés » et « Partages publics » n'ont pas d'icône | `config.section_hidden` → « 🗂️ Fichiers cachés » / « 🗂️ Hidden files », `config.section_shares` → « 📤 Partages publics » (EN avait déjà l'icône). Test : `tests/frontend/unit.test.mjs` (+1 : toutes les entrées du sommaire portent une icône FR/EN) | Les libellés du sommaire utilisent des clés i18n distinctes des titres de section (`auto.f8ba6127`, `config.section_partages-publics`) qui, elles, avaient l'icône |
| *BUG-067* | [🔵 MINEUR] Guide d'utilisation : l'entrée « 📱 Mobile » du sommaire ne fait rien (section absente) | 🟢 corrigé | P3 | 📱 frontend | IA | `frontend/index.html` | Ouvrir le Guide → cliquer « 📱 Mobile » dans le sommaire : rien ne se passe | L'ancre `#help-mobile-editor` était présente dans la TOC mais aucune section `id="help-mobile-editor"` n'existait (l'édition mobile n'était qu'un h3 de `help-edition`). Fix #105 : section dédiée créée avec ancre + entrée de nav cohérente. | Vérifié par test statique `tests/test_guide.py::test_nav_anchors_resolve` |
| *BUG-068* | Configuration — section « 🔒 Sécurité du compte » inachevée : boutons hors thème, QR code invisible, fiabilité des fonctions à valider | 🟢 corrigé | P1 | 📱 frontend + ⚙️ backend | IA | `frontend/js/auth.js`, `frontend/style.css`, `backend/auth/router.py` | Configuration → 🔒 Sécurité du compte | `frontend/style.css` (+`config-btn-primary`/`danger` thème), `backend/auth/router.py` (`qr_data_url` segno local), `frontend/js/auth.js` (QR local + fallback, recovery WebAuthn, carte mot de passe, escapeHtml labels), locales FR/EN, `backend/requirements.txt` (+segno) ; tests `tests/test_mfa.py` (+1) + `tests/frontend/mfa-settings.test.mjs` (nouveau, 9) | pytest 1241 passed / 6 skipped, ruff 0, mypy 0, frontend unit + validate-imports verts |
| *BUG-069* | Login 2FA bloqué sans erreur : après user+pwd corrects, la page de login reste affichée et le challenge MFA n'apparaît jamais | 🟢 corrigé | P0 | 📱 frontend | IA | `frontend/js/auth.js`, `frontend/index.html` | Activer 2FA → logout → login (bon user+pwd) | `frontend/js/auth.js` (`showMfaChallenge` → `.login-card` + erreur `mfa.challenge_unavailable` si montage impossible), locales FR/EN ; tests `tests/frontend/mfa-settings.test.mjs` (+2) | Reproduit au navigateur avant correctif (challenge jamais affiché), vérifié après : challenge affiché, code erroné → erreur, code valide (200) → app ; frontend mfa-settings 11/11, unit + validate-imports verts |
| *BUG-070* | Activation clé physique WebAuthn impossible : « Validation du credential WebAuthn échouée » à chaque tentative | 🟢 corrigé | P0 | ⚙️ backend | IA | `backend/auth/webauthn_mfa.py`, `backend/auth/router.py` | Config → Sécurité → Ajouter une clé → cérémonie navigateur → 400 | `resolve_relying_party()` (rp_id/origines dérivés de la requête, config explicite prioritaire, forwarded si TRUST_PROXY) sur les 4 endpoints ; challenges multiples (5 derniers) acceptés ; `.env.example` ; tests `tests/test_webauthn.py` (+8) | Logs : origin `http://localhost:2020` rejetée + challenge mismatch au retry. Vérifié navigateur (authentificateur virtuel CDP) : register 200 + clé listée, clé de test retirée (admin de nouveau TOTP seul) ; pytest 1249 passed, ruff/mypy 0 |
| *BUG-071* | Configuration « Configurations » inutilisable en mode mobile : sommaire masqué sans bouton d'accès, navigation par ancre sans JS, grilles 2 colonnes et rangées d'ajout qui débordent (≤768px) | 🟢 corrigé | P1 | 📱 frontend | IA | `frontend/index.html`, `frontend/js/config.js`, `frontend/js/i18n.js`, `frontend/style.css`, `frontend/locales/{fr,en}.json` | Mobile (≤768px) : ouvrir Configurations → aucun sommaire ni moyen d'atteindre une section ; champs « Clés IA » / jetons / webhooks débordent | `index.html` (+`#config-hamburger` `.help-hamburger`, `config.toc_toggle` FR/EN) ; `config.js` (toggle, scroll doux + actif + repli auto mobile, reset à l'ouverture) ; `i18n.js` (`data-i18n-attr` multi-paires `;`) ; `style.css` (bloc mobile `#config-modal` : sommaire haut 46vh, grilles 1fr, add-rows wrap + `!important`, items wrap, 44px) ; tests `tests/frontend/config-mobile.test.mjs` (nouveau, 11) + CI ; E2E `tests/e2e/config-mobile.spec.js` (nouveau, 3/3 projet chromium-mobile, ignoré en desktop) | pytest 1249 passed / 6 skipped, ruff 0, mypy 0, validate-imports 39 modules, unit 10/10, JSDOM ai 93/93 + sidebar 6/6 + mobile 35/35 + ai-keys 7/7 |
| | | | | | | | | | | |
### TODOs techniques (améliorations / nouvelles tâches)
@@ -249,6 +253,11 @@ Avant de corriger quoi que ce soit, un agent IA doit :
| 2026-09-18 | BUG-066 | Correction | `frontend/locales/fr.json`, `frontend/locales/en.json`, `tests/frontend/unit.test.mjs`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **BUG-066** : la table des matières de la page de configuration n'affichait aucune icône pour « Fichiers cachés » et « Partages publics ». Les libellés du sommaire proviennent de clés i18n (`config.section_hidden`, `config.section_shares`) distinctes des titres de section qui, eux, portaient déjà l'icône. Alignement : 🗂️ / 📤 en FR **et** EN. Test de non-régression : `unit.test.mjs` vérifie que **toutes** les entrées `.help-nav-link` du sommaire portent une icône dans les deux langues (17/17). Vérifié : `unit.test.mjs` 10/10, `validate-imports` 38 modules. | 🟢 corrigé (en attente vérif utilisateur) |
| 2026-09-18 | #105, BUG-067 | Documentation + correction | `frontend/index.html`, `frontend/js/config.js`, `frontend/style.css`, `frontend/locales/{fr,en}.json`, `backend/guide_export.py`, `backend/main.py`, `tests/test_guide.py`, `docs/features/guide-coverage-105.md`, `CHANGELOG.md`, `docs/ROADMAP.md`, `docs/ISSUES_TODOLIST.md` | **#105** : audit complet de couverture du Guide d'utilisation — 8 nouvelles sections (Architecture + diagramme Mermaid, API & intégrations, Diagrammes Mermaid & Excalidraw, Hors-ligne & synchronisation, Collaboration temps réel, Application desktop, Bibliothèque & signets, Multilingue) et compléments (recherche sémantique, MFA/WebAuthn, notifications push, exports HTML/ePub/ZIP, PDF, vue multi-panneaux, admin). Téléchargement du guide en Markdown et PDF (`GET /api/guide/download?format=md|pdf`, FR/EN, rendu par le moteur d'export existant). Guide plus large en desktop. **BUG-067** : ancre morte `#help-mobile-editor` → section dédiée créée. | 🟢 corrigé (en attente vérif utilisateur)
| 2026-09-18 | #105 (ajustements) | Amélioration | `frontend/index.html`, `frontend/js/config.js`, `frontend/sw.js`, `frontend/locales/{fr,en}.json`, `backend/guide_export.py`, `backend/pdf_export.py`, `Dockerfile`, `scripts/build_guide_diagrams.py`, `scripts/render_guide_diagram.mjs`, `scripts/guide_content.py`, `backend/assets/guide_diagrams/df7366a40db6a5a2.png`, `tests/test_guide.py`, `docs/features/guide-coverage-105.md`, `CHANGELOG.md` | **#105 (retour utilisateur)** : 1) boutons de téléchargement du guide passés en icônes seules (tooltips i18n conservés) ; 2) le diagramme Mermaid de la section Architecture est désormais rendu en **vraie image** dans le PDF (pipeline de pré-rendu PNG Chromium+mermaid v11, PNG commité sous `backend/assets/guide_diagrams/<sha1>.png`, résolu par `diagram_png_for()` ; le Markdown garde le fenced mermaid) ; 3) emoji du PDF rendus **en couleur** au lieu de rectangles : `fonts-noto-color-emoji` ajouté au Dockerfile + `"Noto Color Emoji"` en fin de pile de polices PDF. Vérifié : pytest 1218 (test_guide ×13), ruff/mypy 0, validate-imports 38, unit 10/10 ; PDF live conteneur 2020 : 24 pages, 0 glyphes tofu, diagramme 3568x1174 embarqué. | 🟢 livré
| 2026-09-22 | BUG-068 | Correction | `backend/auth/router.py`, `backend/requirements.txt`, `frontend/js/auth.js`, `frontend/style.css`, `frontend/locales/{fr,en}.json`, `tests/test_mfa.py`, `tests/frontend/mfa-settings.test.mjs` (nouveau), `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **BUG-068** : section « 🔒 Sécurité du compte » finalisée. (1) Boutons hors thème : `config-btn-primary`/`config-btn-danger` n'existaient pas en CSS → définis depuis les variables du thème (+ états disabled). (2) QR invisible : l'image tierce était bloquée par la CSP (`img-src 'self' data: blob:`) et exposait le secret TOTP → QR SVG `data:` généré en local par le backend (`qr_data_url`, segno) avec repli saisie manuelle. (3) Codes de récupération perdus à la 1re activation WebAuthn → `_showRecoveryCodes(codes, targetId)` avec repli `webauthn-flow-area`. (4) Carte « Mot de passe » ajoutée (endpoint `change-password` existant, jusque-là sans UI) + échappement des libellés de clés WebAuthn. Vérifié : pytest 1241 passed / 6 skipped, ruff 0, mypy 0 (78 fichiers), `mfa-settings.test.mjs` 9/9, unit 10/10, validate-imports 39 modules. | 🟢 corrigé (en attente vérif utilisateur) |
| 2026-09-23 | BUG-069 | Correction | `frontend/js/auth.js`, `frontend/locales/{fr,en}.json`, `tests/frontend/mfa-settings.test.mjs`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **BUG-069** : login 2FA bloqué sans erreur — après user+pwd corrects, `showMfaChallenge` cherchait `.login-box` (inexistant dans `index.html`, marquage réel `#login-screen > .login-card`) et faisait un `return` silencieux : page de login figée, aucune erreur. Correctif : montage dans `.login-card` (repli `#login-screen`) + erreur visible `mfa.challenge_unavailable` (FR/EN) si le point de montage manque. **Reproduit au navigateur** (Playwright, instance Docker `obsigate-test`, compte jetable avec TOTP) : avant → challenge jamais affiché ; après → challenge affiché, code erroné → erreur, code valide (verify 200) → app. Tests : `mfa-settings.test.mjs` 11/11 (+2 ancrage DOM), unit 10/10, validate-imports 39 modules. | 🟢 corrigé (en attente vérif utilisateur) |
| 2026-09-23 | BUG-070 | Correction | `backend/auth/webauthn_mfa.py`, `backend/auth/router.py`, `.env.example`, `tests/test_webauthn.py`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **BUG-070** : activation WebAuthn rejetée en 400. (1) Défauts `localhost` sans port → `resolve_relying_party()` dérive rp_id/origines de la requête (config explicite prioritaire, forwarded sous TRUST_PROXY), appliqué aux endpoints register + login. (2) Challenge single-use → 5 derniers conservés, vérification contre le challenge de la cérémonie en cours. **Vérifié au navigateur** (authentificateur virtuel CDP, instance Docker) : register 200, clé listée, clé de test retirée. Tests : `test_webauthn.py` 19/19 (+8), suite complète 1249 passed / 6 skipped, ruff/mypy 0. | 🟢 corrigé (en attente vérif utilisateur) |
| 2026-09-23 | BUG-071 | Correction | `frontend/index.html`, `frontend/js/config.js`, `frontend/js/i18n.js`, `frontend/style.css`, `frontend/locales/{fr,en}.json`, `tests/frontend/config-mobile.test.mjs` (nouveau), `.gitea/workflows/ci.yml`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **BUG-071** : page « Configurations » inutilisable en mobile. (1) `#config-nav` masquée sous 768px sans toggle → hamburger `#config-hamburger` ajouté à l'en-tête (`.help-hamburger`, libellé `config.toc_toggle` FR/EN). (2) Ancres brutes sans JS → interception en `config.js` (scroll doux, lien actif, repli auto mobile, reset à l'ouverture). (3) Débordements 360px → bloc CSS mobile `#config-modal` (sommaire haut 46vh, grilles 1fr, add-rows wrap + largeurs inline neutralisées, items wrap, cibles 44px). `data-i18n-attr` multi-paires (`;`). Vérifié : `config-mobile.test.mjs` 11/11 (nouveau, au CI), pytest 1249 passed / 6 skipped, ruff/mypy 0, validate-imports 39 modules, unit 10/10, JSDOM ai 93/93 + ai-sidebar 6/6 + sidebar-filters 8/8 + mobile-editor 35/35 + config-ai-keys 7/7. | 🟢 corrigé (en attente vérif utilisateur) |
| 2026-09-23 | BUG-071 (complément E2E) | Test | `tests/e2e/config-mobile.spec.js` (nouveau), `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **BUG-071 (complément E2E)** : spec Playwright mobile (convention `mobile-editor.spec.js` : `test.skip` hors viewport ≤768px, donc inactive sur le projet `chromium-desktop` du CI). Vérifié en local sur l'instance de test (port 2029, auth désactivée) : hamburger → sommaire, sélection → scroll + actif + repli, 0 débordement horizontal à 393px (3/3 `chromium-mobile`, 3 ignorés en desktop) ; suite `mobile-editor.spec.js` intacte (3/3). | 🟢 corrigé (en attente vérif utilisateur) |
---
+7 -179
View File
@@ -1,182 +1,10 @@
# ObsiGate — Guide MCP (Model Context Protocol)
# Guide MCP — déplacé
> **Statut :** livré (#79 phase E + F) · **Dernière mise à jour :** 2026-09-11
> **Voir aussi :** [AI_ARCHITECTURE_GUIDE.md](./AI_ARCHITECTURE_GUIDE.md) ·
> [features/ai-tools-mcp.md](./features/ai-tools-mcp.md) · [ROADMAP.md](./ROADMAP.md)
> Ce guide a été déplacé dans le répertoire des guides utilisateur :
> **[docs/GUIDES/MCP.md](./GUIDES/MCP.md)**.
ObsiGate expose ses vaults à des **clients MCP externes** (Claude Desktop, Cursor,
tout client compatible MCP) via un serveur **Streamable HTTP** monté sur `/mcp`.
Les outils sont les **mêmes** que ceux de l'assistant in-app : la couche
`backend/tools/` est la source unique de vérité.
Le serveur MCP d'ObsiGate (`/mcp`) expose les mêmes outils que l'assistant IA à
Claude Desktop, Cursor, Cline et tout client compatible MCP. Configuration,
outils, resources/prompts, sécurité et dépannage s'y trouvent désormais.
---
## 1. Prérequis
1. Une instance ObsiGate accessible (locale ou distante).
2. Un **jeton JWT** valide (`Authorization: Bearer <token>`), obtenu via
`POST /api/auth/login` (ou une clé API). Le jeton porte les permissions par
vault de l'utilisateur — l'autorisation MCP réutilise `get_current_user`.
3. Si l'authentification est désactivée (`OBSIGATE_AUTH_ENABLED=false`), le
serveur MCP accepte un utilisateur anonyme disposant de tous les vaults.
> Le transport `stdio` n'est **pas** encore supporté ; utilisez le transport
> HTTP (un pont local type `mcp-remote` si votre client ne gère pas nativement
> le Streamable HTTP distant).
---
## 2. Endpoint & protocole
| Élément | Valeur |
|---|---|
| URL | `https://<obsigate>/mcp` |
| Transport | Streamable HTTP (`POST` JSON-RPC 2.0, `Accept: application/json, text/event-stream`) |
| Auth | `Authorization: Bearer <JWT>` |
| Protocole MCP | `2025-03-26` (négocié à l'`initialize`) |
| Réponses | JSON (`json_response=True`) |
Handshake minimal :
```bash
curl -sS https://obsigate.example/mcp \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json, text/event-stream" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{
"protocolVersion":"2025-03-26","capabilities":{},
"clientInfo":{"name":"curl","version":"1.0"}}}'
```
La réponse contient l'en-tête `Mcp-Session-Id` à réutiliser pour les appels
suivants (`tools/list`, `tools/call`, `resources/read`, …).
---
## 3. Configuration des clients
### Claude Desktop (via pont `mcp-remote`)
```json
{
"mcpServers": {
"obsigate": {
"command": "npx",
"args": [
"-y", "mcp-remote",
"https://obsigate.example/mcp",
"--header", "Authorization: Bearer ${OBSIGATE_TOKEN}"
],
"env": { "OBSIGATE_TOKEN": "eyJ..." }
}
}
}
```
### Cursor
`.cursor/mcp.json` :
```json
{
"mcpServers": {
"obsigate": {
"url": "https://obsigate.example/mcp",
"headers": { "Authorization": "Bearer eyJ..." }
}
}
}
```
---
## 4. Primitives exposées
### 4.1 Tools
Les outils de **lecture/recherche** sont exposés directement. Les outils
**d'écriture/destructifs** sont exposés via une paire **two-step** :
`propose_<tool>` (aperçu + jeton de confirmation, aucune modification) puis
`apply_<tool>` (consomme le jeton et exécute).
| Catégorie | Outils |
|---|---|
| Vaults / navigation | `list_vaults`, `list_directory`, `list_all_files` |
| Lecture | `read_file`, `read_file_raw`, `get_backlinks`, `list_backups`, `diff_backup`, `get_graph` |
| Recherche | `search_fulltext`, `search_advanced`, `search_paths`, `list_tags`, `suggest_tags`, `list_recent` |
| Écriture (propose/apply) | `create_file`, `create_directory`, `edit_file`, `append_to_file`, `restore_backup` |
| Destructif (propose/apply) | `rename_file`, `rename_directory`, `move_path`, `replace_in_files`, `delete_file`, `delete_directory` |
Flux d'une mutation :
```text
1. tools/call { name: "propose_edit_file",
arguments: { vault, path, content } }
→ { tool, arguments, diff, confirmation_token, expires_in }
2. (l'utilisateur / l'agent valide)
3. tools/call { name: "apply_edit_file",
arguments: { confirmation_token } }
→ { ok: true, data: { ... } }
```
Le jeton est **signé (JWT), à usage unique et à durée de vie limitée**
(`OBSIGATE_MCP_CONFIRMATION_TTL`, défaut 300 s). Un rejeu renvoie
`token_reused`.
### 4.2 Resources
| URI | Contenu |
|---|---|
| `vault://<name>` | Vault accessible (métadonnées, nombre de fichiers) |
| `vault://<name>/<path>` | Contenu d'un fichier (lecture seule, **secrets redactés**) |
### 4.3 Prompts
`summarize-directory`, `generate-note`, `find-related`.
---
## 5. Sécurité
- **Permissions par vault** : `check_vault_access` est appliqué à chaque outil
et chaque resource ; un utilisateur ne voit que ses vaults.
- **Anti path-traversal** : `resolve_safe_path` rejette tout chemin hors du vault.
- **Confirmation two-step** pour toute mutation (jeton signé, usage unique).
- **Toggle par vault** `aiDestructiveTools` (défaut : activé) : le désactiver
bloque rename/move/replace/delete tout en laissant create/edit/append.
- **Backup automatique** avant chaque opération destructive.
- **Rate limiting** : par jeton et par outil
(`OBSIGATE_TOOL_RATE_LIMIT`, `OBSIGATE_TOOL_RATE_LIMIT_PER_TOOL`,
`OBSIGATE_TOOL_RATE_WINDOW`). Une limite dépassée renvoie le code
`rate_limited`.
- **Redaction des secrets** : les résultats d'outils (lectures, diffs,
extraits de recherche) sont nettoyés avant tout retour au client.
- **Audit** : chaque appel est journalisé (`data/audit.log`, action
`ai_tool_call`) avec arguments sensibles résumés.
### Variables d'environnement
| Variable | Défaut | Rôle |
|---|---|---|
| `OBSIGATE_MCP_CONFIRMATION_TTL` | `300` | Durée de vie (s) des jetons de confirmation |
| `OBSIGATE_TOOL_RATE_LIMIT` | `60` | Appels d'outils max par identité et par fenêtre |
| `OBSIGATE_TOOL_RATE_LIMIT_PER_TOOL` | = global | Appels max par outil et par fenêtre |
| `OBSIGATE_TOOL_RATE_WINDOW` | `60` | Longueur de la fenêtre (s) |
| `BOOKSLM_MAX_TOOL_CALLS` | `25` | Quota d'appels d'outils par run d'agent |
| `BOOKSLM_MAX_TOOL_READ_BYTES` | `200000` | Taille max renvoyée par `read_file` |
---
## 6. Dépannage
| Symptôme | Cause probable / remède |
|---|---|
| `401 Authentification requise` | En-tête `Authorization: Bearer` absent ou jeton expiré |
| `vault_access_denied` | Le jeton n'a pas accès à ce vault (`vaults` / `_token_vaults`) |
| `destructive_tools_disabled` | `aiDestructiveTools=false` pour ce vault |
| `confirmation_required` | Appeler d'abord `propose_<tool>` puis `apply_<tool>` |
| `token_reused` / `invalid_confirmation` | Jeton déjà consommé ou expiré → refaire un `propose_` |
| `rate_limited` | Quota dépassé ; respecter `retry_after` |
| Le client ne se connecte pas | Vérifier le transport Streamable HTTP / le pont `mcp-remote` |
Sommaire des guides : [docs/GUIDES/README.md](./GUIDES/README.md).
+1 -1
View File
@@ -1,6 +1,6 @@
# ObsiGate — Roadmap
> **Version :** 2.16.0 | **Dernière mise à jour :** 2026-09-22
> **Version :** 2.16.6 | **Dernière mise à jour :** 2026-09-22
> **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact
> vers les fonctionnalités livrées.
> - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)**
+1 -1
View File
@@ -67,7 +67,7 @@
`call_tool` (couvre les diffs, extraits de recherche et lectures non pré-redactées).
- [x] **F3.** Documentation OpenAPI + guide MCP — `backend/openapi_docs.py` : tag `MCP`,
règle `/mcp`, injection du path `/mcp` (Streamable HTTP, JSON-RPC) dans le schéma ;
nouveau [`docs/MCP_GUIDE.md`](../MCP_GUIDE.md) (endpoint, auth, config Claude Desktop /
nouveau [`docs/GUIDES/MCP.md`](../GUIDES/MCP.md) (endpoint, auth, config Claude Desktop /
Cursor, tools/resources/prompts, sécurité, variables, dépannage).
- [x] **F4.** Tests E2E de bout en bout — `tests/test_ai_e2e.py` : agent in-app
read→confirmation→write, quota d'outils, rate limiting, redaction, et flux MCP complet
Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

+12
View File
@@ -1486,6 +1486,18 @@
<div class="editor-modal" id="config-modal">
<div class="editor-container">
<div class="editor-header">
<button
class="help-hamburger"
id="config-hamburger"
data-i18n-attr="title:config.toc_toggle;aria-label:config.toc_toggle"
title="Afficher le sommaire"
aria-label="Afficher le sommaire"
>
<i
data-lucide="menu"
style="width: 18px; height: 18px"
></i>
</button>
<div class="editor-title" data-i18n="header.menu_config">Configurations</div>
<div class="editor-actions">
<button
+123 -15
View File
@@ -1,6 +1,6 @@
/* ObsiGate — Authentication: API helper, AuthManager, login form, AdminPanel */
import { state } from './state.js';
import { safeCreateIcons } from './utils.js';
import { safeCreateIcons, escapeHtml } from './utils.js';
import { showToast, closeHeaderMenu } from './ui.js';
import { t, getLocale, setLocale } from './i18n.js';
import { showWelcome } from './viewer.js';
@@ -266,6 +266,13 @@ const AuthManager = {
});
},
async changePassword(currentPassword, newPassword) {
return await api("/api/auth/change-password", {
method: "POST",
body: JSON.stringify({ current_password: currentPassword, new_password: newPassword }),
});
},
async logout() {
try {
const token = this.getToken();
@@ -545,8 +552,22 @@ function _startWebauthnLogin(mfaSection, username, rememberMe) {
function showMfaChallenge(username, rememberMe, loginBtn, loginErrorEl, mfaMethod) {
const loginBox = document.querySelector(".login-box");
if (!loginBox) return;
// BUG-069: the challenge used to mount into `.login-box`, which does not
// exist in index.html (the login markup is `#login-screen > .login-card >
// #login-form`) — querySelector returned null and the function silently
// returned, leaving the user stuck on the login page with no error after
// entering correct credentials. Mount into the real card, and never fail
// silently: surface the problem in the login error box instead.
const loginBox = document.querySelector(".login-card")
|| document.getElementById("login-screen");
if (!loginBox) {
const fallback = loginErrorEl || document.getElementById("login-error");
if (fallback) {
fallback.textContent = t("mfa.challenge_unavailable");
fallback.classList.remove("hidden");
}
return;
}
// Hide the normal login form
const loginForm = document.getElementById("login-form");
@@ -1058,11 +1079,79 @@ async function initMfaSettings() {
});
}
// Password change (BUG-068: the "Sécurité du compte" section had no way to
// change the password although POST /api/auth/change-password exists).
_renderPasswordSection(area);
// WebAuthn security keys section (ROADMAP #64)
_renderWebauthnSection(area);
}
function _renderPasswordSection(container) {
if (!container || document.getElementById("password-settings")) return;
const section = document.createElement("div");
section.id = "password-settings";
section.className = "password-settings";
section.innerHTML = `
<h4 class="webauthn-title">${t("mfa.password_change_title")}</h4>
<p class="mfa-info-text">${t("mfa.password_change_desc")}</p>
<div class="form-group">
<label>${t("mfa.current_password_label")}</label>
<input type="password" id="pwd-current" class="config-input"
placeholder="${t('mfa.current_password_placeholder')}" autocomplete="current-password">
</div>
<div class="form-group">
<label>${t("mfa.new_password_label")}</label>
<input type="password" id="pwd-new" class="config-input"
placeholder="${t('mfa.new_password_placeholder')}" autocomplete="new-password">
</div>
<div class="form-group">
<label>${t("mfa.new_password_confirm_label")}</label>
<input type="password" id="pwd-confirm" class="config-input"
placeholder="${t('mfa.new_password_confirm_placeholder')}" autocomplete="new-password">
</div>
<div class="mfa-recovery-actions">
<button class="config-btn-primary" id="pwd-change-btn">${t("mfa.password_change_btn")}</button>
</div>
<p class="mfa-error hidden" id="pwd-change-error"></p>
`;
container.appendChild(section);
section.querySelector("#pwd-change-btn").addEventListener("click", async () => {
const errEl = section.querySelector("#pwd-change-error");
const current = section.querySelector("#pwd-current").value;
const next = section.querySelector("#pwd-new").value;
const confirm = section.querySelector("#pwd-confirm").value;
const btn = section.querySelector("#pwd-change-btn");
errEl.classList.add("hidden");
if (!current || !next || !confirm) {
errEl.textContent = t("mfa.fill_all_fields");
errEl.classList.remove("hidden");
return;
}
if (next !== confirm) {
errEl.textContent = t("mfa.password_mismatch");
errEl.classList.remove("hidden");
return;
}
btn.disabled = true;
try {
await AuthManager.changePassword(current, next);
showToast(t("mfa.password_changed"), "success");
section.querySelector("#pwd-current").value = "";
section.querySelector("#pwd-new").value = "";
section.querySelector("#pwd-confirm").value = "";
} catch (err) {
errEl.textContent = err.message || String(err);
errEl.classList.remove("hidden");
} finally {
btn.disabled = false;
}
});
}
async function _renderWebauthnSection(container) {
if (!container || !window.PublicKeyCredential) return;
@@ -1085,10 +1174,10 @@ async function _renderWebauthnSection(container) {
const listHtml = keys.length
? `<ul class="webauthn-key-list">${keys.map((k) => `
<li class="webauthn-key-item">
<span class="webauthn-key-label">🔑 ${k.label || "Security key"}</span>
<span class="webauthn-key-meta">${(k.transports || []).join(", ") || "—"}</span>
<span class="webauthn-key-label">🔑 ${escapeHtml(k.label || "Security key")}</span>
<span class="webauthn-key-meta">${escapeHtml((k.transports || []).join(", ") || "—")}</span>
<button class="config-btn-secondary config-btn-sm webauthn-key-remove"
data-id="${k.credential_id}">${t("mfa.webauthn_remove")}</button>
data-id="${escapeHtml(k.credential_id)}">${t("mfa.webauthn_remove")}</button>
</li>`).join("")}</ul>`
: `<p class="mfa-info-text">${t("mfa.webauthn_none")}</p>`;
@@ -1111,7 +1200,10 @@ async function _renderWebauthnSection(container) {
const label = prompt(t("mfa.webauthn_label_prompt"), "Ma clé");
const result = await AuthManager.webauthnRegister(credential, label || "Security key");
if (result.recovery_codes && result.recovery_codes.length) {
_showRecoveryCodes(result.recovery_codes);
// BUG-068: first-time WebAuthn enable issues recovery codes. There is
// no #mfa-setup-flow-area in the "already enabled" view, so render
// them into the WebAuthn flow area instead of losing them.
_showRecoveryCodes(result.recovery_codes, "webauthn-flow-area");
} else {
showToast(t("mfa.webauthn_added"), "success");
}
@@ -1145,16 +1237,28 @@ async function _startMfaSetup() {
try {
const data = await AuthManager.mfaSetup();
// BUG-068: the QR code comes from the backend as a local SVG data: URI
// (see POST /api/auth/mfa/totp/setup → qr_data_url). The previous
// third-party QR image was blocked by the CSP
// (img-src 'self' data: blob:) so it never displayed — and it leaked the
// otpauth URI (TOTP secret) to a third party. Fall back to the manual
// secret when the backend has no QR generator available.
const qrImg = data.qr_data_url
? `<img id="mfa-qr-img" alt="QR Code" class="mfa-qr-code-img"
src="${data.qr_data_url}"
onerror="this.style.display='none';document.getElementById('mfa-qr-fallback').style.display='block';">`
: "";
const fallbackStyle = data.qr_data_url ? "display:none" : "";
flowArea.innerHTML = `
<div class="mfa-setup-card">
<h4>${t("mfa.scan_qr")}</h4>
<div class="mfa-qr-container">
<img id="mfa-qr-img" alt="QR Code" class="mfa-qr-code"
src="https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(data.otpauth_uri)}">
${qrImg}
<p class="mfa-info-text" id="mfa-qr-fallback" style="${fallbackStyle}">${t("mfa.qr_unavailable")}</p>
</div>
<details class="mfa-secret-details">
<details class="mfa-secret-details" ${data.qr_data_url ? "" : "open"}>
<summary>${t("mfa.manual_entry")}</summary>
<code class="mfa-secret-code">${data.secret}</code>
<code class="mfa-secret-code">${escapeHtml(data.secret)}</code>
</details>
<div class="mfa-verify-section">
<label>${t("mfa.enter_code")}</label>
@@ -1201,12 +1305,16 @@ async function _startMfaSetup() {
}
function _showRecoveryCodes(codes) {
const flowArea = document.getElementById("mfa-setup-flow-area");
const area = document.getElementById("mfa-setup-area");
function _showRecoveryCodes(codes, targetId) {
// BUG-068: the recovery codes must be visible wherever the enable flow ran.
// The TOTP flow owns #mfa-setup-flow-area, but the WebAuthn first-enable
// path (#webauthn-flow-area) has none — previously those codes were lost.
const flowArea = document.getElementById(targetId || "mfa-setup-flow-area")
|| document.getElementById("webauthn-flow-area")
|| document.getElementById("mfa-setup-area");
if (!flowArea) return;
const codesHtml = codes.map(c => `<code class="mfa-recovery-code">${c}</code>`).join("\n");
const codesHtml = codes.map(c => `<code class="mfa-recovery-code">${escapeHtml(c)}</code>`).join("\n");
flowArea.innerHTML = `
<div class="mfa-recovery-card">
<h4>🔑 ${t("mfa.recovery_codes_title")}</h4>
+42
View File
@@ -767,6 +767,10 @@ function initConfigModal() {
openBtn.addEventListener("click", async () => {
modal.classList.add("active");
closeHeaderMenu();
// BUG-071: reset the TOC to the CSS default (mobile: hidden, desktop:
// visible) like the help modal does on open.
var configNavOnOpen = document.getElementById("config-nav");
if (configNavOnOpen) configNavOnOpen.style.display = '';
renderConfigFilters();
loadConfigFields();
loadDiagnostics();
@@ -886,6 +890,44 @@ function initConfigModal() {
});
}
// BUG-071: mobile table of contents. #config-nav shares the .help-nav
// rule that hides it below 768px, but — unlike the help modal — the config
// modal had no toggle to reveal it, leaving mobile users with no way to
// reach a section. The header hamburger opens it as a top block; picking
// a section smooth-scrolls inside the modal and collapses it on mobile.
var configNav = document.getElementById("config-nav");
var configHamburger = document.getElementById("config-hamburger");
function _isConfigMobile() { return window.innerWidth <= 768; }
function _setConfigNav(open) {
if (!configNav) return;
configNav.style.display = open ? "flex" : "none";
if (configHamburger) configHamburger.classList.toggle("active", !!open);
}
if (configHamburger) {
configHamburger.addEventListener("click", function(e) {
e.stopPropagation();
var hidden = !configNav || configNav.style.display === "none" || configNav.style.display === "";
_setConfigNav(hidden);
});
}
if (configNav) {
configNav.querySelectorAll(".help-nav-link").forEach(function(a) {
a.addEventListener("click", function(e) {
var hash = a.getAttribute("href");
if (!hash || hash.charAt(0) !== "#") return;
var target = document.getElementById(hash.slice(1));
if (!target) return;
e.preventDefault();
configNav.querySelectorAll(".help-nav-link").forEach(function(o) { o.classList.remove("active"); });
a.classList.add("active");
if (typeof target.scrollIntoView === "function") {
target.scrollIntoView({ behavior: "smooth", block: "start" });
}
if (_isConfigMobile()) _setConfigNav(false);
});
});
}
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && modal.classList.contains("active")) {
closeConfigModal();
+13 -6
View File
@@ -5,6 +5,7 @@
* Static DOM: data-i18n="key" → textContent
* data-i18n-placeholder="key" → placeholder
* data-i18n-attr:title="key" → title attribute
* data-i18n-attr="a:k1;b:k2" → several attributes (";"-separated)
* data-i18n-html="key" → innerHTML (use sparingly)
* Dynamic JS: import { t } from './i18n.js'; t('key', {param: 'val'})
* Live reload: setLocale('en') updates every data-i18n element instantly.
@@ -183,13 +184,19 @@ function _applyDOM() {
el.innerHTML = t(key);
});
// data-i18n-attr:TITLE → sets any attribute
// data-i18n-attr:ATTR:key[;ATTR:key…] → sets any attribute(s).
// Single-pair form (data-i18n-attr="title:key") is preserved; multiple
// pairs are separated with ";" (BUG-071: the config TOC toggle needs both
// title and aria-label translated).
document.querySelectorAll('[data-i18n-attr]').forEach(function (el) {
const raw = el.getAttribute('data-i18n-attr');
const colon = raw.indexOf(':');
if (colon === -1) return;
const attr = raw.substring(0, colon);
const key = raw.substring(colon + 1);
el.setAttribute(attr, t(key));
raw.split(';').forEach(function (pair) {
const colon = pair.indexOf(':');
if (colon === -1) return;
const attr = pair.substring(0, colon).trim();
const key = pair.substring(colon + 1).trim();
if (!attr || !key) return;
el.setAttribute(attr, t(key));
});
});
}
+14
View File
@@ -569,6 +569,7 @@
"config.test": "Test",
"config.timeout_label": "Search timeout (ms)",
"config.title": "Settings",
"config.toc_toggle": "Show contents",
"config.title_boost": "Title boost",
"config.title_boost_hint": "Relevance multiplier for title matches",
"config.title_boost_label": "Title boost",
@@ -1887,6 +1888,19 @@
"mfa.disable_confirm_btn": "Disable 2FA",
"mfa.disabled_success": "2FA has been disabled.",
"mfa.fill_all_fields": "Please fill in all fields.",
"mfa.qr_unavailable": "QR code unavailable — use manual entry below.",
"mfa.password_change_title": "Password",
"mfa.password_change_desc": "Change your account password (min. 8 characters). All other sessions are invalidated.",
"mfa.current_password_label": "Current password",
"mfa.current_password_placeholder": "Your current password",
"mfa.new_password_label": "New password",
"mfa.new_password_placeholder": "Min. 8 characters",
"mfa.new_password_confirm_label": "Confirm new password",
"mfa.new_password_confirm_placeholder": "Repeat the new password",
"mfa.password_change_btn": "Change password",
"mfa.password_mismatch": "The two passwords do not match.",
"mfa.password_changed": "Password updated.",
"mfa.challenge_unavailable": "Verification screen unavailable — please reload the page.",
"bookslm.title": "BooksLM",
"bookslm.files_indexed": "{count} files indexed",
"bookslm.chars_loaded": "{chars} chars loaded",
+14
View File
@@ -569,6 +569,7 @@
"config.test": "Tester",
"config.timeout_label": "Timeout recherche (ms)",
"config.title": "Configuration",
"config.toc_toggle": "Afficher le sommaire",
"config.title_boost": "Boost titre",
"config.title_boost_hint": "Multiplicateur de pertinence pour les correspondances dans le titre",
"config.title_boost_label": "Boost titre",
@@ -1887,6 +1888,19 @@
"mfa.disable_confirm_btn": "Désactiver la 2FA",
"mfa.disabled_success": "La 2FA a été désactivée.",
"mfa.fill_all_fields": "Veuillez remplir tous les champs.",
"mfa.qr_unavailable": "QR code indisponible — utilisez la saisie manuelle ci-dessous.",
"mfa.password_change_title": "Mot de passe",
"mfa.password_change_desc": "Modifiez le mot de passe de votre compte (min. 8 caractères). Toutes les autres sessions sont invalidées.",
"mfa.current_password_label": "Mot de passe actuel",
"mfa.current_password_placeholder": "Votre mot de passe actuel",
"mfa.new_password_label": "Nouveau mot de passe",
"mfa.new_password_placeholder": "Min. 8 caractères",
"mfa.new_password_confirm_label": "Confirmer le nouveau mot de passe",
"mfa.new_password_confirm_placeholder": "Répétez le nouveau mot de passe",
"mfa.password_change_btn": "Changer le mot de passe",
"mfa.password_mismatch": "Les deux mots de passe ne correspondent pas.",
"mfa.password_changed": "Mot de passe mis à jour.",
"mfa.challenge_unavailable": "Écran de vérification indisponible — veuillez recharger la page.",
"bookslm.title": "BooksLM",
"bookslm.files_indexed": "{count} fichiers indexés",
"bookslm.chars_loaded": "{chars} caractères chargés",
+146
View File
@@ -4347,6 +4347,76 @@ body.resizing-v {
background: var(--bg-hover);
}
/* BUG-068: .config-btn-primary / .config-btn-danger were used by the account
security section (frontend/js/auth.js) but never defined — the buttons fell
back to the browser default and ignored the theme. Defined here with the
same conventions as .config-btn-save / .config-btn-secondary. */
.config-btn-primary {
padding: 8px 16px;
border: 1px solid var(--accent);
border-radius: 6px;
background: var(--accent);
color: #fff;
font-family: "JetBrains Mono", monospace;
font-size: 0.8rem;
font-weight: 600;
cursor: pointer;
transition: opacity 150ms;
}
.config-btn-primary:hover {
opacity: 0.9;
}
.config-btn-primary:disabled {
opacity: 0.55;
cursor: not-allowed;
}
.config-btn-danger {
padding: 8px 16px;
border: 1px solid var(--danger, #e74c3c);
border-radius: 6px;
background: var(--danger-bg, #3d1a18);
color: var(--danger, #ff7b72);
font-family: "JetBrains Mono", monospace;
font-size: 0.8rem;
font-weight: 600;
cursor: pointer;
transition: opacity 150ms;
}
.config-btn-danger:hover {
opacity: 0.9;
}
.config-btn-danger:disabled {
opacity: 0.55;
cursor: not-allowed;
}
/* BUG-068: local QR code (backend SVG data: URI) + password section share the
security-tab card conventions. */
.mfa-qr-code-img {
max-width: 200px;
border-radius: 8px;
background: #fff;
padding: 8px;
}
.password-settings {
margin-top: 20px;
padding-top: 16px;
border-top: 1px solid var(--border, #333);
}
.password-settings .form-group {
margin: 8px 0;
}
.password-settings .form-group label {
display: block;
font-size: 0.78rem;
color: var(--text-secondary, #aaa);
margin-bottom: 4px;
}
.password-settings .config-input {
width: 100%;
max-width: 320px;
}
/* --- AI keys section: accordion redesign (#104) --- */
.ai-keys-header {
display: flex;
@@ -4653,6 +4723,82 @@ body.resizing-v {
}
}
/* BUG-071: Configurations modal — mobile usability (viewport ≤ 768px).
#config-nav shares the .help-nav rule that hides it, but the config modal
had no toggle (unlike the help modal): the header hamburger
(#config-hamburger, same .help-hamburger treatment) reveals it as a
collapsible top block. Two-column grids and fixed-width add-rows are
stacked/wrapped so nothing overflows a 360px viewport. */
@media (max-width: 768px) {
/* TOC as a collapsible top block (JS toggles inline display flex/none,
which wins over the hiding rule); the list scrolls within a capped nav. */
#config-modal #config-nav {
width: 100%;
min-width: 0;
max-width: 100%;
border-right: none;
border-bottom: 1px solid var(--border);
max-height: 46vh;
}
/* Two-column grids → single column. */
#config-modal .ai-default-grid,
#config-modal .ai-provider-fields {
grid-template-columns: 1fr;
}
/* Add-rows (tokens, webhooks, tag filters) wrap instead of overflowing. */
#config-modal .config-add-row,
#config-modal .config-add-pattern {
flex-wrap: wrap;
}
#config-modal .config-add-row .config-input,
#config-modal .config-add-pattern .config-input,
#config-modal .config-add-row .config-select {
flex: 1 1 140px;
width: auto !important; /* override fixed inline widths (180/140/100px) */
min-width: 0;
}
#config-modal .config-add-row .config-btn-add,
#config-modal .config-add-pattern .config-btn-add {
flex: 1 1 auto;
min-height: 44px;
}
/* Webhook / token / share rows wrap; long URLs and meta take their own
line instead of squeezing the delete control off-screen. */
#config-modal .webhook-item,
#config-modal .token-item,
#config-modal .share-item {
flex-wrap: wrap;
}
#config-modal .webhook-url,
#config-modal .token-meta,
#config-modal .share-url {
flex: 1 1 100%;
min-width: 0;
white-space: normal;
overflow-wrap: anywhere;
}
#config-modal .webhook-delete,
#config-modal .token-delete,
#config-modal .share-revoke {
min-width: 44px;
min-height: 44px;
}
/* Sticky AI-keys footer: full-width touch-friendly buttons. */
#config-modal .ai-keys-footer .config-btn-save,
#config-modal .ai-keys-footer .config-btn-secondary {
flex: 1 1 100%;
min-height: 44px;
}
/* Long inline code in tips (ex. MCP usage snippet) must wrap. */
#config-modal .qh-tip {
flex-wrap: wrap;
}
#config-modal .qh-tip span {
min-width: 0;
overflow-wrap: anywhere;
}
}
/* --- Toast notifications --- */
.toast-container {
position: fixed;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "obsigate",
"version": "2.16.0",
"version": "2.16.6",
"description": "**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.",
"main": "patch.js",
"directories": {
+94
View File
@@ -0,0 +1,94 @@
/**
* E2E tests for the Configurations modal on mobile (BUG-071).
*
* Runs only under the `chromium-mobile` Playwright project (viewport ≤ 768px);
* skipped on the desktop project that the CI job executes — same convention
* as mobile-editor.spec.js.
*
* Covered:
* - the TOC hamburger (#config-hamburger) is visible and reveals #config-nav,
* which is hidden by default on mobile;
* - picking a TOC entry scrolls to the section, marks the link active and
* collapses the nav;
* - the modal content does not overflow horizontally at 393px.
*
* Run:
* npx playwright test tests/e2e/config-mobile.spec.js --project=chromium-mobile
* (ObsiGate listening on http://localhost:2029, auth disabled)
*/
import { test, expect } from '@playwright/test';
const MOBILE_MAX_WIDTH = 768;
async function boot(page) {
await page.goto('/');
await page.waitForSelector('#app:not(.hidden)', { timeout: 15000 });
await expect(page.locator('#header-menu-btn')).toBeVisible({ timeout: 15000 });
}
async function openConfigModal(page) {
await page.locator('#header-menu-btn').click();
await page.locator('#config-open-btn').click();
await expect(page.locator('#config-modal.active')).toBeVisible();
}
test.describe('Configurations modal on mobile (BUG-071)', () => {
test('hamburger reveals the table of contents', async ({ page, viewport }) => {
test.skip((viewport?.width ?? 0) > MOBILE_MAX_WIDTH, 'Mobile viewport required');
await boot(page);
await openConfigModal(page);
// TOC hidden by default on mobile, hamburger visible.
await expect(page.locator('#config-hamburger')).toBeVisible();
await expect(page.locator('#config-nav')).toBeHidden();
await page.locator('#config-hamburger').click();
await expect(page.locator('#config-nav')).toBeVisible();
});
test('picking a section scrolls to it and collapses the nav', async ({ page, viewport }) => {
test.skip((viewport?.width ?? 0) > MOBILE_MAX_WIDTH, 'Mobile viewport required');
await boot(page);
await openConfigModal(page);
await page.locator('#config-hamburger').click();
const link = page.locator('#config-nav a[href="#cfg-tokens"]');
await expect(link).toBeVisible();
await link.click();
// Nav collapses on mobile after selection…
await expect(page.locator('#config-nav')).toBeHidden();
// …the link is marked active…
await expect(link).toHaveClass(/active/);
// …and the section scrolls into view inside the modal (smooth scroll:
// poll for the settled position instead of racing the animation).
await expect
.poll(
async () => {
const box = await page.locator('#cfg-tokens').boundingBox();
const modalBox = await page.locator('#config-modal').boundingBox();
if (!box || !modalBox) return Number.POSITIVE_INFINITY;
return box.y - (modalBox.y + modalBox.height);
},
{ timeout: 8000 },
)
.toBeLessThanOrEqual(0);
});
test('no horizontal overflow at 393px', async ({ page, viewport }) => {
test.skip((viewport?.width ?? 0) > MOBILE_MAX_WIDTH, 'Mobile viewport required');
await boot(page);
await openConfigModal(page);
for (const section of ['#cfg-ai', '#cfg-tokens', '#cfg-webhooks', '#cfg-partages-publics']) {
await page.locator('#config-hamburger').click();
await page.locator(`#config-nav a[href="${section}"]`).click();
}
const overflow = await page.evaluate(() => {
const scroller = document.getElementById('config-scroll');
return scroller.scrollWidth - scroller.clientWidth;
});
expect(overflow).toBeLessThanOrEqual(1);
});
});
+131
View File
@@ -0,0 +1,131 @@
#!/usr/bin/env node
/**
* ObsiGate — Configurations modal mobile usability non-regression tests (BUG-071).
*
* Static checks (no jsdom needed — runs in the "Frontend unit tests" CI step):
* - BUG-071a: #config-nav shared the .help-nav rule that hides it below
* 768px, but the config modal had no toggle (the help modal has
* #help-hamburger) → the table of contents was unreachable on mobile.
* The header must carry #config-hamburger (same .help-hamburger
* treatment: hidden on desktop, visible on mobile) wired in config.js.
* - BUG-071b: the TOC links were bare anchors with no JS — no active state,
* no auto-collapse on mobile, unreliable scrolling inside the modal.
* config.js must smooth-scroll to the section, mark it active and collapse
* the nav on mobile, and reset the nav on open.
* - BUG-071c: two-column grids (.ai-default-grid, .ai-provider-fields),
* fixed-width add-rows (.config-add-row, 180/140/100px inline widths) and
* single-line webhook/token/share items overflowed a 360px viewport.
* style.css must stack/wrap them below 768px with 44px touch targets.
* - BUG-071d: every #config-nav link target must exist (dead-anchor guard,
* same class of bug as BUG-067 for the help modal).
* - BUG-071e: data-i18n-attr supports several "attr:key" pairs (";"-
* separated) so the toggle carries translated title AND aria-label.
*
* Usage: node tests/frontend/config-mobile.test.mjs
*/
import { strict as assert } from "node:assert";
import { readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.join(__dirname, "..", "..");
const indexHtml = readFileSync(path.join(ROOT, "frontend", "index.html"), "utf8");
const configJs = readFileSync(path.join(ROOT, "frontend", "js", "config.js"), "utf8");
const i18nJs = readFileSync(path.join(ROOT, "frontend", "js", "i18n.js"), "utf8");
const css = readFileSync(path.join(ROOT, "frontend", "style.css"), "utf8");
const fr = JSON.parse(readFileSync(path.join(ROOT, "frontend", "locales", "fr.json"), "utf8"));
const en = JSON.parse(readFileSync(path.join(ROOT, "frontend", "locales", "en.json"), "utf8"));
function test(label, fn) {
try {
fn();
console.log(" ✓ " + label);
} catch (err) {
console.error(" ✗ " + label + "\n " + err.message);
process.exitCode = 1;
}
}
// ── BUG-071a: header TOC toggle ─────────────────────────────────────────────
test("index.html — #config-hamburger exists in the config modal header", () => {
const modal = indexHtml.match(/<div class="editor-modal" id="config-modal">([\s\S]*?)<div class="editor-body help-body" id="config-body">/);
assert.ok(modal, "#config-modal with #config-body not found");
const header = modal[1].match(/<div class="editor-header">([\s\S]*?)<\/div>\s*<\/div>/);
assert.ok(header, "config modal .editor-header not found");
assert.match(header[1], /id="config-hamburger"/, "no #config-hamburger in the config header — TOC unreachable on mobile");
assert.match(header[1], /help-hamburger/, "the toggle must reuse .help-hamburger (desktop-hidden, mobile-visible)");
assert.match(header[1], /aria-label/, "the toggle needs an accessible label");
assert.match(header[1], /config\.toc_toggle/, "the toggle label must use the i18n key config.toc_toggle");
});
test("i18n — config.toc_toggle exists in FR and EN", () => {
assert.ok(fr["config.toc_toggle"], "fr.json missing config.toc_toggle");
assert.ok(en["config.toc_toggle"], "en.json missing config.toc_toggle");
assert.notEqual(fr["config.toc_toggle"], "config.toc_toggle", "FR value must be translated");
assert.notEqual(en["config.toc_toggle"], "config.toc_toggle", "EN value must be translated");
});
// ── BUG-071b: TOC behaviour in config.js ────────────────────────────────────
test("config.js — hamburger toggles #config-nav", () => {
assert.match(configJs, /getElementById\("config-hamburger"\)/, "no binding on #config-hamburger");
assert.match(configJs, /_setConfigNav\(/, "TOC open/close helper missing");
});
test("config.js — TOC links smooth-scroll, mark active, collapse on mobile", () => {
assert.match(configJs, /#config-nav[\s\S]{0,400}?help-nav-link/, "no handler on the #config-nav links");
assert.match(configJs, /scrollIntoView/, "section scroll must use scrollIntoView inside the modal");
assert.match(configJs, /innerWidth <= 768/, "the nav must auto-collapse on mobile viewports only");
});
test("config.js — TOC display reset when the modal opens", () => {
assert.match(configJs, /configNavOnOpen[\s\S]{0,120}?style\.display = ''/, "stale inline display would stick across sessions");
});
// ── BUG-071c: mobile CSS ────────────────────────────────────────────────────
test("style.css — config TOC becomes a capped top block on mobile", () => {
assert.match(css, /#config-modal #config-nav/, "no mobile rule scoped to #config-modal #config-nav");
assert.match(css, /#config-modal #config-nav[\s\S]{0,400}?max-height/, "the opened TOC must be height-capped so content stays reachable");
});
test("style.css — two-column config grids stack on mobile", () => {
assert.match(css, /#config-modal \.ai-default-grid/, ".ai-default-grid still 2 columns on mobile");
assert.match(css, /#config-modal \.ai-provider-fields/, ".ai-provider-fields still 3fr/2fr on mobile");
assert.match(css, /grid-template-columns: 1fr;/, "mobile grids must collapse to a single column");
});
test("style.css — add-rows wrap and fixed inline widths are neutralised", () => {
assert.match(css, /#config-modal \.config-add-row/, "no mobile rule for .config-add-row (token/webhook rows overflow)");
assert.match(css, /width: auto !important/, "fixed inline widths (180/140/100px) must be overridden on mobile");
assert.match(css, /min-height: 44px/, "mobile action controls need 44px touch targets");
});
test("style.css — webhook/token/share rows wrap on mobile", () => {
for (const cls of ["webhook-item", "token-item", "share-item"]) {
assert.match(css, new RegExp("#config-modal \\." + cls), `.${cls} has no mobile wrap rule`);
}
});
// ── BUG-071d: dead-anchor guard ─────────────────────────────────────────────
test("index.html — every #config-nav link resolves to an element id", () => {
const nav = indexHtml.match(/<nav class="help-nav" id="config-nav">([\s\S]*?)<\/nav>/);
assert.ok(nav, "#config-nav not found");
const hrefs = [...nav[1].matchAll(/href="(#[^"]+)"/g)].map((m) => m[1].slice(1));
assert.ok(hrefs.length > 0, "no links in #config-nav");
const missing = hrefs.filter((id) => !indexHtml.includes(`id="${id}"`));
assert.deepEqual(missing, [], `dead TOC anchors (cf. BUG-067): ${missing.join(", ")}`);
});
// ── BUG-071e: multi-pair data-i18n-attr ─────────────────────────────────────
test("i18n.js — data-i18n-attr supports several attr:key pairs", () => {
assert.match(i18nJs, /split\(['"];/, "pairs must be split on ';'");
assert.match(i18nJs, /el\.setAttribute\(attr, t\(key\)\)/, "each pair must set its attribute");
});
if (process.exitCode) {
console.error("\nConfig mobile tests FAILED");
} else {
console.log("\nAll config mobile tests passed.");
}
+136
View File
@@ -0,0 +1,136 @@
#!/usr/bin/env node
/**
* ObsiGate — Account security section non-regression tests (BUG-068).
*
* Static checks on the "🔒 Sécurité du compte" configuration section:
* - BUG-068a: the TOTP QR code must NOT depend on the third-party
* https://api.qrserver.com service (blocked by the CSP
* `img-src 'self' data: blob:`, so the QR never displayed — and the
* otpauth URI, TOTP secret included, leaked to a third party). The setup
* endpoint returns a local SVG data: URI (qr_data_url) instead.
* - BUG-068b: .config-btn-primary / .config-btn-danger are used by
* frontend/js/auth.js but were never defined — buttons fell back to the
* browser default and ignored the theme. They must exist and derive from
* theme variables.
* - BUG-068c: recovery codes issued on first-time WebAuthn enable were lost
* (no #mfa-setup-flow-area in the "already enabled" view).
* - BUG-068d: the section had no password change although
* POST /api/auth/change-password exists.
*
* Usage: node tests/frontend/mfa-settings.test.mjs
*/
import { strict as assert } from "node:assert";
import { readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.join(__dirname, "..", "..");
const auth = readFileSync(path.join(ROOT, "frontend", "js", "auth.js"), "utf8");
const css = readFileSync(path.join(ROOT, "frontend", "style.css"), "utf8");
const router = readFileSync(path.join(ROOT, "backend", "auth", "router.py"), "utf8");
const indexHtml = readFileSync(path.join(ROOT, "frontend", "index.html"), "utf8");
const fr = JSON.parse(readFileSync(path.join(ROOT, "frontend", "locales", "fr.json"), "utf8"));
const en = JSON.parse(readFileSync(path.join(ROOT, "frontend", "locales", "en.json"), "utf8"));
function test(label, fn) {
try {
fn();
console.log(" ✓ " + label);
} catch (err) {
console.error(" ✗ " + label + "\n " + err.message);
process.exitCode = 1;
}
}
// ── QR code: local data URI, no third-party service ─────────────────────────
test("auth.js — no external QR service left (CSP blocks it, secret leaks)", () => {
assert.doesNotMatch(auth, /qrserver\.com/, "api.qrserver.com is blocked by img-src and leaks the otpauth URI");
assert.doesNotMatch(auth, /https:\/\/api\./, "no third-party https://api.* image may carry the TOTP secret");
});
test("auth.js — setup flow renders the backend qr_data_url with a fallback", () => {
assert.match(auth, /qr_data_url/, "the QR <img> must use the backend-provided qr_data_url");
assert.match(auth, /mfa-qr-fallback/, "a manual-entry fallback must show when no QR is available");
assert.match(auth, /mfa\.qr_unavailable/, "the fallback needs its i18n string");
});
test("backend — /mfa/totp/setup returns a local qr_data_url", () => {
assert.match(router, /qr_data_url/, "setup must include qr_data_url in its response");
assert.match(router, /svg_data_uri/, "the QR must be generated locally (segno SVG data URI)");
assert.match(router, /import segno/, "segno import must stay local with a graceful fallback");
});
// ── Buttons follow the theme ────────────────────────────────────────────────
for (const cls of ["config-btn-primary", "config-btn-danger"]) {
test(`style.css — .${cls} is defined from theme variables`, () => {
const rule = css.match(new RegExp(`\\.${cls}\\s*\\{([^}]*)\\}`));
assert.ok(rule, `.${cls} rule not found — buttons fall back to the browser default`);
assert.match(rule[1], /var\(--/, `.${cls} must derive from CSS theme variables, not hardcoded colors`);
});
}
test("style.css — themed buttons have disabled states", () => {
assert.match(css, /\.config-btn-primary:disabled/, ".config-btn-primary needs a disabled state");
assert.match(css, /\.config-btn-danger:disabled/, ".config-btn-danger needs a disabled state");
});
// ── Recovery codes are never lost ───────────────────────────────────────────
test("auth.js — _showRecoveryCodes falls back to the WebAuthn flow area", () => {
const fn = auth.match(/function _showRecoveryCodes\(codes(?:, targetId)?\) \{([\s\S]*?)\n\}/);
assert.ok(fn, "_showRecoveryCodes helper not found");
assert.match(fn[1], /webauthn-flow-area/, "codes issued on first WebAuthn enable must render without #mfa-setup-flow-area");
});
// ── Password change lives in the security section ───────────────────────────
test("auth.js — password change calls POST /api/auth/change-password", () => {
assert.match(auth, /\/api\/auth\/change-password/, "changePassword must hit the existing endpoint");
assert.match(auth, /_renderPasswordSection/, "the security section must render a password card");
});
test("i18n — password + QR strings exist in FR and EN", () => {
for (const key of [
"mfa.qr_unavailable",
"mfa.password_change_title",
"mfa.password_change_btn",
"mfa.password_mismatch",
"mfa.password_changed",
"mfa.challenge_unavailable",
]) {
assert.ok(fr[key], `fr.json missing ${key}`);
assert.ok(en[key], `en.json missing ${key}`);
}
});
// ── BUG-069: the MFA challenge must mount into a real DOM node ──────────────
test("auth.js — challenge mounts into .login-card (exists in index.html)", () => {
assert.doesNotMatch(
auth,
/querySelector\("\.login-box"\)/,
"showMfaChallenge queried .login-box, which never existed in index.html → silent return, login stuck with no error",
);
assert.match(
auth,
/querySelector\("\.login-card"\)/,
"the challenge must mount into the real login container",
);
assert.ok(
indexHtml.includes('class="login-card"'),
"index.html must contain the .login-card mount point",
);
});
test("auth.js — showMfaChallenge never fails silently", () => {
const fn = auth.match(/function showMfaChallenge\(username, rememberMe, loginBtn, loginErrorEl, mfaMethod\) \{([\s\S]*?)\n \/\/ WebAuthn second factor/);
assert.ok(fn, "showMfaChallenge helper not found");
assert.match(fn[1], /challenge_unavailable/, "a missing mount point must surface an error, not silently return");
assert.doesNotMatch(fn[1], /if \(!loginBox\) return;/, "bare silent return is forbidden in the challenge flow");
});
if (process.exitCode) {
console.error("\nMFA settings tests FAILED");
} else {
console.log("\nAll MFA settings tests passed.");
}
+16
View File
@@ -248,6 +248,22 @@ class TestMfaApiEndpoints:
assert "otpauth://totp/" in data["otpauth_uri"]
assert len(data["secret"]) >= 16
def test_mfa_setup_returns_local_qr_data_url(self, mfa_client):
"""BUG-068: the setup response carries a CSP-safe local QR code.
The previous client used an https://api.qrserver.com image, blocked by
the CSP (img-src 'self' data: blob:) — the QR never displayed — and
leaking the otpauth URI to a third party.
"""
token, _ = _login(mfa_client)
resp = mfa_client.post("/api/auth/mfa/totp/setup", headers=_auth_headers(token))
assert resp.status_code == 200
data = resp.json()
qr_data_url = data.get("qr_data_url")
assert qr_data_url, "setup must return a local qr_data_url"
assert qr_data_url.startswith("data:image/svg+xml"), qr_data_url[:60]
assert "qrserver.com" not in qr_data_url
def test_mfa_enable_flow(self, mfa_client):
token, _ = _login(mfa_client)
headers = _auth_headers(token)
+130 -2
View File
@@ -134,8 +134,8 @@ class TestWebauthnModule:
w._pending.clear()
w._store_challenge("u2:register")
key = "u2:register"
ch, _ = w._pending[key]
w._pending[key] = (ch, _t.time() - 1)
ch, _ = w._pending[key][0]
w._pending[key] = [(ch, _t.time() - 1)]
assert w._take_challenge(key) is None
def test_full_registration_and_authentication_roundtrip(self):
@@ -337,3 +337,131 @@ class TestWebauthnApi:
assert r2.status_code == 200
st = wa_client.get("/api/auth/mfa/status", headers=headers).json()
assert st["mfa_enabled"] is False
# ── BUG-070: relying party derived from the request ─────────────────────
#
# The old defaults (rp_id "localhost", origins ["http://localhost"]) rejected
# every real access URL: "Unexpected client data origin
# "http://localhost:2020", expected one of ['http://localhost']".
def _fake_request(host, scheme="http", forwarded_host=None, forwarded_proto=None):
from fastapi import Request
headers = [(b"host", host.encode())]
if forwarded_host is not None:
headers.append((b"x-forwarded-host", forwarded_host.encode()))
if forwarded_proto is not None:
headers.append((b"x-forwarded-proto", forwarded_proto.encode()))
return Request({
"type": "http", "method": "POST", "path": "/",
"headers": headers, "scheme": scheme,
"server": ("testserver", 80), "client": ("127.0.0.1", 5000),
})
class TestRelyingPartyResolution:
def test_defaults_without_request(self, monkeypatch):
import backend.auth.webauthn_mfa as w
monkeypatch.delenv("OBSIGATE_WEBAUTHN_RP_ID", raising=False)
monkeypatch.delenv("OBSIGATE_WEBAUTHN_ORIGINS", raising=False)
assert w.resolve_relying_party(None) == ("localhost", ["http://localhost"])
def test_derives_host_with_port(self, monkeypatch):
"""Exact BUG-070 report: http://localhost:2020 was rejected."""
import backend.auth.webauthn_mfa as w
monkeypatch.delenv("OBSIGATE_WEBAUTHN_RP_ID", raising=False)
monkeypatch.delenv("OBSIGATE_WEBAUTHN_ORIGINS", raising=False)
rp, origins = w.resolve_relying_party(_fake_request("localhost:2020"))
assert rp == "localhost"
assert origins == ["http://localhost:2020"]
def test_derives_ip_host(self, monkeypatch):
import backend.auth.webauthn_mfa as w
monkeypatch.delenv("OBSIGATE_WEBAUTHN_RP_ID", raising=False)
monkeypatch.delenv("OBSIGATE_WEBAUTHN_ORIGINS", raising=False)
rp, origins = w.resolve_relying_party(_fake_request("127.0.0.1:2020"))
assert rp == "127.0.0.1"
assert origins == ["http://127.0.0.1:2020"]
def test_explicit_env_wins_over_request(self, monkeypatch):
import backend.auth.webauthn_mfa as w
monkeypatch.setenv("OBSIGATE_WEBAUTHN_RP_ID", "obs.example.com")
monkeypatch.setenv("OBSIGATE_WEBAUTHN_ORIGINS",
"https://obs.example.com, https://www.obs.example.com")
rp, origins = w.resolve_relying_party(_fake_request("localhost:2020"))
assert rp == "obs.example.com"
assert origins == ["https://obs.example.com",
"https://www.obs.example.com"]
def test_forwarded_headers_require_trust(self, monkeypatch):
import backend.auth.webauthn_mfa as w
monkeypatch.delenv("OBSIGATE_WEBAUTHN_RP_ID", raising=False)
monkeypatch.delenv("OBSIGATE_WEBAUTHN_ORIGINS", raising=False)
monkeypatch.setenv("OBSIGATE_TRUST_PROXY", "false")
req = _fake_request("internal:8080", scheme="http",
forwarded_host="obs.example.com",
forwarded_proto="https")
assert w.resolve_relying_party(req) == ("internal", ["http://internal:8080"])
monkeypatch.setenv("OBSIGATE_TRUST_PROXY", "true")
assert w.resolve_relying_party(req) == ("obs.example.com",
["https://obs.example.com"])
def test_hostname_only(self):
import backend.auth.webauthn_mfa as w
assert w._hostname_only("example.com:2020") == "example.com"
assert w._hostname_only("example.com") == "example.com"
assert w._hostname_only("[::1]:8080") == "::1"
assert w._hostname_only("127.0.0.1:2020") == "127.0.0.1"
def test_retry_after_reoptions_still_verifies(self):
"""A re-requested options call (double-click) must not kill the
in-flight ceremony: "challenge was not expected challenge"."""
import backend.auth.webauthn_mfa as w
w._pending.clear()
auth = VirtualAuthenticator()
first = w._store_challenge("bob:register")
w._store_challenge("bob:register") # second options call overwrites
cred = auth.make_registration({"challenge": _b64url(first)})
rec = w.complete_registration("bob", cred, rp_id_override="localhost",
origins_override=["http://localhost"])
assert rec["credential_id"] == cred["id"]
def test_register_flow_without_env_config(self, wa_client, monkeypatch):
"""Full register + login roundtrip with no WEBAUTHN env at all: the
relying party derives from the request (TestClient host)."""
import backend.auth.webauthn_mfa as w
monkeypatch.delenv("OBSIGATE_WEBAUTHN_RP_ID", raising=False)
monkeypatch.delenv("OBSIGATE_WEBAUTHN_ORIGINS", raising=False)
w._pending.clear()
headers = _login_headers(wa_client)
r = wa_client.post("/api/auth/mfa/webauthn/register/options", headers=headers)
assert r.status_code == 200
options = r.json()["options"]
assert options["rp"]["id"] == "testserver"
auth = VirtualAuthenticator()
auth.RP_ID = "testserver"
auth.ORIGIN = "http://testserver"
cred = auth.make_registration(options)
r2 = wa_client.post("/api/auth/mfa/webauthn/register", headers=headers,
json={"credential": cred, "label": "Key"})
assert r2.status_code == 200, r2.text
opts_r = wa_client.post("/api/auth/mfa/webauthn/options",
json={"username": "testuser"})
assertion = auth.make_assertion(opts_r.json()["options"])
v = wa_client.post("/api/auth/mfa/webauthn/verify",
json={"username": "testuser", "credential": assertion})
assert v.status_code == 200, v.text
assert "access_token" in v.json()