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>
317 lines
12 KiB
Markdown
317 lines
12 KiB
Markdown
# 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<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
|
||
|
||
```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 `<TaskBookStackLinks>` 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)
|
||
```
|