9.9 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é, récupère toutes les pages)searchInShelf(): GET/api/search?query={query}+{type:page|book}puis filtre côté serveur par shelf IDgetPage(): 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);
ALTER TABLE project ADD bookstack_shelf_id INT DEFAULT NULL;
ALTER TABLE project ADD bookstack_shelf_name VARCHAR(255) DEFAULT NULL;
Variable d'environnement
Réutilise la même clé de chiffrement que Gitea (TokenEncryptor existant) — pas besoin d'une nouvelle clé.
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)
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)