mediaDrop/README.md
Nicolas fe8bd26bb7 Initial commit: mediaDrop — serveur de dépôt de fichiers médias
Serveur TypeScript (Fastify 5) permettant de déposer des fichiers médias
dans des dossiers nommés, via une page web de glisser-déposer.

- Authentification OpenID Connect gérée côté serveur (Authorization Code +
  PKCE, client confidentiel, openid-client) ; session via cookie chiffré
  (@fastify/secure-session).
- Configuration dans un fichier JSON (schéma fourni) ; l'environnement ne
  porte que le pointeur MEDIADROP_CONFIG.
- Cibles de dépôt nommées : liste { name, label, dir }, sélection par
  ?target=, première cible par défaut.
- Upload multipart en streaming : nom de fichier dérivé côté serveur (pas
  de path traversal), liste blanche de types MIME, plafond de taille.
- TypeScript strict, ESLint + Prettier sans warning, 46 tests Vitest.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 23:06:53 +02:00

8.7 KiB

mediaDrop

Petit serveur pour déposer des fichiers médias (images / vidéos / audio) sur une machine : une page web de glisser-déposer et un endpoint POST /upload. Authentification OpenID Connect gérée côté serveur : le serveur effectue le flux Authorization Code + PKCE, puis maintient une session (cookie chiffré). Les fichiers sont écrits dans un dossier local. Pas de base de données, pas de listing.

Prérequis

  • Node.js >= 22.9
  • Un fournisseur OIDC (Keycloak, Authentik, Auth0, Okta…) joignable au démarrage.

Déclarer le client côté fournisseur

Créer un client confidentiel (« web application ») :

  • type : confidentiel — avec client_secret
  • PKCE : activé (S256)
  • flux : Authorization Code
  • redirect URI : <publicUrl>/auth/callback (ex. http://localhost:3000/auth/callback)
  • post-logout redirect URI : <publicUrl>/
  • scopes : openid profile email

Noter l'issuer, le client_id et le client_secret.

Installation

npm install
cp config.example.json config.json

Éditer config.json : au minimum publicUrl, session.key, oidc.issuer, oidc.clientId, oidc.clientSecret. Générer la clé de session :

openssl rand -base64 32

Toute la configuration vit dans ce fichier JSON. Le serveur lit ./config.json par défaut ; pour un autre emplacement, créer un .env (cp .env.example .env) et y définir MEDIADROP_CONFIG=/chemin/vers/config.json. Le .env ne contient que ce pointeur.

Lancer

npm run dev

Ouvrir http://localhost:3000/, cliquer Se connecter (redirection vers le fournisseur), puis glisser des fichiers.

En production :

npm run build
npm start

Endpoints

Route Auth Rôle
GET / non Page de dépôt
GET /health non Sonde : { "status": "ok", "uptime": 12.3 }
GET /auth/login non Démarre le flux OIDC (redirige vers le fournisseur)
GET /auth/callback non Fin du flux : ouvre la session, redirige vers /
POST /auth/logout session Ferme la session (+ déconnexion RP si disponible)
GET /auth/me non { "authenticated": bool, "name"?: string }
GET /targets session Cibles de dépôt : [{ "name", "label" }] (sans chemin)
POST /upload?target=<nom> session Dépôt d'un fichier multipart/form-data, champ file

POST /upload — le paramètre ?target=<nom> choisit la cible ; absent, la première cible de targets est utilisée. Réponse 201 :

{
  "target": "photos",
  "storedName": "photo-1725900000000-1a2b3c4d.jpg",
  "originalName": "photo.jpg",
  "size": 20480,
  "mimeType": "image/jpeg"
}
Code Cas
400 pas de fichier, mauvais champ, type non autorisé, cible inconnue
401 session absente ou expirée
413 fichier au-dessus de maxFileSizeMb

Le dépôt passe uniquement par la session établie via la page. Il n'y a pas d'accès par jeton Bearer pour les scripts.

Configuration

Un seul fichier JSON (voir config.example.json, schéma dans config.schema.json). Chemin résolu depuis le dossier courant.

Clé JSON Défaut Rôle
port 3000 Port d'écoute
host 0.0.0.0 Interface d'écoute
publicUrl — Obligatoire. URL externe de base (redirect_uri, post-logout)
targets — Obligatoire. Liste [{ name, label?, dir }] (≥ 1) ; la 1ʳᵉ est la cible par défaut
maxFileSizeMb 512 Taille maximale par fichier (Mo)
logLevel info fatal | error | warn | info | debug | trace | silent
allowedMimeTypes (liste média) Tableau de types MIME, ou "all" ; absent => liste par défaut
session.key — Obligatoire. 32 octets base64 (openssl rand -base64 32)
session.maxAgeHours 12 Durée de vie absolue de la session
oidc.issuer — Obligatoire. Issuer OIDC
oidc.clientId — Obligatoire. client_id du client confidentiel
oidc.clientSecret — Obligatoire. Secret du client confidentiel
oidc.scopes ["openid","profile","email"] Scopes demandés (openid forcé)

Seule variable d'environnement : MEDIADROP_CONFIG (chemin du fichier, défaut ./config.json).

Scripts

Commande Effet
npm run dev Serveur en mode watch
npm run build Compilation TypeScript vers dist/
npm start Exécute dist/index.js
npm run typecheck tsc --noEmit
npm run lint ESLint
npm run format Prettier (écriture)
npm test Suite Vitest

Sécurité — limites connues

  • Le serveur ne vérifie pas les rôles/scopes : tout compte valide du fournisseur peut se connecter. Restreindre l'accès au niveau du client OIDC, ou faire évoluer src/oidc.ts (handleCallback) pour exiger un scope / une claim.
  • Session = cookie chiffré @fastify/secure-session (httpOnly, SameSite=Lax, Secure si publicUrl est en https). Durée de vie absolue ; pas de rafraîchissement par refresh token — reconnexion à l'expiration.
  • Le type MIME est celui annoncé par le client, non vérifié via les octets de signature. Durcissement possible avec une bibliothèque de détection (file-type).
  • Les limites (maxFileSizeMb, allowedMimeTypes) sont globales : pas encore de réglage par cible. Le name de cible fourni par le client sert uniquement à retrouver une entrée de targets (jamais concaténé à un chemin).
  • Pas de limitation de débit. Placer le service derrière un reverse proxy HTTPS en production et régler publicUrl sur l'URL publique.
  • .env, config.json et uploads/ sont ignorés par Git.

Structure

config.example.json  modèle de configuration (à copier en config.json)
config.schema.json   schéma JSON (validation / autocomplétion éditeur)
src/
  index.ts          entrée : config -> app -> écoute + arrêt propre
  app.ts            construction de l'instance Fastify (exportée pour les tests)
  config.ts         lecture du fichier JSON + validation -> AppConfig
  oidc.ts           découverte de l'issuer + flux Authorization Code (openid-client)
  session.ts        type de session + hook preHandler requireSession
  errors.ts         classes d'erreur (AppError + dérivées)
  error-handler.ts  gestionnaire d'erreurs global
  routes/
    auth.ts         GET /auth/login, /auth/callback, /auth/me ; POST /auth/logout
    health.ts       GET /health
    upload.ts       GET /targets ; POST /upload?target=... (session requise)
  lib/
    storage.ts      écriture disque dans la cible, nom de fichier sûr, plafond de taille
    media-types.ts  liste blanche des types + extensions
public/
  index.html        page de dépôt (login = lien vers /auth/login, sans build)