docs(share) : design explorateur de partage Windows + viewer (SMB)

This commit is contained in:
Matthieu
2026-06-03 16:30:36 +02:00
parent 226ab8ea84
commit 83d938fd91
@@ -0,0 +1,186 @@
# Explorateur de partage réseau Windows + viewer — Design
Date : 2026-06-03
Statut : design validé (brainstorming), à transformer en plan d'implémentation.
## 1. Objectif
Donner accès, **depuis Lesstime**, à un partage de fichiers Windows (SMB), avec :
- un **explorateur de fichiers façon Google Drive / SharePoint** qui parcourt le partage **en direct** (live, pas d'index) ;
- un **viewer propre** pour ouvrir les documents (image, PDF, texte) sans quitter l'app ;
- une **configuration en admin** (serveur, partage, identifiants) avec un **bouton « Tester la connexion »** et un **interrupteur d'activation**, sur le même modèle que les intégrations existantes (Zimbra, Gitea, BookStack) ;
- une **visibilité conditionnelle** : si l'option SMB est **désactivée** dans l'admin, l'entrée « Documents » et la page **n'apparaissent pas** pour les utilisateurs.
### Hors périmètre (POC)
- Pas d'index en base, pas de recherche plein texte, pas d'extraction de contenu (pas de Tika).
- Pas d'OCR.
- Pas d'écriture sur le partage (lecture seule).
- Pas de cron / synchronisation. Tout est lu **à la volée** à chaque navigation.
## 2. Décisions d'architecture
| Sujet | Décision |
|-------|----------|
| Accès au partage | **`icewind/smb`** (protocole SMB en PHP), **pas de montage CIFS**. La connexion est configurée dans l'app. |
| Configuration | Entité `ShareConfiguration` (1 ligne) saisie en admin, mot de passe chiffré au repos — calquée sur `ZimbraConfiguration`. |
| Abstraction | Interface `FileSource` (lister / lire), implémentation `SmbFileSource`. Permet de remplacer la source plus tard sans toucher au front ni aux endpoints. |
| API navigation | 2 endpoints live : `browse` (lister un dossier) et `download` (streamer un fichier). |
| Front | Explorateur **maison léger** (fil d'Ariane + tableau), cohérent avec `@malio/layer-ui`. Aucune lib de file-manager externe (elFinder/vue-finder écartés : vieux ou hors design system). |
| Rendu PDF | **PDF.js via `vue-pdf-embed`** dans le viewer (meilleur rendu qu'un `<iframe>`). Images et texte : rendu natif. |
| Sécurité chemin | Validation stricte anti path-traversal : tout chemin demandé doit rester sous la racine configurée. |
### Schéma
```
//WIN-SRV/Partage
│ SMB (icewind/smb, identifiants chiffrés en base)
Lesstime (Symfony) ──FileSource → SmbFileSource──┐
│ │
├─ GET /api/share/browse?path=/Compta/2024 → listing live (dossiers + fichiers)
├─ GET /api/share/download?path=…/x.pdf → stream du fichier (viewer / download)
├─ GET/PUT /api/settings/share → lire / enregistrer la config (admin)
└─ POST /api/settings/share/test → tester la connexion (admin)
```
## 3. Backend (Symfony)
### 3.1 Entité `ShareConfiguration`
Une seule ligne de config (singleton, comme `ZimbraConfiguration`). Champs :
- `id`
- `host` (string, ex. `WIN-SRV` ou IP)
- `shareName` (string, nom du partage SMB, ex. `Documents`)
- `basePath` (string nullable, sous-dossier racine optionnel, ex. `/Projets`) — la navigation est confinée à cette racine
- `domain` (string nullable, workgroup/domaine, défaut `WORKGROUP`)
- `username` (string nullable)
- `encryptedPassword` (text nullable) — chiffré, réutilise le mécanisme de chiffrement déjà employé par Zimbra
- `enabled` (bool, défaut `false`)
- `hasPassword()` helper
Migration Doctrine dédiée. Repository singleton (`findConfiguration()` renvoie la ligne unique ou en crée une vide), calqué sur `ZimbraConfigurationRepository`.
### 3.2 Ressources API de configuration (admin)
Calquées **à l'identique** sur Zimbra :
- `ShareSettings` (ApiResource) — `Get` + `Put` sur `/api/settings/share`, `security: ROLE_ADMIN`.
- Champs lus/écrits : `host`, `shareName`, `basePath`, `domain`, `username`, `enabled`.
- `password` : **write-only** (groupe write uniquement).
- `hasPassword` : **read-only** (indique si un mot de passe est déjà enregistré).
- Provider `ShareSettingsProvider` (lit l'entité → DTO), Processor `ShareSettingsProcessor` (DTO → entité, chiffre le mot de passe si fourni, ne l'écrase pas s'il est vide).
- `ShareTestConnection` (ApiResource) — `Post` sur `/api/settings/share/test`, `input: false`, `security: ROLE_ADMIN`.
- Renvoie `{ success: bool, message: string|null }`.
- Provider `ShareTestConnectionProvider` : tente une connexion SMB + un `dir()` sur la racine ; `success=false` + message d'erreur lisible en cas d'échec.
### 3.3 Source de fichiers
```
interface FileSource {
list(string $relativeDir): FileEntry[] // dossiers d'abord, puis fichiers
read(string $relativePath): resource // flux binaire du fichier
test(): TestResult // connexion + accès racine
}
```
`FileEntry` = `{ name, path, isDir, size, modifiedAt, mimeType }`.
`SmbFileSource` :
- construit la connexion à partir de `ShareConfiguration` (déchiffre le mot de passe) via `icewind/smb` ;
- préfixe tous les chemins par `basePath` ;
- **valide chaque chemin** (`normalize` + rejet de tout chemin qui s'échappe de la racine : pas de `..`, pas de chemin absolu hors racine) → `InvalidPathException` sinon ;
- déduit le `mimeType` à partir de l'extension (suffisant pour piloter le viewer ; pas de lecture du contenu pour le listing).
> **Dépendance infra** : `icewind/smb` requiert le binaire `smbclient` (ou l'extension `libsmbclient`) dans le conteneur PHP. Les deux images sont Debian (`apt-get`), donc une seule ligne suffit, **à appliquer dans les deux Dockerfiles** :
> - `infra/dev/Dockerfile` — ajouter `smbclient` à la liste `apt-get install` existante (~ligne 9).
> - `infra/prod/Dockerfile` — ajouter `smbclient` à l'`apt-get install` du **stage `production`** (le runtime FPM, ~ligne 41), **pas** au stage de build.
>
> Conséquence déploiement : l'image prod (`lesstime-app`) doit être **rebuildée et redéployée** pour embarquer `smbclient` ; sans ça, la fonctionnalité marcherait en dev et échouerait en prod. À inscrire comme étape du plan (avec la migration Doctrine de `ShareConfiguration`).
### 3.4 Endpoints de navigation
Controllers custom sous `/api/` (pas d'entité Doctrine derrière → controllers, avec `priority: 1` sur la route pour éviter le conflit avec API Platform `{id}`), `security: IS_AUTHENTICATED_FULLY` :
- `GET /api/share/browse?path=<rel>``ShareBrowseController`
- renvoie `{ path, breadcrumb[], entries: FileEntry[] }` ;
- si config désactivée/incomplète → `409` avec message clair ;
- chemin invalide → `400`.
- `GET /api/share/download?path=<rel>&disposition=inline|attachment``ShareDownloadController`
- streame le fichier (`StreamedResponse`) avec le bon `Content-Type` ;
- `inline` par défaut (pour le viewer), `attachment` pour le téléchargement ;
- fichier absent → `404`.
- `GET /api/share/status``ShareStatusController`, `security: IS_AUTHENTICATED_FULLY`
- renvoie `{ enabled: bool }`**uniquement le booléen**, aucune donnée de connexion ;
- utilisé par le front pour afficher/masquer l'entrée « Documents » et garder la page.
## 4. Frontend (Nuxt)
### 4.1 Explorateur — `pages/documents.vue`
- **Fil d'Ariane** du chemin courant (cliquable pour remonter).
- **Tableau** des entrées : dossiers d'abord, puis fichiers ; colonnes nom (icône par type), taille, date de modification.
- clic dossier → on descend (met à jour `path`, recharge `browse`) ;
- clic fichier → ouvre le viewer.
- **Filtre par nom** du dossier courant, **côté client** (live, non-indexé) — filtre simplement la liste déjà chargée.
- États : chargement, dossier vide, erreur (config désactivée / connexion KO) avec message.
### 4.2 Viewer — `components/share/SharedFilePreview.vue`
Adapté de `TaskDocumentPreview.vue` existant :
- **Image** : `<img>` sur l'URL `download?disposition=inline`.
- **PDF** : **`vue-pdf-embed`** (PDF.js) — rendu, pagination, zoom.
- **Texte/markdown/csv/json** : chargement du contenu + `<pre>` (comme l'existant).
- **Autre** : carte « fichier » + bouton de téléchargement (`attachment`).
- Navigation précédent/suivant dans la liste du dossier courant, fermeture clavier — repris de l'existant.
### 4.3 Service & config admin
- `services/share.ts` : `browse(path)`, `getDownloadUrl(path, disposition)` + DTO `FileEntry`.
- `services/share-settings.ts` (+ DTO) : `get()`, `update(payload)`, `test()` — calqué sur `services/zimbra.ts`.
- `components/admin/AdminShareTab.vue` : calqué sur `Admin ZimbraTab.vue` — champs host / shareName / basePath / domain / username / password + toggle `enabled`, bouton **« Tester la connexion »** (toast succès/échec) et **« Enregistrer »**. Onglet ajouté à la page admin.
- **i18n** : nouvelles clés (`sharedFiles.*`, `adminShare.*`) dans `frontend/i18n/locales/`.
- **Navigation conditionnelle** : le lien « Documents » du layout n'est affiché **que si** `GET /api/share/status` renvoie `enabled=true` (récupéré via un composable, ex. `useShareStatus`, mis en cache). Le middleware/garde de `pages/documents.vue` redirige vers l'accueil si la fonctionnalité est désactivée (défense en profondeur, en plus du `409` backend).
### 4.4 Dépendance frontend
`vue-pdf-embed` (+ `pdfjs-dist`) ajouté au `package.json` du frontend.
## 5. Flux
- **Configuration** (admin) : saisie host/partage/identifiants → « Tester » (`POST /settings/share/test`) → « Enregistrer » (`PUT /settings/share`).
- **Navigation** (utilisateur) : ouverture `/documents``GET /share/browse?path=/` → tableau ; clic dossier → re-`browse` ; clic fichier → viewer → `GET /share/download?...inline`.
- **Téléchargement** : bouton → `GET /share/download?...attachment`.
## 6. Gestion des erreurs
- **SMB injoignable / identifiants faux** → `browse`/`download` renvoient une erreur ; l'UI affiche un message clair. Le test de connexion renvoie `success=false` + message.
- **Config désactivée ou incomplète** → `browse` `409`, UI invite à configurer (admin).
- **Path-traversal** (`..`, chemin hors racine) → `400`, jamais d'accès hors `basePath`.
- **Fichier supprimé/déplacé entre listing et ouverture** → `download` `404`, message dans le viewer.
## 7. Sécurité
- **Lecture seule** : aucune écriture sur le partage.
- **Rôles** : navigation/lecture = utilisateur authentifié (`IS_AUTHENTICATED_FULLY`) ; configuration = `ROLE_ADMIN`.
- **Mot de passe chiffré au repos** (réutilise le mécanisme Zimbra), jamais renvoyé au front (`hasPassword` seulement).
- **Confinement** strict à `basePath` (anti path-traversal).
## 8. Tests
- **Unitaire**
- `SmbFileSource` : validation/normalisation de chemin, rejet `..` et chemins hors racine (connexion SMB mockée).
- Déduction du `mimeType` par extension.
- **Fonctionnel**
- `GET/PUT /api/settings/share` et `POST /api/settings/share/test` exigent `ROLE_ADMIN` ; le mot de passe n'est jamais exposé en lecture.
- `GET /api/share/browse` et `/download` exigent l'authentification ; un chemin `..` est rejeté (`400`).
## 9. Notes & suites possibles
- Perf : chaque `browse` = un aller-retour SMB live ; acceptable pour un POC. Gros dossiers = listing potentiellement lent (pas de pagination au POC).
- Évolutions naturelles (non incluses) : index + recherche plein texte (Tika), miniatures, multi-partages, restriction par dossier/rôle, mise en cache des listings.
```