21-task plan covering backend (entities, migration, service, API resources) and frontend (DTOs, service, admin tab, project drawer, task modal integration). Reviewed and fixed: readonly class issue, page URL construction, Delete provider handling, task:read group, search query syntax, security attributes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
12 KiB
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)
// 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
TokenEncryptorexistant (même pattern que Gitea) - Repository avec
findSingleton()
TaskBookStackLink
// 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
// 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.jsonlistShelves(): GET/api/shelves(paginé viacount/offset, paspage/limit— spécificité BookStack)searchInShelf(): algorithme en 3 étapes :- GET
/api/shelves/{shelfId}→ récupère la liste desbooksde l'étagère (IDs) - GET
/api/search?query={query} {type:page|book}→ recherche globale (espace entre query et filtre type, BookStack syntax) - Filtre côté PHP : pour les books, vérifie que
book.idest dans la liste de l'étagère ; pour les pages, vérifie quepage.book_idest dans la liste. Exclut les résultatschapteretbookshelf.
- Note : la liste des books de l'étagère peut être cachée en mémoire pour la durée de la requête.
- GET
getPage(): GET/api/pages/{id}getBook(): GET/api/books/{id}
BookStackApiException
// 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
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
BookStackTestConnectionProviderimplémente à la foisProviderInterfaceetProcessorInterface(même pattern queGiteaTestConnectionProvider)- Les endpoints collection du frontend utilisent
extractHydraMembers()pour extraire les résultats des réponses Hydra - Les titres/URLs stockés dans
TaskBookStackLinksont des snapshots au moment du lien — pas de rafraîchissement automatique (intentionnel) - Le select étagère dans
ProjectDrawern'est affiché que pour les admins (endpointROLE_ADMIN)
Frontend
Service
// frontend/services/bookstack.ts
export function useBookStackService() {
// Admin
async function getSettings(): Promise<BookStackSettings>
async function saveSettings(payload: BookStackSettingsWrite): Promise<BookStackSettings>
async function testConnection(): Promise<BookStackTestResult>
// Projet
async function listShelves(): Promise<BookStackShelf[]>
// Tâche
async function getLinks(taskId: number): Promise<BookStackLink[]>
async function addLink(taskId: number, payload: BookStackLinkCreate): Promise<BookStackLink>
async function removeLink(taskId: number, linkId: number): Promise<void>
async function search(taskId: number, query: string): Promise<BookStackSearchResult[]>
}
DTOs
// 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+bookstackShelfNamesur 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
<TaskBookStackLinks>dans le modal, conditionné parproject.bookstackShelfId - Passe
taskIdetprojectIden 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.dockerne contient pasGITEA_ENCRYPTION_KEY. Les développeurs utilisantdocker/.env.docker.localdoivent 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)