docs: guide complet des API (API_GUIDE_V6.md) — référence implémentation v6.0.0
FlowDeck CI / test (push) Failing after 6s
FlowDeck CI / docker (push) Skipped

This commit is contained in:
2026-09-05 01:16:43 -04:00
parent 56fa5dcd61
commit 9eedfa67ca
2 changed files with 679 additions and 1 deletions
+678
View File
@@ -0,0 +1,678 @@
# Guide des API FlowDeck — Référence d'implémentation v6.0.0
> **Statut** : référence de conception pour la mise en place de l'API publique complète (v6.0.0).
> **Dernière mise à jour** : 2026-09-04
> **Portée** : inventaire de l'API existante, conventions cibles, design CRUD par ressource, webhooks, sécurité, checklist d'implémentation.
---
## 1. Vision & architecture
FlowDeck est **API-first** (FastAPI). Deux couches d'API coexistent :
| Couche | Préfixe | Auth | Usage |
|--------|---------|------|-------|
| **API interne** (existant) | `/api`, `/workspace`, `/db`, `/board`… | Session cookie `flowdeck_session` + CSRF | Le frontend HTMX/Alpine |
| **API publique v1** (existant, lecture seule) | `/api/v1` | Bearer token | Intégrations externes minimales |
| **API publique v2** (cible v6.0.0) | `/api/v2` | Bearer token + scopes | CRUD complet, usage externe (clone Notion API) |
**Principe** : l'API v2 expose les mêmes opérations que l'API interne, mais avec une surcouche de contrôle (auth par token, scopes, pagination, filtres, erreurs normalisées). Elle est implémentée comme des wrappers sur les mêmes services/logique métier — jamais de duplication.
### Règles d'or
1. Un endpoint public = un wrapper autour de la logique existante (un seul chemin de code).
2. L'API publique ne renvoie **jamais** de HTML, de secrets, ni de colonnes internes (`password_hash`, tokens OAuth).
3. Toute mutation passe par les mêmes validations que l'UI (CSRF concerne l'UI seule ; les tokens remplacent CSRF).
4. Versionnage par préfixe d'URL (`/api/v1`, `/api/v2`) — jamais de breaking change sur une version publiée.
---
## 2. État actuel — inventaire de l'API
### 2.1 Schéma global des routeurs (`app/main.py`)
| Routeur | Fichier | Préfixe | Rôle |
|---------|---------|---------|------|
| `auth` | `routers/auth.py` | `/auth` | Login OAuth/local, register, logout, sessions |
| `dashboard` | `routers/dashboard.py` | — | Pages HTML (landing, workspace, settings…) |
| `board` | `routers/board.py` | `/board` | Kanban Gitea (legacy, compatible) |
| `notes` | `routers/notes.py` | `/notes` | Notes Markdown par projet |
| `api` | `routers/api.py` | `/api` | Pages, workspaces, tags, trash, favoris, settings |
| `webhooks` | `routers/webhooks.py` | `/api/webhook` | Réception webhooks Gitea (issues/PR/repo) |
| `collections` | `routers/collections.py` | `/db` | Databases Notion-style (CRUD) |
| `my_tasks` | `routers/my_tasks.py` | `/my-tasks` | Agrégation tâches |
| `workspace` | `routers/workspace.py` | `/workspace` | Local workspace, arborescence, webhooks sortants |
| `library` | `routers/library.py` | `/api/library` | Bibliothèque (sources local/Gitea/GitHub) |
| `admin` | `routers/admin.py` | `/api/admin` | Admin utilisateurs + audit |
| `gitea` | `routers/gitea.py` | `/api/gitea` | Projets, fichiers, commits, labels |
| `github` | `routers/github_routes.py` | `/api/github` | Intégration GitHub |
| `public_api` | `routers/public_api.py` | `/api/v1` | **API publique existante (lecture seule)** |
| `sharing` | `routers/sharing.py` | `/api` | Share/publish pages |
| `sidebar_config` | `routers/sidebar_config.py` | `/api/sidebar` | Customisation sidebar |
| `export` | `routers/export.py` | `/api/export` | Export MD/PDF/HTML/CSV |
| `notifications` | `routers/notifications.py` | `/api/notifications` | Notifications in-app |
| `collaboration` | `routers/collaboration.py` | `/api` | Commentaires inline, mentions, historique |
### 2.2 Récapitulatif des endpoints existants (par domaine)
> Liste non exhaustive côté pages HTML ; les endpoints API sont tous listés.
**Auth & users**
```
GET /auth/login, /auth/register, /auth/callback, /auth/logout, /auth/user
POST /auth/local-login, /auth/register, /auth/register/{owner}/{repo}
GET /api/users, /api/users/me, /api/users/search
PUT /api/users/me, /api/user/profile, /api/user/password
POST /api/user/token, /api/settings/avatar, /api/settings/avatar-color, /api/settings/tags
GET /api/settings/account, /api/settings/avatar/{filename}
PUT /api/settings/tags/{tag_id}, /api/settings/tags/all (GET)
DELETE /api/settings/tags/{tag_id}, /api/user/forge/{provider}
```
**Workspaces & membres**
```
GET /api/workspaces, POST /api/workspaces
GET /api/workspace/{ws_id}/members, POST /api/workspace/{ws_id}/members
PUT /api/workspace/{ws_id}/members/{user_id}, DELETE /api/workspace/{ws_id}/members/{user_id}
PUT /api/workspaces/{ws_id}, DELETE /api/workspaces/{ws_id}
POST /api/workspaces/{ws_id}/select
```
**Pages (éditeur blocs)**
```
GET /api/pages/{page_id}, PUT /api/pages/{page_id}, DELETE /api/pages/{page_id}
POST /api/pages (création), /api/pages/{page_id}/blocks, /api/pages/{page_id}/content
PUT /api/pages/{page_id}/rename, /api/pages/{page_id}/move
POST /api/pages/{page_id}/trash, /api/trash/{page_id}/restore
GET /api/trash, DELETE /api/trash/{page_id}
POST /api/pages/{page_id:int}/convert-to-database
GET /api/local-workspace/tree, /api/sidebar/workspace-tree, /api/nav/menu
GET /api/local-workspace/page-content/{page_id}, /children/{page_id}
```
**Collections (databases)**
```
GET /db/{collection_id}/api (détail), DELETE /db/{collection_id}
POST /db/inline/api (créer inline)
POST /db/{id}/linked/api, /db/{id}/toggle-inline/api, PUT /db/{id}/toggle-task/api
GET /db/{id}/sources/api, POST /db/{id}/sources/api, DELETE /db/{id}/sources/{sid}/api
GET /db/{id}/properties/api, POST /db/{id}/properties/api
PUT /properties/{prop_id}/api, DELETE /properties/{prop_id}/api
POST /db/{id}/properties/relation, /db/{id}/properties/relation/link
GET /db/{id}/views/api, POST /db/{id}/views/save-as, PUT /views/{view_id}/config
GET /property-types/api
```
**Collection pages & tâches**
```
GET /db/{id}/pages/api, POST /db/{id}/pages/api, PUT /pages/{page_id}/api, DELETE /pages/{page_id}/api
POST /db/{id}/pages/{pid}/sub-items, GET /db/{id}/pages/{pid}/sub-items
GET /db/{id}/pages/{pid}/dependencies/api, POST /db/{id}/pages/{pid}/dependencies/api
DELETE /db/{id}/pages/{pid}/dependencies/{dep_id}/api
POST /db/{id}/pages/{pid}/auto-shift/api, /db/{id}/pages/{pid}/check-deps
GET /db/{id}/pages/{pid}/status-aggregate
POST /formula/evaluate, /rollup/compute
```
**Sprints, dashboards, templates**
```
GET/POST /workspace/collections/{id}/sprints, PUT/DELETE /workspace/collections/{id}/sprints/{sid}
POST /workspace/collections/{id}/sprints/{sid}/assign, DELETE .../assign/{page_id}
GET /workspace/collections/{id}/sprints/burndown/{sid}
GET/POST/PUT/DELETE /workspace/collections/{id}/dashboards[/{did}]
GET/POST /workspace/collections/{id}/templates/page, PUT/DELETE .../templates/page/{tid}
POST .../templates/page/{tid}/apply
GET/POST /templates/database, POST /templates/database/{tid}/apply
```
**Favoris, tags, recents**
```
GET/POST /api/favorites, DELETE /api/favorites/{page_id}
GET/POST /db/{id}/pages/api… (tags par page via /api/local-workspace/items/{id}/tags)
GET /api/local-workspace/tags, /api/local-workspace/tags/search
POST /api/local-workspace/items/{id}/tags, DELETE .../tags/{tag_id}
GET /api/recents, POST /api/recents/track
```
**Partage & publication**
```
POST /api/share/{page_id}, GET /api/pages/{page_id}/shares, DELETE /api/pages/{page_id}/share/{share_id}
POST /pages/{page_id}/share, POST /pages/{page_id}/publish, DELETE /pages/{page_id}/publish
GET /public/{collection_id}, /published, /shared, /private, /p/{slug}
```
**Commentaires & notifications (v4.9.0)**
```
GET/POST /pages/{page_id}/comments, PUT/DELETE /comments/{comment_id}
POST /pages/{page_id}/mentions
GET /api/notifications, /api/notifications/unread-count
POST /api/notifications/read, /api/notifications/read-all
```
**Historique**
```
GET/POST /pages/{page_id}/history
```
**Import / Export**
```
GET /api/collections/{id}/export/csv, POST /api/collections/{id}/import/csv
GET /api/export/markdown|pdf|html (via /export et /markdown/{page_id}, /pdf/{page_id}, /html/{page_id})
```
**Forge Gitea / GitHub**
```
GET /api/workspace/projects, POST /api/workspace/projects
GET /projects/{owner}/{repo}/tree|labels|commits|private-pages|file
PUT /projects/{owner}/{repo}/file, DELETE /projects/{owner}/{repo}/file
POST /projects/{owner}/{repo}/upload, /sync-labels, /private-pages
GET/POST /issues/{owner}/{repo}, PATCH /issues/{owner}/{repo}/{issue_id}
POST /api/sync/{owner}/{repo}, /api/ai-keywords/{owner}/{repo}/extract
```
**Admin**
```
GET /api/admin/users, PUT /api/admin/users/{user_id}, DELETE /api/admin/users/{user_id}
GET /audit, /api/frontend-errors, POST /api/frontend-error
```
**Santé & divers**
```
GET /api/health, /api/stats, /api/config, PUT /api/config, /api/favorites…
POST /api/move, /col-mapping (GET/DELETE/POST), /board-config/{owner}/{repo} (GET/POST)
```
### 2.3 API publique v1 existante (`app/routers/public_api.py`)
| Méthode | Route | Description |
|---------|-------|-------------|
| POST | `/api/v1/token` | Génère un token `fd_xxx` (inséré dans `user_tokens`) |
| GET | `/api/v1/collections` | Liste des collections (id, name, description, icon, created_at) |
| GET | `/api/v1/collections/{id}` | Collection + ses pages |
| GET | `/api/v1/collections/{id}/pages` | Pages racine de la collection |
| GET | `/api/v1/pages/{page_id}` | Page détaillée |
| GET | `/api/v1/my-tasks` | Tâches (limit 50) |
**Auth v1** : header `Authorization: Bearer <token>`. Token par défaut : `fd-public-key` (fallback dev). Vérification dans la table `user_tokens` (colonne `gitea_token`).
**Limites de v1** (ce que v2 doit corriger) :
- Lecture seule — aucun POST/PUT/DELETE sur les ressources.
- Un seul token par utilisateur (`UNIQUE(gitea_user_id)`) et stocké dans une table prévue pour les tokens Gitea.
- Pas de scopes, pas d'expiration, pas de nom d'affichage, pas de révocation individuelle.
- Pas de pagination, filtres, tri.
- Pas de documentation OpenAPI en production (`docs_url` n'est actif qu'en DEBUG).
---
## 3. Conventions cibles pour l'API v2
### 3.1 Auth & tokens
**Nouvelle table `api_tokens`** (remplace l'usage détourné de `user_tokens`) :
```sql
CREATE TABLE api_tokens (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name TEXT NOT NULL DEFAULT 'default', -- ex: "CI production"
token_hash TEXT NOT NULL UNIQUE, -- sha256 du token, jamais en clair
scopes TEXT NOT NULL DEFAULT 'read', -- 'read' | 'read,write' | 'read,write,admin'
expires_at TIMESTAMP, -- NULL = jamais
last_used_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
revoked INTEGER NOT NULL DEFAULT 0
);
```
- **Format token** : `fd_{user_id}_{urlsafe(32)}` — l'utilisateur ne le voit qu'une fois. Stockage en `sha256`.
- **Crud tokens** : `POST /api/v2/tokens`, `GET /api/v2/tokens`, `DELETE /api/v2/tokens/{id}` (révocation), `POST /api/v2/tokens/rotate`.
- **Scopes** : `read` (GET), `write` (POST/PUT/PATCH), `admin` (gestion users/admin). Dépendance FastAPI `require_scope("write")`.
- **Compat v1** : garder le fallback `fd-public-key` uniquement si `settings.public_api_insecure_ok=true` (défaut : **false** en prod).
### 3.2 Format des réponses
- JSON brut (pas d'enveloppe `{data: …}`) — style Notion API.
- Timestamps **ISO-8601 UTC** (`2026-09-04T14:30:00Z`). SQLite stocke `YYYY-MM-DD HH:MM:SS` → convertir dans le convertisseur de sortie.
- IDs : entiers SQLite (`int`). Exposés tels quels (pas de UUID) — `ponytail: garder int, passer en UUID seulement si exposition publique nécessaire`.
- Champs `*_json` stockés en SQLite (ex: `property_values_json`, `config_json`) → **déjà parsés en objets JSON** dans les réponses API.
### 3.3 Erreurs (RFC 7807 `application/problem+json`)
```json
{
"type": "https://flowdeck/api/errors/not-found",
"title": "Resource not found",
"status": 404,
"detail": "Collection 42 n'existe pas",
"instance": "/api/v2/collections/42"
}
```
Codes : `400` validation (détail = liste des champs), `401` token manquant/invalide, `403` scope insuffisant / permissions, `404` inexistant, `409` conflit (nom dupliqué, dépendance bloquante), `422` erreur de schéma (FastAPI), `429` rate limit, `500` interne.
Handler global : `@app.exception_handler(HTTPException)` + adaptation des erreurs internes → problème JSON pour tout préfixe `/api/v2`.
### 3.4 Pagination, filtres, tri
**Pagination offset** (simple, SQLite) :
```
GET /api/v2/collections?limit=30&offset=0
→ response headers: X-Total-Count: 142
```
Requête : `limit` (défaut 30, max 100), `offset` (défaut 0).
Utilise `SELECT COUNT(*)` + `LIMIT ? OFFSET ?` — `ponytail: offset OK jusqu'à ~10k lignes, passer à keyset (cursor) si besoin`.
**Filtres** (par propriété de la ressource) :
```
GET /api/v2/collections/{id}/pages?filter[status]=Done&filter[assignee]=bruno
GET /api/v2/pages?query=mot&workspace_id=3
```
Convention : `filter[<property>]=<value>` (AND implicite), `sort=<property>`, `sort=-<property>` (desc), `fields=a,b,c` (projection).
**Recherche FTS** : réserver `?query=` sur les listes → recherche plein texte (table FTS5 à créer, indexant pages + collection_pages).
### 3.5 Idempotence & mutations
- `POST` de création accepte un header optionnel `Idempotency-Key` — sur conflit, renvoyer la ressource existante créée avec cette clé (table `idempotency_keys`).
- Les `PATCH` sont partiels (seuls les champs présents sont modifiés).
### 3.6 Rate limiting par token
Étendre `RateLimitMiddleware` (actuellement 100 req/min/IP sur `/api/`, `/board/api/`, `/auth/`) :
- `/api/v2/*` : quota **par token** (défaut 300 req/min, configurable par scope), en plus du quota IP.
- Remplacer le store `defaultdict` in-memory par une clé composite `(ip|token_id)` — `ponytail: in-memory OK mono-instance, Redis si multi-workers`.
- Header de réponse : `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`.
- Exemptés : `/api/health`, `/api/v2/tokens` (auth).
---
## 4. Design des endpoints v2 — CRUD par ressource
> Pour chaque ressource : table(s) SQLite, endpoints internes existants (réutilisables), endpoints publics v2 à créer.
> **`GET /collections` → wrapper `public_list_collections` existant à enrichir** ; les mutations s'appuient sur les routeurs `collections.py` (préfixe `/db`).
### 4.1 Users
| Table | `users` |
|-------|---------|
| Colonnes clés | id, login, full_name, email, avatar_url, avatar_color, is_admin, is_active, auth_method, sidebar_config, notification_prefs |
| **Interne** | `GET/PUT /api/users/me`, `GET /api/users`, `PUT/DELETE /api/users/{id}` (admin) |
Endpoints publics v2 :
| Méthode | Route | Description |
|---------|-------|-------------|
| GET | `/api/v2/users/me` | Profil du user courant (sans `password_hash`) |
| PATCH | `/api/v2/users/me` | Mettre à jour profil (full_name, email, avatar_color, notification_prefs) |
| GET | `/api/v2/users` | **admin** — lister (pagination) |
| GET | `/api/v2/users/search?q=` | Recherche par login/email (pour @mentions) |
### 4.2 Workspaces & membres
| Tables | `workspaces`, `workspace_members` (role: owner/admin/editor/viewer) |
|--------|-----|
| **Interne** | `GET/POST /api/workspaces`, `PUT/DELETE /api/workspaces/{ws_id}`, `GET/POST /api/workspace/{ws_id}/members`, `PUT/DELETE …/members/{user_id}` |
Endpoints publics v2 :
| Méthode | Route | Description |
|---------|-------|-------------|
| GET | `/api/v2/workspaces` | Workspaces de l'utilisateur (avec rôle) |
| POST | `/api/v2/workspaces` | Créer (body: name, settings_json) |
| GET | `/api/v2/workspaces/{id}` | Détail + membres |
| PATCH | `/api/v2/workspaces/{id}` | Renommer / settings |
| DELETE | `/api/v2/workspaces/{id}` | Supprimer (owner only) |
| GET | `/api/v2/workspaces/{id}/members` | Lister membres |
| POST | `/api/v2/workspaces/{id}/members` | Inviter (user_id ou email) |
| PATCH | `/api/v2/workspaces/{id}/members/{uid}` | Changer le rôle |
| DELETE | `/api/v2/workspaces/{id}/members/{uid}` | Retirer un membre |
> ⚠️ **Dépendance v6** : créer `app/services/permission_manager.py` (la roadmap y fait référence mais le fichier n'existe pas). Centraliser : `require_workspace_role(ws_id, "editor")`, `can_read(workspace_id|collection_id|page_id, user)`.
### 4.3 Collections (databases)
| Tables | `collections` (+ `workspace_id`, `created_by`, `is_inline`, `parent_page_id`, `is_task`) |
|--------|-----|
| **Interne** | `POST /db/inline/api`, `GET/DELETE /db/{id}/api`, `POST /db/{id}/linked/api`, `POST /db/{id}/toggle-inline/api`, `PUT /db/{id}/toggle-task/api`, sources (`/db/{id}/sources/api`) |
Endpoints publics v2 :
| Méthode | Route | Description |
|---------|-------|-------------|
| GET | `/api/v2/collections?workspace_id=&query=` | Liste (paginée) — wrapper de v1 enrichi |
| POST | `/api/v2/collections` | Créer (name, description, icon, workspace_id, schema_json) |
| GET | `/api/v2/collections/{id}` | Détail + pages (existant v1) |
| PATCH | `/api/v2/collections/{id}` | Renommer, icon, description, schema |
| DELETE | `/api/v2/collections/{id}` | Supprimer (cascade pages/vues/props) |
| POST | `/api/v2/collections/{id}/linked` | Créer une DB liée |
| POST | `/api/v2/collections/{id}/task` | toggler `is_task` |
| GET | `/api/v2/collections/{id}/sources` | Sources / DB liées — wrapper v1 |
| POST | `/api/v2/collections/{id}/sources` | Ajouter une source |
| DELETE | `/api/v2/collections/{id}/sources/{sid}` | Retirer une source |
### 4.4 Pages de collection (collection_pages)
| Tables | `collection_pages` (property_values_json, parent_id, position, gitea_issue_*) |
|--------|-----|
| **Interne** | `GET/POST /db/{id}/pages/api`, `GET/PUT/DELETE /pages/{page_id}/api`, sub-items, dependencies, auto-shift, status-aggregate, move |
Endpoints publics v2 :
| Méthode | Route | Description |
|---------|-------|-------------|
| GET | `/api/v2/collections/{id}/pages?filter[status]=…&sort=…` | Lister (pagination, filtres, tri, fields) |
| POST | `/api/v2/collections/{id}/pages` | Créer (title, icon, property_values, parent_id) |
| GET | `/api/v2/pages/{id}` | Détail (propriétés parsées) |
| PATCH | `/api/v2/pages/{id}` | Titre, icon, position, property_values (merge) |
| DELETE | `/api/v2/pages/{id}` | Corbeille (soft delete via `deleted_at`) |
| POST | `/api/v2/pages/{id}/restore` | Restaurer |
| POST | `/api/v2/pages/{id}/move` | Reordonner (position / parent_id) |
| GET | `/api/v2/pages/{id}/sub-items` | Sous-tâches |
| POST | `/api/v2/pages/{id}/sub-items` | Créer sous-item |
| GET/POST/DELETE | `/api/v2/pages/{id}/dependencies[/{dep_id}]` | Dépendances |
> **Détail `property_values`** : objet JSON `{"<property_id>": <valeur typée>}` — utiliser `property_types.py` (validation/formatage) déjà en place.
### 4.5 Propriétés (`collection_properties`)
| Colonnes clés | name, prop_type (21 types), options_json, number_format, related_collection_id, reverse_name, relation_property_id, target_property_id, rollup_function, formula_expression, required, visible_in_views |
|-----|-----|
| **Interne** | `GET/POST /db/{id}/properties/api`, `PUT/DELETE /properties/{prop_id}/api`, `POST …/properties/relation`, `…/relation/link`, `POST /formula/evaluate`, `POST /rollup/compute`, `GET /property-types/api` |
Endpoints publics v2 :
| Méthode | Route | Description |
|---------|-------|-------------|
| GET | `/api/v2/collections/{id}/properties` | Types + configuration (wrapper) |
| POST | `/api/v2/collections/{id}/properties` | Créer (name, prop_type, options…) |
| PATCH | `/api/v2/properties/{id}` | Modifier (options, required, formula, rollup…) |
| DELETE | `/api/v2/properties/{id}` | Supprimer |
| POST | `/api/v2/properties/{id}/relation` | Créer relation bidirectionnelle |
| POST | `/api/v2/properties/evaluate-formula` | Moteur formula (debug) |
| POST | `/api/v2/properties/compute-rollup` | Moteur rollup (debug) |
### 4.6 Vues (`collection_views`) & Dashboards (`collection_dashboards`)
| **Interne** | `GET /db/{id}/views/api`, `POST /db/{id}/views/save-as`, `PUT /views/{view_id}/config`, `GET/POST/PUT/DELETE /workspace/collections/{id}/dashboards[/{did}]` |
|-----|-----|
| Méthode | Route | Description |
|---------|-------|-------------|
| GET | `/api/v2/collections/{id}/views` | Lister les vues (config_json parsé) |
| POST | `/api/v2/collections/{id}/views` | Créer (name, view_type, config) |
| PATCH | `/api/v2/views/{id}` | Modifier config (filtres, tri, group_by, layout) |
| DELETE | `/api/v2/views/{id}` | Supprimer |
| POST | `/api/v2/views/{id}/save-as` | Dupliquer sous un autre nom |
| GET/POST/PUT/DELETE | `/api/v2/collections/{id}/dashboards[/{did}]` | Dashboards (layout_json) |
### 4.7 Commentaires (`comments`, v4.9.0)
| Colonnes clés | target_type ('page'/'collection_page'), target_id, user_id, body, parent_id, resolved, anchor_block_id, anchor_start, anchor_end |
|-----|-----|
| **Interne** | `GET/POST /pages/{page_id}/comments`, `PUT/DELETE /comments/{comment_id}`, `POST /pages/{page_id}/mentions` |
| Méthode | Route | Description |
|---------|-------|-------------|
| GET | `/api/v2/pages/{id}/comments` | Lister (avec tri chronologique) |
| POST | `/api/v2/pages/{id}/comments` | Ajouter (body, anchor_block_id/start/end optionnels) |
| PATCH | `/api/v2/comments/{id}` | Éditer / résoudre (`resolved: true`) |
| DELETE | `/api/v2/comments/{id}` | Supprimer |
| POST | `/api/v2/pages/{id}/mentions` | Mentionner des users (déclenche notification + email) |
### 4.8 Notifications (`notifications`)
| ntype | 'mention' / 'comment' / 'page' — resource_type, resource_id, url, is_read |
|-----|-----|
| **Interne** | `GET /api/notifications`, `GET /unread-count`, `POST /read`, `POST /read-all` |
| Méthode | Route | Description |
|---------|-------|-------------|
| GET | `/api/v2/notifications?unread=1` | Lister (pagination) |
| POST | `/api/v2/notifications/{id}/read` | Marquer lue |
| POST | `/api/v2/notifications/read-all` | Tout marquer lu |
| GET | `/api/v2/notifications/unread-count` | Compteur (badge) |
| PATCH | `/api/v2/users/me/preferences` | prefs email (comments/mentions) |
### 4.9 Favoris & Tags & Recents
| Tables | `favorites`, `tags` (per-user), `page_tags`, `recents` |
|-----|-----|
| **Interne** | `GET/POST /api/favorites`, `DELETE /api/favorites/{page_id}`, `/api/settings/tags*`, `/api/local-workspace/items/{id}/tags*`, `/api/recents*` |
| Méthode | Route | Description |
|---------|-------|-------------|
| GET | `/api/v2/favorites` | Lister |
| POST | `/api/v2/favorites` | Ajouter `{page_id}` |
| DELETE | `/api/v2/favorites/{page_id}` | Retirer |
| GET | `/api/v2/tags?q=` | Tags de l'utilisateur |
| POST | `/api/v2/tags` | Créer (name, color) |
| PUT/DELETE | `/api/v2/tags/{id}` | Modifier / supprimer |
| POST | `/api/v2/pages/{id}/tags` | Attacher `{tag_id}` |
| DELETE | `/api/v2/pages/{id}/tags/{tag_id}` | Détacher |
| GET | `/api/v2/recents?source_type=` | Récents (limit 20) |
### 4.10 Partage & publication (`page_shares`)
| Colonnes clés | page_id, shared_with_user_id, shared_with_email, permission ('view'/'edit'), created_by + colonnes pages (is_published, publish_slug, is_shared, share_mode) |
|-----|-----|
| **Interne** | `POST /api/share/{page_id}`, `GET /api/pages/{id}/shares`, `DELETE /api/pages/{id}/share/{share_id}`, `POST /pages/{id}/publish`, `DELETE /pages/{id}/publish` |
| Méthode | Route | Description |
|---------|-------|-------------|
| GET | `/api/v2/pages/{id}/shares` | Liste des accès |
| POST | `/api/v2/pages/{id}/shares` | Partager `{email|user_id, permission}` |
| PATCH | `/api/v2/shares/{share_id}` | Changer permission |
| DELETE | `/api/v2/shares/{share_id}` | Révoquer |
| POST | `/api/v2/pages/{id}/publish` | Publier (`{slug}`) → page publique `/p/{slug}` |
| DELETE | `/api/v2/pages/{id}/publish` | Dé-publier |
### 4.11 Historique (`page_history`)
| Colonnes clés | page_id, user_id, change_type, snapshot_json, created_at |
|-----|-----|
| **Interne** | `GET/POST /pages/{page_id}/history` |
| Méthode | Route | Description |
|---------|-------|-------------|
| GET | `/api/v2/pages/{id}/history` | Liste des versions (change_type, created_at, user) |
| POST | `/api/v2/pages/{id}/history/restore` | Restaurer `{history_id}` |
### 4.12 Sprints (`sprints`, `sprint_pages`)
| **Interne** | `/workspace/collections/{id}/sprints[/{sid}]`, assign, burndown |
|-----|-----|
| Méthode | Route | Description |
|---------|-------|-------------|
| GET/POST | `/api/v2/collections/{id}/sprints` | Lister / créer |
| PATCH/DELETE | `/api/v2/sprints/{sid}` | Mettre à jour / supprimer |
| POST | `/api/v2/sprints/{sid}/assign` | Assigner `{page_id, velocity_points}` |
| DELETE | `/api/v2/sprints/{sid}/assign/{page_id}` | Retirer |
| GET | `/api/v2/sprints/{sid}/burndown` | Points (total/completed/remaining/ideal) |
### 4.13 Templates (`page_templates`, `database_templates`)
| **Interne** | `/workspace/collections/{id}/templates/page*`, `/templates/database*`, apply |
|-----|-----|
| Méthode | Route | Description |
|---------|-------|-------------|
| GET/POST | `/api/v2/collections/{id}/templates` | Lister / créer un template de page |
| PATCH/DELETE | `/api/v2/templates/{tid}` | Modifier / supprimer |
| POST | `/api/v2/templates/{tid}/apply` | Instancier (crée une page depuis le template) |
| GET | `/api/v2/templates/database` | Templates de DB (Project tracker, CRM…) |
| POST | `/api/v2/templates/database/{tid}/apply` | Créer une collection depuis le template |
### 4.14 Export & Import
| **Interne** | `GET /api/collections/{id}/export/csv`, `POST /api/collections/{id}/import/csv`, `/export`, `/markdown/{page_id}`, `/pdf/{page_id}`, `/html/{page_id}` (routers/export.py) |
|-----|-----|
| Méthode | Route | Description |
|---------|-------|-------------|
| GET | `/api/v2/pages/{id}/export` | `?format=markdown|pdf|html` → fichier |
| GET | `/api/v2/collections/{id}/export/csv` | CSV |
| POST | `/api/v2/collections/{id}/import/csv` | Import CSV (multipart) |
> Les exports PDF/HTML/MD existent déjà (`export.py`) — v2 ne fait que les exposer avec auth token. Générer le fichier puis le renvoyer (`FileResponse`) ou une URL signée temporaire.
### 4.15 Forges (Gitea / GitHub)
| **Interne** | `routers/gitea.py` + `services/gitea_client.py` (adapter), `routers/github_routes.py` + `services/github_adapter.py` |
|-----|-----|
| Méthode | Route | Description |
|---------|-------|-------------|
| GET | `/api/v2/projects` | Projets forges liés (workspace) |
| GET | `/api/v2/projects/{owner}/{repo}/tree` | Arborescence |
| GET | `/api/v2/projects/{owner}/{repo}/file?path=` | Lire un fichier |
| PUT | `/api/v2/projects/{owner}/{repo}/file` | Écrire `{path, content, message}` (commit) |
| DELETE | `/api/v2/projects/{owner}/{repo}/file` | Supprimer (commit) |
| GET | `/api/v2/projects/{owner}/{repo}/commits?path=` | Historique |
| GET | `/api/v2/projects/{owner}/{repo}/issues` | Issues (provider-agnostic via ForgeAdapter) |
| PATCH | `/api/v2/issues/{provider}/{owner}/{repo}/{id}` | Mettre à jour |
> Cible v6 (roadmap) : interface **`ForgeAdapter`** commune (Gitea + GitHub) — les endpoints v2 consomment l'adapter, pas les clients bruts.
### 4.16 Admin & Audit
| **Interne** | `routers/admin.py` (`/api/admin`), `/audit` |
|-----|-----|
| Méthode | Route | Description |
|---------|-------|-------------|
| GET | `/api/v2/admin/users` | **scope admin** — lister (pagination) |
| PATCH | `/api/v2/admin/users/{id}` | Activer/désactiver, rôle admin |
| DELETE | `/api/v2/admin/users/{id}` | Supprimer |
| GET | `/api/v2/admin/audit-logs` | Dernières actions (login, exports, changements rôle) |
### 4.17 Recherche
| Métier | FTS5 sur `pages` + `collection_pages` (+ tags) — table d'index à créer à la migration v6 |
|-----|-----|
| Méthode | Route | Description |
|---------|-------|-------------|
| GET | `/api/v2/search?query=&workspace_id=&type=page|collection|all` | Résultats groupés par type, snippets |
---
## 5. Webhooks sortants (à étendre en v2)
**Mécanisme existant** (`app/services/webhook_outbound.py`) :
- Table `webhook_subscriptions`: id, url, event, secret, active, created_at.
- Événements actuels (8) :
```python
EVENTS = [
"page.created", "page.updated", "page.deleted",
"collection.created", "collection.updated", "collection.deleted",
"comment.added", "page.moved",
]
```
- Dispatch `fire_event(event, payload)` → POST JSON avec headers `X-FlowDeck-Event` + `X-FlowDeck-Secret`.
**Endpoints v2 webhooks** (wrapper du CRUD sortant existant + gestion des abonnements) :
| Méthode | Route | Description |
|---------|-------|-------------|
| GET | `/api/v2/webhooks` | Abonnements de l'utilisateur |
| POST | `/api/v2/webhooks` | Créer `{url, event, secret}` |
| PATCH | `/api/v2/webhooks/{id}` | Activer/désactiver, changer url/secret |
| DELETE | `/api/v2/webhooks/{id}` | Supprimer |
| POST | `/api/v2/webhooks/{id}/test` | Événement de test `ping` |
| GET | `/api/v2/webhooks/{id}/deliveries` | Journal des livraisons (à ajouter : table `webhook_deliveries`) |
**Événements à ajouter** (parité Notion + besoins v6) :
```python
"page.created", "page.updated", "page.deleted", "page.restored",
"collection.created", "collection.updated", "collection.deleted",
"collection_page.created", "collection_page.updated", "collection_page.deleted",
"comment.added", "comment.resolved",
"mention.created", "notification.created",
"sprint.created", "sprint.updated", "sprint.completed",
"share.created", "share.revoked", "page.published", "page.unpublished",
"agent.run.completed",
```
**Payload type** :
```json
{
"event": "page.updated",
"data": { "id": 42, "workspace_id": 3, "updated_at": "2026-09-04T14:30:00Z" },
"actor": { "id": 1, "login": "bruno" },
"timestamp": "2026-09-04T14:30:00Z"
}
```
- Signature : HMAC-SHA256 du body avec `secret`, header `X-FlowDeck-Signature`.
- Retry : 3 tentatives (2s, 10s, 60s) + journal `webhook_deliveries` (status, http_code, error, duration_ms).
---
## 6. Schéma de données — résumé par ressource
> Schéma complet dans `app/db.py` (`init_db()` + migrations). Tables principales :
| Domaine | Tables |
|---------|--------|
| Auth | `users`, `login_history`, `user_tokens` (Gitea), `user_oauth_tokens`, **`api_tokens` (à créer)** |
| Kanban legacy | `boards`, `cards`, `col_mapping`, `checklists`, `checklist_items`, `notes` |
| Pages | `pages` (content_format='markdown'/'blocks', deleted_at, share_mode, is_published, publish_slug, is_shared, collection_id) |
| Databases | `collections`, `collection_pages`, `collection_views`, `collection_properties`, `collection_data_sources`, `collection_dashboards` |
| Multi-utilisateur | `workspaces`, `workspace_members`, `comments` (target_type/target_id/anchors), `page_history`, `favorites`, `recents`, `page_shares` |
| Templates | `database_templates`, `page_templates` (is_recurring, recurrence_rule) |
| Tags | `tags` (per-user), `page_tags` |
| Tâches | `page_dependencies` (blocks/related, auto_shift), `sprints`, `sprint_pages` |
| Notifications | `notifications` (+ users.notification_prefs) |
| Forges | `gitea_private_pages`, (projets via adapters) |
| Webhooks | `webhook_subscriptions`, **`webhook_deliveries` (à créer)** |
| Agent | (v4.10.0 — à créer : `agents`, `agent_conversations`, `agent_messages`, `agent_actions`, `agent_skills`, `agent_triggers`) |
**Règles** :
- FKs avec `ON DELETE CASCADE` là où la suppression parent doit purger (pages, vues, props, membres, dépendances).
- `property_values_json`, `schema_json`, `config_json`, `layout_json`, `options_json` : JSON textuel, **parse/validate côté service** (`property_types.py`).
- WAL mode + `PRAGMA foreign_keys=ON` (déjà en place dans `get_conn()`).
---
## 7. Sécurité (v2)
| Menace | Contre-mesure |
|--------|---------------|
| Token volé | Hash sha256 en DB, révocation, rotation, `expires_at`, scopes minimaux |
| Rejeu CSRF | N/A côté token (Bearer) ; l'UI garde son CSRF middleware |
| Brute force / abus | Rate limit par token + par IP (middleware existant étendu), 429 |
| Injection SQL | SQL paramétré (`?` seul, jamais de f-string dans les requêtes) |
| XSS (contenu) | Sanitisation à la sortie HTML (UI) ; l'API renvoie du JSON brut (aucun rendu) |
| Exfiltration de secrets | Projection stricte : jamais `password_hash`, `*_token`, clés OAuth dans les réponses |
| CORS | Configurer `allow_origins` pour les clients externes (settings v2) |
| Fichiers | `validate_upload()` existant : extensions whitelist + 10 MB max |
| Audit | Journaliser chaque mutation v2 (user, token_id, action, resource, ts) — table `api_audit_log` |
---
## 8. OpenAPI & documentation
- Activer `docs_url="/docs"` et `redoc_url="/redoc"` **en production** sur le routeur v2 uniquement (ou protéger par token admin).
- Générer `openapi.json` à la release et le versionner dans `docs/openapi-v2.json`.
- Tags OpenAPI par domaine (workspaces, collections, pages, properties, views, comments…).
- Exemples request/response réels dans les docstrings (FastAPI le génère automatiquement en OpenAPI).
- Garder `docs_url=None` tant que v1 est seule (comportement actuel), l'activer lors du merge v2.
---
## 9. Checklist d'implémentation v6.0.0 (ordre recommandé)
1. **Migration DB** : table `api_tokens` (+ index sur token_hash), `webhook_deliveries`, `api_audit_log`, FTS5 index de recherche.
2. **`PermissionManager`** (`app/services/permission_manager.py`) — prerequisite de toute la v2 : `can_read/can_write/can_admin` par workspace + collection + page.
3. **Conventions** : handler d'erreurs RFC 7807, convertisseur ISO-8601, helper pagination (`paginate(query, limit, offset)`), dépendances `require_scope`.
4. **Wrappers v2 read** : reprendre `public_api.py` → `/api/v2` avec pagination/filtres (collections, pages, my-tasks).
5. **Wrappers v2 write** : collections, pages, properties, views (mutation) — les plus demandés par les intégrations.
6. **Webhooks v2** : CRUD abonnements + déliveries + retry + signature HMAC + événements étendus.
7. **Reste des ressources** : sprints, templates, dashboards, favoris, tags, partage, notifications, admin.
8. **Recherche FTS** (`/api/v2/search`).
9. **OpenAPI** : activer /docs + générer openapi-v2.json + exemples.
10. **Tests** (`tests/test_public_api_v2.py`) : auth token + scopes, CRUD complet par ressource, pagination, erreurs, rate limit, webhook delivery (mock httpx). Cible : couverture ≥ 80 % sur le routeur v2.
11. **Documentation** utilisateur : page `/help` + ce guide référencé depuis le README.
## 10. Références
- Schéma DB : `app/db.py`
- Routeurs existants : `app/routers/*.py`
- Frontend : `app/templates/*.html`, `static/js/app.js`
- Webhooks sortants : `app/services/webhook_outbound.py`
- Types de propriétés : `app/services/property_types.py`
- Roadmap : `ROADMAP.md` (v6.0.0 — API publique, realtime, synced blocks, web clipper, permissions granulaires)