Aller au contenu

Project Presentation Images

Flux d'upload des images de la présentation d'un projet (image principale, images secondaires, images de lots).

Le modèle est déclaratif : l'upload ne fait que déposer un fichier sur S3 et le confirmer ; le placement des images (quelle clé est principale / secondaire / sur quel lot) est décidé au save de la présentation (PATCH …/presentation/:id, voir project-presentation.md), pas ici. Il n'y a donc plus d'endpoint de suppression d'image : retirer une image = l'enlever de la liste envoyée au save.

Source : project-presentation-images.controller.ts.

Tous les endpoints requièrent un porteur authentifié via better-auth (ProjectFinancingRequestOwnerGuard). La route portant un :projectId, le guard vérifie aussi que le projet appartient au porteur (403) et qu'il existe (404).

Cycle de vie d'une image

  1. request-upload → le back génère une clé S3 + une URL PUT presignée par fichier.
  2. Le navigateur PUT le fichier directement sur S3 (jamais via l'API).
  3. confirm-upload → le back vérifie seulement que l'objet existe et appartient au projet. Il ne pose aucun tag : une image confirmée n'est jamais candidate à la suppression (elle survit tant qu'un save ne la retire pas).
  4. Le front met les clés dans le body du save de la présentation.
  5. Au save : les clés qui disparaissent de la galerie sont taguées pending ; les clés conservées n'ont aucun tag.
  6. Une lifecycle rule S3 (filtre pending=true) expire les objets restés pending après leur TTL (images retirées d'une galerie).

Le tag pending a un seul sens : « image retirée de la galerie, à nettoyer ». Une image uploadée mais jamais confirmée (onglet fermé en plein upload) reste sans tag, donc non nettoyée — fuite de stockage mineure, hors périmètre de ce flux.

POST …/presentation/:presentationId/images/request-upload

Génère un lot d'URLs PUT presignées. La cible (main/secondary/lot) n'est pas connue ici — elle est décidée au save.

Request body

requestUploadProjectPresentationImagesBodySchema :

{ "files": [ { "type": "image/jpeg | image/png | image/webp", "size": 123456 } ] }

Contraintes : 1 à 20 fichiers, 10 MB max chacun.

Response

{ "uploads": [ { "imageKey": "ProjectFinancingRequestS3Key", "presignedUrl": "https://...", "expiresAt": "ISO date" } ] }

imageKey est la clé S3 brandée à conserver pour le confirm-upload puis le save.

POST …/presentation/:presentationId/images/confirm-upload

Valide que les clés uploadées appartiennent au projet et existent sur S3, puis les tague pending. Ne touche pas à la présentation.

Request body

confirmUploadProjectPresentationImagesBodySchema :

{ "imageKeys": ["ProjectFinancingRequestS3Key", "..."] }

Contrainte : 1 à 20 clés. Chaque clé doit commencer par projects/{projectId}/presentation-images/.

Response

{ "imageKeys": ["ProjectFinancingRequestS3Key", "..."] }

Erreurs

Code Statut Cause
validation-body 400 Body invalide (batch hors limites)
s3-key-invalid-prefix 400 Une clé n'appartient pas au projet
image-key-not-found 400 Une clé ne correspond à aucun objet S3
s3-put-tagging-failed 500 Échec du tag pending

Placement et suppression des images

Il n'y a pas d'endpoint de placement ni de suppression dédiés. Le front envoie l'état voulu de la galerie dans le body du PATCH …/presentation/:id :

{
  "mainImageKey": "key | null",
  "secondaryImageKeys": ["key", "..."],
  "lots": [ { "id": "...", "name": "...", "imageKey": "key | null" } ]
}
  • mainImageKey / lots[].imageKey : une clé = set, null = efface, absent = inchangé.
  • secondaryImageKeys : la liste exacte voulue (ordre inclus) ; [] efface toutes les secondaires.

Le détail du contrat de lecture ({ key, url }) et d'écriture est dans project-presentation.md.

Liens