# 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.
```
