# BookStack Connector — Design Spec **Date:** 2026-03-15 **BookStack version:** v25.12.8 **Pattern:** Mirror of Gitea connector ## Overview Connecteur BookStack permettant de lier des documents (pages et livres) du wiki à des tâches Lesstime. Chaque projet peut être associé à une étagère (shelf) BookStack, et les utilisateurs peuvent rechercher et lier des pages/livres de cette étagère à leurs tâches. ## Périmètre - Types liés : **pages** et **livres** (books) - Niveau projet : liaison à une **étagère** (shelf) - Niveau tâche : liaison à une ou plusieurs **pages/livres** de l'étagère du projet - Recherche : filtrée dans l'étagère du projet uniquement - Stockage : **référence** (titre + URL), pas d'aperçu du contenu - Auth BookStack : Token ID + Token Secret (header `Authorization: Token {id}:{secret}`) ## Backend ### Entités #### BookStackConfiguration (singleton) ```php // src/Entity/BookStackConfiguration.php class BookStackConfiguration { private ?int $id; private ?string $url = null; private ?string $encryptedTokenId = null; private ?string $encryptedTokenSecret = null; public function hasToken(): bool; // vérifie que les deux sont présents } ``` - Chiffrement via `TokenEncryptor` existant (même pattern que Gitea) - Repository avec `findSingleton()` #### TaskBookStackLink ```php // src/Entity/TaskBookStackLink.php class TaskBookStackLink { private ?int $id; private Task $task; // ManyToOne, CASCADE on delete private int $bookstackId; // ID dans BookStack private string $bookstackType; // 'page' | 'book' private string $title; // titre au moment du lien (cache) private string $url; // URL complète private \DateTimeImmutable $createdAt; } ``` #### Project (extension) Ajout de deux champs : - `bookstackShelfId` (nullable int) - `bookstackShelfName` (nullable string) — cache du nom pour affichage ### Service #### BookStackApiService ```php // src/Service/BookStackApiService.php class BookStackApiService { public function testConnection(): bool; public function listShelves(): array; public function searchInShelf(int $shelfId, string $query): array; public function getPage(int $id): array; public function getBook(int $id): array; } ``` - Utilise `HttpClientInterface` (Symfony HttpClient) - Auth : header `Authorization: Token {tokenId}:{tokenSecret}` - Timeout : 10 secondes - `testConnection()` : GET `/api/docs.json` - `listShelves()` : GET `/api/shelves` (paginé via `count`/`offset`, pas `page`/`limit` — spécificité BookStack) - `searchInShelf()` : algorithme en 3 étapes : 1. GET `/api/shelves/{shelfId}` → récupère la liste des `books` de l'étagère (IDs) 2. GET `/api/search?query={query} {type:page|book}` → recherche globale (espace entre query et filtre type, BookStack syntax) 3. Filtre côté PHP : pour les **books**, vérifie que `book.id` est dans la liste de l'étagère ; pour les **pages**, vérifie que `page.book_id` est dans la liste. Exclut les résultats `chapter` et `bookshelf`. - Note : la liste des books de l'étagère peut être cachée en mémoire pour la durée de la requête. - `getPage()` : GET `/api/pages/{id}` - `getBook()` : GET `/api/books/{id}` #### BookStackApiException ```php // src/Exception/BookStackApiException.php class BookStackApiException extends \RuntimeException {} ``` ### API Resources & Endpoints #### Admin | Méthode | Route | Ressource API Platform | Sécurité | |---------|-------|----------------------|----------| | GET | `/api/settings/bookstack` | BookStackSettings | ROLE_ADMIN | | PUT | `/api/settings/bookstack` | BookStackSettings | ROLE_ADMIN | | POST | `/api/settings/bookstack/test` | BookStackTestConnection | ROLE_ADMIN | **BookStackSettings** (DTO) : - Read : `url`, `hasToken` - Write : `url`, `tokenId`, `tokenSecret` **BookStackTestConnection** (DTO) : - Read : `success` #### Projet | Méthode | Route | Ressource API Platform | Sécurité | |---------|-------|----------------------|----------| | GET | `/api/bookstack/shelves` | BookStackShelf | ROLE_ADMIN | **BookStackShelf** (DTO) : - Read : `id`, `name` L'étagère sélectionnée est sauvée via le PATCH existant de Project (`bookstackShelfId`, `bookstackShelfName`). #### Tâche | Méthode | Route | Ressource API Platform | Sécurité | |---------|-------|----------------------|----------| | GET | `/api/tasks/{taskId}/bookstack/links` | BookStackLink | Authenticated | | POST | `/api/tasks/{taskId}/bookstack/links` | BookStackLink | Authenticated | | DELETE | `/api/tasks/{taskId}/bookstack/links/{id}` | BookStackLink | Authenticated | | GET | `/api/tasks/{taskId}/bookstack/search?q=` | BookStackSearchResult | Authenticated | **BookStackLink** (DTO) : - Read : `id`, `bookstackId`, `bookstackType`, `title`, `url`, `createdAt` - Write : `bookstackId`, `bookstackType`, `title`, `url` **BookStackSearchResult** (DTO) : - Read : `id`, `type`, `name`, `url` ### State Providers / Processors | Classe | Rôle | |--------|------| | `BookStackSettingsProvider` | Lit config singleton, retourne DTO masqué | | `BookStackSettingsProcessor` | Persiste config, chiffre tokens | | `BookStackTestConnectionProvider` | Appelle `testConnection()` | | `BookStackShelfProvider` | Appelle `listShelves()`, mappe en DTOs | | `BookStackLinkProvider` | Lit `TaskBookStackLink` par task ID | | `BookStackLinkProcessor` | POST : crée lien en DB / DELETE : supprime | | `BookStackSearchResultProvider` | Appelle `searchInShelf()`, mappe en DTOs | ### Migration ```sql CREATE TABLE bookstack_configuration ( id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, url VARCHAR(255) DEFAULT NULL, encrypted_token_id TEXT DEFAULT NULL, encrypted_token_secret TEXT DEFAULT NULL ); CREATE TABLE task_bookstack_link ( id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, task_id INT NOT NULL REFERENCES task(id) ON DELETE CASCADE, bookstack_id INT NOT NULL, bookstack_type VARCHAR(10) NOT NULL, title VARCHAR(255) NOT NULL, url VARCHAR(500) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL ); CREATE INDEX IDX_task_bookstack_link_task_id ON task_bookstack_link (task_id); CREATE UNIQUE INDEX UNIQ_task_bookstack_link ON task_bookstack_link (task_id, bookstack_id, bookstack_type); ALTER TABLE project ADD bookstack_shelf_id INT DEFAULT NULL; ALTER TABLE project ADD bookstack_shelf_name VARCHAR(255) DEFAULT NULL; ``` ### Variable d'environnement Prérequis : renommer `GITEA_ENCRYPTION_KEY` en `ENCRYPTION_KEY` (générique) dans `TokenEncryptor`, `.env`, et `docker/.env.docker`. Mettre à jour le message d'erreur dans `TokenEncryptor`. Cela permet de réutiliser le même service pour chiffrer les tokens BookStack (deux appels `encrypt()`/`decrypt()` : un pour tokenId, un pour tokenSecret). ### Notes techniques - `BookStackTestConnectionProvider` implémente à la fois `ProviderInterface` et `ProcessorInterface` (même pattern que `GiteaTestConnectionProvider`) - Les endpoints collection du frontend utilisent `extractHydraMembers()` pour extraire les résultats des réponses Hydra - Les titres/URLs stockés dans `TaskBookStackLink` sont des snapshots au moment du lien — pas de rafraîchissement automatique (intentionnel) - Le select étagère dans `ProjectDrawer` n'est affiché que pour les admins (endpoint `ROLE_ADMIN`) ## Frontend ### Service ```typescript // frontend/services/bookstack.ts export function useBookStackService() { // Admin async function getSettings(): Promise async function saveSettings(payload: BookStackSettingsWrite): Promise async function testConnection(): Promise // Projet async function listShelves(): Promise // Tâche async function getLinks(taskId: number): Promise async function addLink(taskId: number, payload: BookStackLinkCreate): Promise async function removeLink(taskId: number, linkId: number): Promise async function search(taskId: number, query: string): Promise } ``` ### DTOs ```typescript // frontend/services/dto/bookstack.ts type BookStackSettings = { url: string | null; hasToken: boolean } type BookStackSettingsWrite = { url: string | null; tokenId: string | null; tokenSecret: string | null } type BookStackTestResult = { success: boolean } type BookStackShelf = { id: number; name: string } type BookStackLink = { id: number; bookstackId: number; bookstackType: 'page' | 'book'; title: string; url: string; createdAt: string } type BookStackLinkCreate = { bookstackId: number; bookstackType: 'page' | 'book'; title: string; url: string } type BookStackSearchResult = { id: number; type: 'page' | 'book'; name: string; url: string } ``` ### Composants #### AdminBookStackTab.vue Onglet admin (même pattern que `AdminGiteaTab.vue`) : - Champs : URL, Token ID, Token Secret - Bouton "Tester la connexion" avec indicateur résultat - Indicateur "Token configuré" (ne montre jamais le token) - Sauvegarde via `saveSettings()` #### ProjectDrawer.vue (extension) - Si BookStack est configuré : select pour choisir une étagère - Charge `listShelves()` à l'ouverture - Sauvegarde `bookstackShelfId` + `bookstackShelfName` sur le projet via PATCH #### TaskBookStackLinks.vue Petit composant intégré dans `TaskModal.vue`, visible directement : - **Input de recherche** avec debounce (~300ms) → appel `search(taskId, query)` → dropdown résultats - Chaque résultat : icône (page 📄 / livre 📕) + titre — clic pour ajouter - **Liste des liens** sous le champ recherche : icône type + titre cliquable (ouvre BookStack dans nouvel onglet) + bouton × supprimer - Affiché uniquement si le projet de la tâche a une shelf BookStack configurée - Charge les liens existants au mount via `getLinks(taskId)` #### TaskModal.vue (extension) - Ajoute `` dans le modal, conditionné par `project.bookstackShelfId` - Passe `taskId` et `projectId` en props ## Fichiers à créer/modifier ### Backend — Nouveaux fichiers ``` src/Entity/BookStackConfiguration.php src/Entity/TaskBookStackLink.php src/Repository/BookStackConfigurationRepository.php src/Repository/TaskBookStackLinkRepository.php src/Service/BookStackApiService.php src/Exception/BookStackApiException.php src/ApiResource/BookStackSettings.php src/ApiResource/BookStackTestConnection.php src/ApiResource/BookStackShelf.php src/ApiResource/BookStackLink.php src/ApiResource/BookStackSearchResult.php src/State/BookStackSettingsProvider.php src/State/BookStackSettingsProcessor.php src/State/BookStackTestConnectionProvider.php src/State/BookStackShelfProvider.php src/State/BookStackLinkProvider.php src/State/BookStackLinkProcessor.php src/State/BookStackSearchResultProvider.php migrations/VersionXXXX.php ``` ### Backend — Fichiers modifiés ``` src/Entity/Project.php (ajout bookstackShelfId, bookstackShelfName) src/Service/TokenEncryptor.php (renommage GITEA_ENCRYPTION_KEY → ENCRYPTION_KEY) ``` ### Config — Fichiers modifiés ``` .env (renommage GITEA_ENCRYPTION_KEY → ENCRYPTION_KEY) ``` > Note : `docker/.env.docker` ne contient pas `GITEA_ENCRYPTION_KEY`. Les développeurs utilisant `docker/.env.docker.local` doivent le mettre à jour manuellement. ### Frontend — Nouveaux fichiers ``` frontend/services/bookstack.ts frontend/services/dto/bookstack.ts frontend/components/admin/AdminBookStackTab.vue frontend/components/task/TaskBookStackLinks.vue ``` ### Frontend — Fichiers modifiés ``` frontend/components/task/TaskModal.vue (ajout TaskBookStackLinks) frontend/components/project/ProjectDrawer.vue (ajout select étagère) frontend/components/admin/ (ajout onglet BookStack dans la page admin) ```