upload_d_n_d 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +229 -0
  4. data/Rakefile +8 -0
  5. data/app/assets/config/upload_d_n_d_manifest.js +3 -0
  6. data/app/assets/stylesheets/upload_d_n_d/application.css +604 -0
  7. data/app/components/upload_d_n_d/upload_component.html.erb +53 -0
  8. data/app/components/upload_d_n_d/upload_component.rb +204 -0
  9. data/app/controllers/concerns/upload_d_n_d/rate_limiting.rb +89 -0
  10. data/app/controllers/upload_d_n_d/attachments_controller.rb +218 -0
  11. data/app/controllers/upload_d_n_d/base_attachments_controller.rb +250 -0
  12. data/app/controllers/upload_d_n_d/temporary_attachments_controller.rb +200 -0
  13. data/app/helpers/upload_d_n_d/application_helper.rb +51 -0
  14. data/app/helpers/upload_d_n_d/controllers/helpers.rb +21 -0
  15. data/app/javascript/upload_d_n_d/controllers/preview_controller.js +217 -0
  16. data/app/javascript/upload_d_n_d/controllers/uploads_controller.js +396 -0
  17. data/app/javascript/upload_d_n_d/index.js +13 -0
  18. data/app/jobs/upload_d_n_d/application_job.rb +4 -0
  19. data/app/jobs/upload_d_n_d/cleanup_temporary_attachments_job.rb +17 -0
  20. data/app/jobs/upload_d_n_d/purge_orphan_blobs_job.rb +25 -0
  21. data/app/jobs/upload_d_n_d/purge_rejected_blob_job.rb +24 -0
  22. data/app/models/upload_d_n_d/application_record.rb +5 -0
  23. data/app/models/upload_d_n_d/temporary_attachment.rb +129 -0
  24. data/app/views/upload_d_n_d/shared/_file_icon.html.erb +9 -0
  25. data/app/views/upload_d_n_d/shared/_files_list.html.erb +63 -0
  26. data/app/views/upload_d_n_d/shared/_temp_files_list.html.erb +53 -0
  27. data/config/importmap.rb +5 -0
  28. data/config/initializers/inflections.rb +11 -0
  29. data/config/locales/upload_d_n_d.en.yml +80 -0
  30. data/config/locales/upload_d_n_d.fr.yml +80 -0
  31. data/config/routes.rb +12 -0
  32. data/docs/CHANGELOG.md +292 -0
  33. data/lib/generators/upload_d_n_d/POLICY_README +49 -0
  34. data/lib/generators/upload_d_n_d/USAGE +80 -0
  35. data/lib/generators/upload_d_n_d/install_generator.rb +174 -0
  36. data/lib/generators/upload_d_n_d/mime_type_helper.rb +106 -0
  37. data/lib/generators/upload_d_n_d/model_generator.rb +313 -0
  38. data/lib/generators/upload_d_n_d/policy_generator.rb +25 -0
  39. data/lib/generators/upload_d_n_d/templates/POST_INSTALL +62 -0
  40. data/lib/generators/upload_d_n_d/templates/concerns/controllers_uploadable.rb +115 -0
  41. data/lib/generators/upload_d_n_d/templates/concerns/upload_authorization.rb +91 -0
  42. data/lib/generators/upload_d_n_d/templates/concerns/uploadable.rb +101 -0
  43. data/lib/generators/upload_d_n_d/templates/controller.rb.tt +60 -0
  44. data/lib/generators/upload_d_n_d/templates/helpers/upload_helper.rb +105 -0
  45. data/lib/generators/upload_d_n_d/templates/initializers/upload_d_n_d.rb +69 -0
  46. data/lib/generators/upload_d_n_d/templates/migrations/create_upload_d_n_d_temporary_attachments.rb +18 -0
  47. data/lib/generators/upload_d_n_d/templates/policies/upload_d_n_d_attachment_policy.rb +37 -0
  48. data/lib/generators/upload_d_n_d/templates/policies/upload_d_n_d_temporary_attachment_policy.rb +56 -0
  49. data/lib/generators/upload_d_n_d/templates/validators/file_validator.rb +171 -0
  50. data/lib/tasks/upload_d_n_d_storage.rake +27 -0
  51. data/lib/tasks/upload_d_n_d_tasks.rake +20 -0
  52. data/lib/upload_d_n_d/attachment_lookup.rb +24 -0
  53. data/lib/upload_d_n_d/authorization/can_can_can_adapter.rb +29 -0
  54. data/lib/upload_d_n_d/authorization/no_authorization.rb +13 -0
  55. data/lib/upload_d_n_d/authorization/pundit_adapter.rb +41 -0
  56. data/lib/upload_d_n_d/authorization/unconfigured.rb +15 -0
  57. data/lib/upload_d_n_d/authorization.rb +66 -0
  58. data/lib/upload_d_n_d/configuration.rb +97 -0
  59. data/lib/upload_d_n_d/controllers/uploadable.rb +271 -0
  60. data/lib/upload_d_n_d/engine.rb +50 -0
  61. data/lib/upload_d_n_d/field_constraints.rb +92 -0
  62. data/lib/upload_d_n_d/field_reference.rb +44 -0
  63. data/lib/upload_d_n_d/file_validator.rb +156 -0
  64. data/lib/upload_d_n_d/mime_types.rb +90 -0
  65. data/lib/upload_d_n_d/orphan_blobs.rb +151 -0
  66. data/lib/upload_d_n_d/rejected_blob.rb +42 -0
  67. data/lib/upload_d_n_d/session_id.rb +25 -0
  68. data/lib/upload_d_n_d/uploadable.rb +117 -0
  69. data/lib/upload_d_n_d/version.rb +3 -0
  70. data/lib/upload_d_n_d.rb +62 -0
  71. metadata +150 -0
data/docs/CHANGELOG.md ADDED
@@ -0,0 +1,292 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.9.2] - 2026-10-29
11
+
12
+ Reconnaissance des formats `.ods`/`.heic`/`.heif` et affichage des extensions (au lieu des types MIME bruts) sous la zone de dépôt. Patch, sans changement d'API.
13
+
14
+ ### Added
15
+ - **Formats `.ods`, `.heic` et `.heif`** ajoutés à la table de correspondance (`MimeTypes`).
16
+
17
+ ### Fixed
18
+ - **Zone de dépôt : extensions affichées au lieu des types MIME bruts** — un format absent de la table (ex. `.ods`, `.heic`) affichait son type MIME complet (`application/vnd.oasis.opendocument.spreadsheet`) sous la zone. `to_extensions` dérive désormais l'extension via Marcel pour tout type non listé ; le type brut n'apparaît qu'en dernier recours (inconnu de la gem et de Marcel).
19
+
20
+ ## [0.9.1] - 2026-10-29
21
+
22
+ ### Changed
23
+ - Documentation : clarifications mineures.
24
+
25
+ ## [0.9.0] - 2026-10-29
26
+
27
+ Lot post-0.8.0 : nettoyage opt-in des blobs orphelins, CI matricielle avec audit des CVE, et deux correctifs — dont `strict_mime_check` désormais utilisable avec un champ fichier classique. Version mineure, rétrocompatible. **Mise à jour recommandée.** Les apps sous Pundit ou CanCanCan détecté automatiquement n'ont qu'à passer leur `tag:` à `v0.9.0` ; le nettoyage des orphelins est opt-in, rien n'est activé sans config.
28
+
29
+ ### Added
30
+ - **Nettoyage des blobs orphelins** (`UploadDND::OrphanBlobs`, `UploadDND::PurgeOrphanBlobsJob`, tâche rake `upload_d_n_d:purge_orphans`) : supprime, de façon opt-in, les blobs envoyés par DirectUpload mais jamais rattachés (refus, coupure réseau, onglet fermé). Un blob n'est supprimé que s'il n'a aucun rattachement, est plus vieux que `orphan_blob_min_age` (2 jours par défaut), n'est pas gardé par un fichier temporaire, et n'est **cité dans aucun texte de la base** (colonnes texte et JSON) — par clé du bucket, par `signed_id` ou par sgid ; jamais par le nom de fichier. La rake simule par défaut (`APPLY=1` pour supprimer), le job journalise un rapport par défaut (`apply: true` pour supprimer). Réglages : `orphan_blob_min_age`, `orphan_scan_columns` (liste blanche pour borner la mémoire), `orphan_scan_patterns`, `orphan_scan_excluded_tables`. PostgreSQL requis pour le scan des citations (sinon erreur explicite, jamais de suppression sans ce filet)
31
+ - **`view_component` déclaré en dépendance d'exécution** (`>= 3.0`) : `UploadComponent` en hérite, la dépendance était jusqu'ici implicite. Sans effet pour une app qui l'a déjà
32
+
33
+ ### Changed
34
+ - **CI matricielle** : la CI teste désormais la plage de Rails déclarée (Ruby 3.2/3.3/3.4 × Rails 7.2/8.0/8.1, en diagonale plus un croisé), avec service PostgreSQL, et ajoute `bundler-audit` (audit des CVE). Dépendances de développement mises à jour pour les correctifs de sécurité (Rails 8.1.4, view_component 3.27, puma 8.0.2) — sans effet pour les apps, qui résolvent leur propre lock
35
+ - **Gemspec finalisé pour la publication open source** : borne Rails `>= 7.2.3, < 8.2` (seul Rails 8.1 est testé) ; métadonnées complètes (homepage, `source_code_uri`, `changelog_uri`, `bug_tracker_uri`, MFA requise) pointant vers le dépôt GitHub public ; licence MIT ; e-mail de contact à jour ; dépendance de dev `rack-attack` retirée (le rate limiting s'appuie sur `Rails.cache`, pas sur rack-attack)
36
+ - **La gem embarque désormais son CHANGELOG** (`docs/CHANGELOG.md` ajouté à la liste de fichiers publiée). Nettoyage du dépôt : `node_modules/` n'est plus suivi, le squelette Minitest inutilisé est retiré (la suite est RSpec), `package.json` passe en 0.8.0
37
+
38
+ ### Fixed
39
+ - **`strict_mime_check` avec un champ fichier classique** : avec un `file_field` ordinaire (le fichier part avec le formulaire, envoyé au stockage seulement après le `save`), la vérification anti-spoofing lisait le fichier depuis le stockage pendant la validation, donc avant son envoi. Le fichier n'y était pas encore → `ActiveStorage::FileNotFoundError`, traduit en « Impossible de vérifier le type de fichier » : tous les fichiers refusés. Le validateur lit désormais la source encore en attente (`attachment_changes`) quand le blob n'est pas encore stocké, et retombe sur le stockage sinon (zone de dépôt). Gère `has_one_attached` et `has_many_attached`. Aucun changement pour la zone de dépôt, déjà fonctionnelle. Même correction portée au template du générateur
40
+ - **Conseil dangereux du README** : la doc proposait `ActiveStorage::Blob.unattached.purge_later` sans garde-fou, ce qui supprime des envois en cours et des fichiers cités dans du HTML stocké hors Action Text. Remplacé par un renvoi vers le nettoyage sûr ci-dessus
41
+
42
+ ### Notes
43
+ - **Montée de Rails** : la borne `< 8.2` empêche une app hôte de passer à Rails 8.2 tant qu'une version de la gem ne la relâche pas. Prévoir une release de la gem avant toute montée majeure de Rails côté app.
44
+
45
+ ## [0.8.0] - 2026-10-19
46
+
47
+ Durcissement de la sécurité avant 1.0 : autorisation fail-closed, limites d'un champ appliquées côté serveur dès le dépôt, et statuts d'erreur corrects sur les réponses Turbo Stream. Version mineure, rétrocompatible, avec notes de migration (voir ci-dessous). **Mise à jour recommandée.** Les apps avec Pundit ou CanCanCan détecté automatiquement n'ont rien à changer.
48
+
49
+ ### Security
50
+ - **Autorisation fail-closed** : la gem n'autorisait que si l'adaptateur valait `:pundit` et que Pundit était chargé. Dans tous les autres cas, uploads et suppressions passaient sans contrôle et sans avertissement : aucune librairie détectée (`nil`), `:cancancan` (détecté mais jamais implémenté), `:pundit` sans Pundit chargé. Côté concern `Controllers::Uploadable`, `attach_file` et `remove_file` sautaient l'autorisation quand le contrôleur n'avait pas de méthode `authorize`. Toute autorisation passe désormais par un port unique (`UploadDND::Authorization`). Sans adaptateur utilisable (`nil`, valeur inconnue), la requête est refusée (403). Un adaptateur déclaré mais inutilisable fait échouer la requête (500, `UploadDND::Authorization::ConfigurationError`)
51
+ - **Limites d'un champ appliquées au dépôt** : `POST /upload_d_n_d/attachments` et `Controllers::Uploadable#attach_file` n'appliquaient aucune limite de taille ni de type (seule la validation du modèle, à l'enregistrement, protégeait), et `POST /upload_d_n_d/temporary_attachments` n'appliquait que les limites globales. Le transfert des fichiers temporaires vers le record (`Uploadable#attach_temporary_uploads`, `TemporaryAttachment.attach_to_record`) ne validait rien : un fichier hors limites du champ y était attaché. Les limites du champ (`uploadable`) sont désormais vérifiées au dépôt sur les trois points d'entrée, et de nouveau au transfert (fichier écarté et journalisé). Un fichier refusé est purgé
52
+ - **Messages d'erreur** : `ActionController::BadRequest` et l'échec de détection MIME (contrôleur et `FileValidator`) ne renvoient plus le message de l'exception au client (`upload_d_n_d.errors.bad_request`, `upload_d_n_d.errors.mime_detection_failed` sans `%{message}`). Le détail reste dans les logs
53
+
54
+ ### Fixed
55
+ - **Statut 200 sur un échec en Turbo Stream** : dans le concern `Controllers::Uploadable`, chaque réponse d'erreur en Turbo Stream rendait le flux sans statut (200), alors que la variante JSON renvoyait le bon code. Un client ne pouvait pas distinguer l'échec du succès sur le flux Turbo. Concerné : `attach_file` (échec d'enregistrement, désormais 422), `remove_file` (fichier absent ou refusé, 404), et les `rescue_from` `ActiveStorage::IntegrityError` et `ActiveStorage::UnrepresentableError` (422). `422 + turbo_stream` est l'idiome Rails/Turbo pour un envoi en échec
56
+ - **Limites différentes entre navigateur et serveur** : sur un formulaire de création, le composant affichait les limites du modèle quand le serveur appliquait les limites globales. Un fichier accepté par le navigateur pouvait être refusé au dépôt (champ plus large que la configuration globale), ou l'inverse. Le composant et les contrôleurs partagent désormais les mêmes limites (`UploadDND::FieldConstraints`)
57
+ - **Intégration importmap inopérante** : le moteur appelait `config.importmap.draw`, qui n'existe pas (`config.importmap` est un `OrderedOptions`, l'appel ne faisait rien). Une app sous importmap-rails n'obtenait aucun pin `upload_d_n_d` (`Failed to resolve module specifier "upload_d_n_d"`). Le moteur ajoute désormais son `config/importmap.rb` à `config.importmap.paths`. Avec Sprockets, le JS de la gem est aussi déclaré dans `upload_d_n_d_manifest.js` (sans quoi `AssetNotPrecompiledError`). Les apps qui chargent le JS par un bundler ne sont pas concernées
58
+ - **Fichiers `.jpeg` refusés par le navigateur** sur un champ déclaré `accept: [:jpg, :jpeg]` : seule la première extension d'un type MIME était proposée (`.jpg`). Toutes les extensions connues sont désormais listées
59
+
60
+ ### Added
61
+ - **Référence de champ signée** (`field_ref`) émise par le composant et renvoyée au dépôt : le serveur en déduit le modèle et l'attribut, et applique leurs limites. Sans référence (ancien client, appel direct), les limites globales restent appliquées
62
+ - **Adaptateur CanCanCan** (`authorization_adapter = :cancancan`) : `authorize! action, sujet`, avec les sujets génériques `:upload_d_n_d_attachment` et `:upload_d_n_d_temporary_attachment` (voir `docs/AUTHORIZATION.md`). La détection automatique s'appuie désormais sur `CanCan::Ability` : `CanCanCan` n'est pas défini par la gem cancancan
63
+ - **`authorization_adapter = :none`** : désactivation explicite de l'autorisation (seule l'authentification est vérifiée)
64
+ - **Avertissement au démarrage** quand l'autorisation est désactivée (`:none`) ou qu'aucun adaptateur n'est utilisable
65
+
66
+ ### Notes
67
+ - **Migration** :
68
+ - les apps avec Pundit ou CanCanCan détecté n'ont rien à changer : les policies et l'erreur `Pundit::NotAuthorizedError` relancée par `attach_file` et `remove_file` sont inchangées ;
69
+ - une app **sans librairie d'autorisation**, ou qui posait `config.authorization_adapter = nil`, voit désormais tous ses uploads refusés : déclarer `config.authorization_adapter = :none` pour retrouver le comportement précédent, ou installer Pundit ;
70
+ - avec `:none`, `attach_file` et `remove_file` n'appellent plus `authorize`, même si le contrôleur le définit.
71
+ - **Migration (statuts Turbo Stream)** : `attach_file`, `remove_file` et les erreurs Active Storage renvoient désormais un statut d'erreur (422 ou 404) sur le flux Turbo Stream, au lieu de 200. Une app qui testait un 200 sur ces échecs doit attendre le statut d'erreur ; le `fetch` du composant détecte déjà l'erreur par `response.ok`.
72
+ - **Migration (limites des champs)** :
73
+ - un fichier hors limites du champ est désormais refusé dès le dépôt (422) au lieu de l'être à l'enregistrement du formulaire ;
74
+ - sur un record existant, un champ sans `uploadable` reçoit les limites globales au dépôt (`config.max_size`, `config.accepted_types`), qui n'y étaient pas appliquées ;
75
+ - sur un formulaire de création, passer `record:` (un record neuf) à `upload_field` pour que les limites du modèle s'appliquent ; sans `record:`, ce sont les limites globales ;
76
+ - `accepted_types:` et `max_size:` passés à `upload_field` ne changent que l'affichage : une valeur plus large que le modèle est refusée par le serveur ;
77
+ - les apps qui appellent les endpoints sans passer par le composant gardent les limites globales sur les fichiers temporaires.
78
+ - L'ancienne méthode « autorisation personnalisée » de `docs/AUTHORIZATION.md` (surcharger `authorize_upload_d_n_d_create!` dans l'ApplicationController) n'a jamais été appelée par la gem : elle est retirée de la documentation
79
+
80
+ ## [0.7.3] - 2026-10-16
81
+
82
+ Correctif de sécurité (un fichier déjà rattaché ne peut plus être rattaché ailleurs) et purge des fichiers refusés après l'envoi direct. **Mise à jour recommandée.**
83
+
84
+ ### Security
85
+ - **Un fichier déjà rattaché pouvait être rattaché une seconde fois** : `POST /upload_d_n_d/temporary_attachments`, `POST /upload_d_n_d/attachments` et `Controllers::Uploadable#attach_file` acceptaient le `signed_id` d'un blob déjà rattaché à un autre record (ou gardé par un fichier temporaire). Ce `signed_id` figure dans chaque URL de fichier (`/rails/active_storage/blobs/...`) : un utilisateur qui l'obtenait pouvait rattacher le fichier d'un autre à ses propres records, et le fichier restait accessible même après sa suppression par son propriétaire. Un tel blob est désormais refusé (422, `upload_d_n_d.errors.blob_already_attached`), après l'autorisation et avant toute écriture
86
+
87
+ ### Fixed
88
+ - **Blobs orphelins à chaque refus** : avec DirectUpload, le fichier est déjà dans le bucket quand la gem le contrôle. Un refus (taille, type, MIME strict, validation du modèle, erreur à l'enregistrement du fichier temporaire ou de l'attachement) laissait le blob en place, et chaque nouvel essai en ajoutait un. Le blob refusé est désormais purgé en arrière-plan (`UploadDND::PurgeRejectedBlobJob`), sur `POST /upload_d_n_d/temporary_attachments`, `POST /upload_d_n_d/attachments` et `Controllers::Uploadable#attach_file`
89
+
90
+ ### Added
91
+ - **`config.rejected_blob_purge_window`** (1 heure par défaut) : seul un blob créé dans cette fenêtre est purgé au refus. `nil` désactive la purge
92
+
93
+ ### Notes
94
+ - Garde-fous : un blob rattaché à un record ou à un fichier temporaire n'est jamais supprimé, ni au refus ni à l'exécution du job (revérifié). Un blob plus ancien que la fenêtre est conservé (il peut être cité dans un HTML stocké). Rien n'est supprimé si l'autorisation n'a pas été explicitement accordée : refus (401, 403) ou autorisation qui n'aboutit pas (exception, policy absente)
95
+ - Le job utilise la file `active_storage.queues.purge`, comme `ActiveStorage::PurgeJob` : rien à configurer si la purge Active Storage fonctionne déjà
96
+ - Les blobs orphelins déjà présents ne sont pas concernés : leur nettoyage est prévu séparément
97
+ - **Pour les apps** : un `signed_id` accepté jusqu'ici peut désormais être refusé. Le composant n'envoie que des fichiers fraîchement déposés et n'est pas concerné. Une app qui réutilise volontairement un fichier existant (duplication d'un record par `signed_id`, par exemple) doit le faire hors des endpoints de la gem (`record.file.attach(blob)` côté serveur) — ou, pour une vraie copie, passer par `blob.open` + `attach(io:)`
98
+
99
+ ## [0.7.2] - 2026-10-16
100
+
101
+ Correctif de sécurité. **Mise à jour fortement recommandée depuis toutes les versions antérieures.**
102
+
103
+ ### Security
104
+ - **Appel de méthode arbitraire via `attribute_name`** : le nom d'attribut venu du client était résolu par `respond_to?` + `send`. Un nom comme `destroy`, `delete` ou `touch` passait le contrôle de format et était appelé sur le record : via `POST /upload_d_n_d/attachments`, tout utilisateur autorisé à uploader pouvait supprimer le record désigné par `record_gid`, quel que soit son modèle. Seuls les attachments déclarés (`has_one_attached` / `has_many_attached`) sont désormais résolus (`UploadDND::AttachmentLookup`), dans `AttachmentsController#create`, `Controllers::Uploadable#attach_file` et le transfert des fichiers temporaires (`Uploadable#attach_temporary_uploads`, `TemporaryAttachment.attach_to_record`)
105
+ - **Upload sur un record sans autorisation sur ce record** : `POST /upload_d_n_d/attachments` ne vérifiait que la policy générique `UploadDNDAttachmentPolicy#create?`. Tout utilisateur autorisé à uploader pouvait attacher un fichier à n'importe quel record et recevoir la liste de ses fichiers. La policy du record (`update?`) est maintenant vérifiée après la policy générique, comme pour la suppression
106
+
107
+ ### Notes
108
+ - Avec Pundit : un utilisateur dont la policy du record refuse `update?` reçoit désormais 403 à l'upload sur ce record. Les modèles sans policy gardent le comportement précédent (policy générique seule)
109
+ - Un `attribute_name` qui n'est pas un attachment du modèle renvoie 422 (comme un attribut inexistant auparavant). Un fichier temporaire portant un tel nom est supprimé au transfert, sans effet sur le record
110
+
111
+ ## [0.7.1] - 2026-08-26
112
+
113
+ Les fichiers déposés dans un formulaire de création survivent au rechargement de page, et l'app peut les purger à l'annulation.
114
+
115
+ ### Fixed
116
+ - **Upload impossible sur une session vierge** — sans cookie de session, `session.id` était nil : le premier upload d'un formulaire de création échouait en 422 (`session_id` vide), et l'id lu au rendu du composant ne correspondait pas à celui des uploads. `UploadDND::SessionId.for(session)` amorce désormais la session avant de lire l'id ; utilisé partout dans la gem et dans les templates du générateur (`upload_session_field`, concern, authorization)
117
+ - **Composant : zone vide après rechargement** — sans record persisté, le composant rend maintenant les fichiers temporaires de la session courante (`UploadComponent#files` existait mais n'était jamais appelé par le template). F5, restauration de brouillon ou reconnexion n'affichent plus une zone vide alors que les fichiers sont côté serveur
118
+
119
+ ### Added
120
+ - **`GET /upload_d_n_d/temporary_attachments?attribute_name=x`** : HTML de la liste des fichiers temporaires de la session, pour les apps qui remplacent la zone hors composant
121
+ - **`DELETE /upload_d_n_d/temporary_attachments?attribute_name=x`** : purge des fichiers temporaires de la session pour cet attribut (bouton « Annuler »), 204. Soumis à la policy `destroy?` si Pundit est activé
122
+
123
+ ### Notes
124
+ - Les fichiers d'un formulaire abandonné sans purge réapparaissent dans tout formulaire suivant de la session utilisant le même `attribute_name`, jusqu'à `temporary_files_retention`. Auparavant ils étaient invisibles mais attachés quand même au submit
125
+
126
+ ## [0.7.0] - 2026-07-17
127
+
128
+ La suppression de fichiers passe par le moteur de la gem, comme l'upload : **plus aucune route requise côté app**, quel que soit le routing du record (imbriqué, singulier, namespacé). Les `resolve()` déclarés pour la gem deviennent inutiles (ils restent utiles au routing polymorphique du reste de votre app).
129
+
130
+ ### Changed
131
+ - **⚠️ `config.delete_route` par défaut `:engine`** : l'UI de suppression appelle `DELETE /upload_d_n_d/attachments/:id` (moteur) au lieu de la route membre `remove_file` de l'app. Les actions `remove_file` custom ne sont plus appelées par l'UI — branchez `config.on_file_removed` ou repassez en `config.delete_route = :host` (flux legacy inchangé)
132
+ - `UploadDND::UnroutableRecordError` et le pattern `resolve()` ne concernent plus que le mode `:host`
133
+
134
+ ### Added
135
+ - **`AttachmentsController#destroy`** : suppression par `record_gid`, autorisation fail-closed via la policy Pundit du record (`update?`) avec fallback sur la policy générique de la gem, vérification d'appartenance attachment ↔ record ↔ attribut
136
+ - **`config.on_file_removed`** : callback `->(record, attribute_name, user)` après suppression
137
+ - **`config.delete_route`** : `:engine` (défaut) / `:host` (legacy)
138
+
139
+ ## [0.6.2] - 2026-07-17
140
+
141
+ Support des records en resources imbriquées ou singulières (ex. `resource :convention` sous `resources :projects`).
142
+
143
+ ### Added
144
+ - **`UploadDND::UnroutableRecordError`** : quand un record affiché dans la liste de fichiers n'a pas de route polymorphique, la gem lève une erreur explicite contenant le remède (déclarer `resolve()` dans `config/routes.rb`, exemple inclus) au lieu d'un `undefined method 'xxx_path'` cryptique
145
+ - **Helper `udnd_remove_file_url(record, attachment, attribute_name)`** : point d'extension officiel pour l'URL de suppression, surchargeable dans le `ApplicationHelper` de l'app — ne shadowez plus le partial `_files_list` pour ça
146
+ - Section README « Records imbriqués ou resources singulières » (pattern `resolve`)
147
+
148
+ ### Fixed
149
+ - **`authorize_record_for_update` : fallback sur la policy top-level** quand la policy namespacée n'existe pas. Le concern cherchait la policy dans le namespace du controller (`Projects::ConventionsController` → `Projects::ConventionPolicy`) alors qu'un `module:` de routes n'implique pas un namespace de policies — `Pundit::NotDefinedError` déclenche désormais une seconde tentative sur la policy top-level (`ConventionPolicy`). Les autres erreurs d'autorisation ne sont pas avalées.
150
+
151
+ ## [0.6.1] - 2026-07-09
152
+
153
+ ### Security
154
+ - **Fail-open d'autorisation dans `Controllers::Uploadable`** : `authorize_record_for_update` testait `respond_to?(:authorize)` sans `include_all: true` — `Pundit::Authorization#authorize` étant privée, le check échouait et `attach_file`/`remove_file` s'exécutaient sans aucune vérification d'autorisation (puis 500 `verify_authorized` après exécution). Mise à jour fortement recommandée depuis 0.6.0 et antérieures.
155
+
156
+ ## [0.6.0] - 2026-07-09
157
+
158
+ Version issue d'un audit sécurité/qualité de la gem.
159
+
160
+ ### Security
161
+ - **Modale de prévisualisation réécrite en DOM APIs** : corrige un XSS possible via un nom de fichier forgé interpolé dans `innerHTML` (breakout d'attribut)
162
+ - **Rate limiting** : compteur atomique via `Rails.cache.increment` (le read + write laissait passer des requêtes concurrentes sous la limite)
163
+ - **README** : nouvelle documentation sécurité — protection de l'endpoint DirectUpload (`/rails/active_storage/direct_uploads`, hors périmètre du rate limiting de la gem) et dimensionnement du rate limiting
164
+
165
+ ### Fixed
166
+ - **Boutons des modales alert/confirm inertes** : les `data-action` posés sur du DOM appendu à `document.body` n'étaient jamais bindés par Stimulus (hors scope du controller) — remplacés par des event listeners directs ; le clic sur l'overlay pour fermer fonctionne désormais
167
+ - Preview PDF via `<object>` avec fallback téléchargement natif (iframe vide sur Safari mobile auparavant)
168
+ - Suppression des styles inline injectés en JS (compatibilité CSP `style-src` stricte)
169
+ - `delete_url` des listes de fichiers construit via `polymorphic_path` (respecte namespaces, scopes de locale, `relative_url_root` ; échappe les query params)
170
+ - Validation d'extension côté client en comparaison exacte (`.jpe` passait si `.jpeg` était accepté)
171
+
172
+ ### Added
173
+ - **URLs de blobs via `resolve_model_to_route`** : la gem suit le réglage `config.active_storage.resolve_model_to_route` de l'app hôte — passez-le à `:rails_storage_proxy` pour servir les blobs en same-origin (CSP stricte, bucket S3-like externe), sans configuration côté gem
174
+ - **`UploadDND::CleanupTemporaryAttachmentsJob`** : job de purge des fichiers temporaires expirés embarqué dans la gem (plus besoin de l'écrire côté app, il ne reste que la planification)
175
+ - Accessibilité de la modale de prévisualisation : `role="dialog"`, `aria-modal`, `aria-labelledby`, focus trap, retour du focus au déclencheur
176
+ - i18n des libellés de la modale de prévisualisation (fr + en)
177
+ - Fermeture des modales sur `turbo:before-visit` (plus de modale orpheline après navigation)
178
+
179
+ ### Changed
180
+ - Icônes de fichiers centralisées dans le partial `upload_d_n_d/shared/_file_icon` — surchargeable par l'app hôte en le shadowant
181
+
182
+ ## [0.5.0] - 2026-03-26
183
+
184
+ Refonte majeure de l'architecture : tous les assets (JS, CSS, ViewComponent) sont désormais servis directement depuis la gem. Plus besoin de copier des fichiers dans l'app hôte. Un simple `bundle update` suffit pour tout mettre à jour.
185
+
186
+ ### Changed
187
+ - **Contrôleurs Stimulus** : servis depuis la gem via importmap, auto-enregistrés avec `import "upload_d_n_d"`
188
+ - **CSS** : servi depuis la gem via l'asset pipeline (`stylesheet_link_tag "upload_d_n_d/application"`)
189
+ - **ViewComponent** : servi directement depuis la gem (plus copié dans l'app)
190
+ - **Install generator** : ne copie plus JS, CSS ni Component ; affiche uniquement les instructions d'import
191
+ - **POST_INSTALL** : mis à jour pour refléter la nouvelle architecture
192
+
193
+ ### Removed
194
+ - `VersionTracker` : plus besoin de tracker les versions des fichiers copiés (il n'y en a plus)
195
+ - `UpdateGenerator` (`rails g upload_d_n_d:update`) : plus nécessaire
196
+ - Fichier `.upload_d_n_d_version` dans l'app hôte : plus nécessaire
197
+ - Documentation `UPDATE_SYSTEM.md` : obsolète
198
+
199
+ ### Added
200
+ - Auto-enregistrement des contrôleurs Stimulus via `window.Stimulus`
201
+ - Pin importmap `upload_d_n_d` comme point d'entrée unique
202
+ - Détection et nettoyage des anciens fichiers copiés (STALE_OVERRIDES étendu)
203
+ - Specs du install generator (20 tests)
204
+ - Specs d'intégration engine assets (14 tests)
205
+
206
+ ### Migration depuis v0.4.x
207
+ Voir le [guide de migration](MIGRATION_0.5.md) pour les instructions détaillées.
208
+
209
+ ## [0.4.1] - 2026-03-25
210
+
211
+ ### Changed
212
+ - Bump version, require Ruby >= 3.2
213
+
214
+ ## [0.4.0] - 2026-03-24
215
+
216
+ ### Changed
217
+ - Les modules critiques (`Uploadable`, `Controllers::Uploadable`, `FileValidator`) sont fournis par le runtime de la gem
218
+ - Les vues sont servies directement depuis la gem
219
+ - Remplacement des icônes Font Awesome par des SVG inline Lucide
220
+
221
+ ### Added
222
+ - Système `VersionTracker` + `UpdateGenerator` pour synchroniser les fichiers copiés
223
+ - Nettoyage automatique des anciennes copies (concerns) lors de l'installation
224
+
225
+ ## [0.1.0] - 2026-01-05
226
+
227
+ Première version stable de UploadDND, un système de drag & drop sécurisé pour Rails.
228
+
229
+ ### Added
230
+
231
+ #### Fonctionnalités principales
232
+ - Interface drag & drop intuitive avec barre de progression
233
+ - Support multi-fichiers (has_one_attached et has_many_attached)
234
+ - Upload temporaire pour les formulaires de création (avant persistance du modèle)
235
+ - Prévisualisation des fichiers (images et PDF) côté client
236
+
237
+ #### Validation et sécurité
238
+ - Validation sécurisée côté serveur (MIME type, taille, nombre de fichiers)
239
+ - Protection contre le spoofing de fichiers via détection MIME avec Marcel
240
+ - Protection CSRF automatique
241
+ - Isolation des fichiers temporaires par session
242
+ - Sanitization des noms de fichiers
243
+ - Vérification des signatures de blob
244
+
245
+ #### Autorisation
246
+ - Support flexible d'autorisation (Pundit, CanCanCan, ou custom)
247
+ - Générateur de politique Pundit
248
+ - Concern `UploadDND::Controllers::UploadAuthorization` pour les contrôleurs
249
+
250
+ #### Configuration
251
+ - Configuration globale et par composant
252
+ - Module `MimeTypeHelper` avec support de 40+ extensions (images, documents, audio, vidéo, archives)
253
+ - Conversion automatique des extensions de fichiers en MIME types (ex: `jpg` → `image/jpeg`)
254
+ - Styles CSS personnalisables avec variables CSS
255
+ - Callbacks personnalisables (on_upload_success, on_upload_error)
256
+
257
+ #### Intégration Rails
258
+ - Compatible Rails 7 et 8
259
+ - Concern `UploadDND::Uploadable` pour les modèles
260
+ - Concern `UploadDND::Controllers::Uploadable` pour les contrôleurs
261
+ - Helper `upload_field` pour les vues
262
+ - Contrôleur Stimulus pour le drag & drop
263
+ - Modèle TemporaryAttachment pour les uploads temporaires
264
+ - Support automatique de Stimulus et des assets
265
+
266
+ #### Générateurs
267
+ - Générateur d'installation avec configuration automatique
268
+ - Installation des dépendances (view_component, stimulus-rails)
269
+ - Migration pour les fichiers temporaires
270
+ - Copie des concerns, helpers et contrôleurs Stimulus
271
+ - Styles CSS (compatible Tailwind et Asset Pipeline)
272
+ - Configuration initiale
273
+ - Générateur de modèle
274
+ - Support des namespaces (Admin::Article, Manager::Report, etc.)
275
+ - Options configurables (--multiple, --content_type, --max_size, --max_files)
276
+ - Génération optionnelle de controller et vues
277
+ - Détection intelligente des attributs uploadables existants
278
+
279
+ #### Documentation
280
+ - README complet avec exemples
281
+ - Guide d'autorisation détaillé
282
+ - Exemples de générateurs
283
+ - Documentation de test
284
+
285
+ ### Security
286
+ - Validation des paramètres pour empêcher l'injection
287
+ - Protection CSRF automatique
288
+ - Isolation des fichiers temporaires par session
289
+ - Vérification des signatures de blob
290
+ - Détection de spoofing MIME avec Marcel
291
+ - Sanitization des noms de fichiers
292
+ - Vérification de propriété des fichiers avant suppression
@@ -0,0 +1,49 @@
1
+ ===============================================================================
2
+
3
+ Policy Pundit générée avec succès !
4
+
5
+ Fichier créé :
6
+ app/policies/upload_d_n_d_temporary_attachment_policy.rb
7
+
8
+ Prochaines étapes :
9
+
10
+ 1. Personnalisez les méthodes d'autorisation dans la policy générée
11
+ selon vos besoins (admin, roles, permissions, etc.)
12
+
13
+ 2. Activez Pundit dans votre configuration UploadDND :
14
+
15
+ # config/initializers/upload_d_n_d.rb
16
+ UploadDND.configure do |config|
17
+ config.authorization_adapter = :pundit
18
+ end
19
+
20
+ 3. Assurez-vous que Pundit est bien configuré dans votre ApplicationController :
21
+
22
+ class ApplicationController < ActionController::Base
23
+ include Pundit::Authorization
24
+ end
25
+
26
+ 4. (Optionnel) Testez votre policy :
27
+
28
+ # spec/policies/upload_d_n_d_temporary_attachment_policy_spec.rb
29
+ require 'rails_helper'
30
+
31
+ RSpec.describe UploadDNDTemporaryAttachmentPolicy do
32
+ subject { described_class.new(user, :upload_d_n_d_temporary_attachment) }
33
+
34
+ context 'for an admin user' do
35
+ let(:user) { create(:user, :admin) }
36
+
37
+ it { is_expected.to permit_action(:create) }
38
+ it { is_expected.to permit_action(:destroy) }
39
+ end
40
+
41
+ context 'for a regular user' do
42
+ let(:user) { create(:user) }
43
+
44
+ it { is_expected.to forbid_action(:create) }
45
+ it { is_expected.to forbid_action(:destroy) }
46
+ end
47
+ end
48
+
49
+ ===============================================================================
@@ -0,0 +1,80 @@
1
+ Description:
2
+ Configure un modèle existant pour utiliser UploadDND avec drag & drop.
3
+ Génère (si absents) les vues et controllers nécessaires, et affiche
4
+ des snippets à copier/coller pour configurer votre modèle, controller et routes.
5
+
6
+ Fonctionne avec ou sans namespace (User, Admin::Article, Manager::Report, etc.)
7
+
8
+ Exemples:
9
+
10
+ # Configuration basique pour un avatar sur User
11
+ rails generate upload_d_n_d:model User --attribute=avatar
12
+
13
+ # Upload multiple d'images sur Admin::Article
14
+ rails generate upload_d_n_d:model Admin::Article --attribute=images --multiple
15
+
16
+ # PDF avec taille max personnalisée
17
+ rails generate upload_d_n_d:model Manager::Report --attribute=pdf_file \
18
+ --content_type=application/pdf --max_size=50
19
+
20
+ # Images avec nombre max de fichiers
21
+ rails generate upload_d_n_d:model Product --attribute=photos --multiple \
22
+ --max_files=10 --content_type=image/png image/jpeg image/webp
23
+
24
+ # Sans générer le controller (si déjà existant)
25
+ rails generate upload_d_n_d:model User --attribute=avatar --no-controller
26
+
27
+ # Sans générer les vues (si déjà existantes)
28
+ rails generate upload_d_n_d:model User --attribute=avatar --no-views
29
+
30
+ Ce qui est généré:
31
+
32
+ Modèle:
33
+ - Snippet pour include UploadDND::Uploadable
34
+ - Snippet pour has_one_attached / has_many_attached
35
+ - Snippet pour uploadable (validations)
36
+
37
+ Controller (optionnel):
38
+ - Création ou mise à jour du controller
39
+ - Support pour les uploads temporaires
40
+ - Actions CRUD de base
41
+ - Si le controller existe déjà, le générateur affiche les modifications à faire
42
+
43
+ Vues (optionnel):
44
+ - Partial de formulaire avec upload_field
45
+ - Partial d'affichage des fichiers uploadés
46
+ - Gestion upload temporaire vs record existant
47
+
48
+ Options disponibles:
49
+
50
+ --attribute=ATTRIBUTE (obligatoire, alias: -a)
51
+ Nom de l'attribut uploadable (ex: avatar, documents, photos)
52
+
53
+ --multiple
54
+ Permet l'upload de plusieurs fichiers (has_many_attached)
55
+ Par défaut: false (has_one_attached)
56
+
57
+ --content_type=TYPE [TYPE...]
58
+ Types MIME acceptés (peut être multiple)
59
+ Par défaut: image/png image/jpeg
60
+ Exemples:
61
+ --content_type=application/pdf
62
+ --content_type=image/png image/jpeg image/webp
63
+
64
+ --max_size=SIZE
65
+ Taille maximale en MB
66
+ Par défaut: 5
67
+ Exemple: --max_size=10
68
+
69
+ --max_files=NUMBER
70
+ Nombre maximum de fichiers (seulement pour --multiple)
71
+ Par défaut: nil (illimité)
72
+ Exemple: --max_files=5
73
+
74
+ --no-controller
75
+ Ne pas générer/modifier le controller
76
+ Par défaut: génère le controller
77
+
78
+ --no-views
79
+ Ne pas générer les vues
80
+ Par défaut: génère les vues
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/base"
5
+
6
+ require "rails/generators/active_record"
7
+
8
+ module UploadDND
9
+ module Generators
10
+ class InstallGenerator < Rails::Generators::Base
11
+ include Rails::Generators::Migration
12
+
13
+ source_root File.expand_path("templates", __dir__)
14
+
15
+ def self.next_migration_number(dirname)
16
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
17
+ end
18
+
19
+ desc "Installe UploadDND dans votre application Rails"
20
+
21
+ def check_dependencies
22
+ say "Vérification des dépendances...", :green
23
+ end
24
+
25
+ def add_view_component_gem
26
+ return if gem_installed?("view_component")
27
+
28
+ say "Ajout de view_component au Gemfile...", :yellow
29
+ gem "view_component", "~> 3.0"
30
+
31
+ say "N'oubliez pas de lancer 'bundle install'", :green
32
+ end
33
+
34
+ def add_stimulus_rails
35
+ return if gem_installed?("stimulus-rails")
36
+
37
+ say "Ajout de stimulus-rails au Gemfile...", :yellow
38
+ gem "stimulus-rails"
39
+
40
+ say "N'oubliez pas de lancer 'bundle install' puis 'rails stimulus:install'", :green
41
+ end
42
+
43
+ def copy_concerns
44
+ say "Installation des concerns...", :green
45
+
46
+ # Les concerns critiques (Uploadable, Controllers::Uploadable, FileValidator)
47
+ # sont désormais fournis par le runtime de la gem (lib/).
48
+ # Seul upload_authorization reste un template car il est personnalisable.
49
+ say " ✓ UploadDND::Uploadable → fourni par la gem (lib/)", :cyan
50
+ say " ✓ UploadDND::Controllers::Uploadable → fourni par la gem (lib/)", :cyan
51
+ say " ✓ UploadDND::FileValidator → fourni par la gem (lib/)", :cyan
52
+
53
+ template "concerns/upload_authorization.rb",
54
+ "app/controllers/concerns/upload_d_n_d/upload_authorization.rb"
55
+
56
+ # Nettoyage : supprime les anciennes copies si elles existent (migration depuis v0.3.x)
57
+ remove_stale_overrides
58
+ end
59
+
60
+ def copy_helpers
61
+ say "Installation des helpers...", :green
62
+
63
+ copy_file "helpers/upload_helper.rb",
64
+ "app/helpers/upload_d_n_d/upload_helper.rb"
65
+ end
66
+
67
+ def setup_stimulus
68
+ say "Configuration Stimulus...", :green
69
+ say " ✓ Controllers Stimulus → servis depuis la gem via importmap", :cyan
70
+ say ""
71
+ say "Ajoutez cette ligne dans votre application.js :", :yellow
72
+ say ' import "upload_d_n_d"', :cyan
73
+ say ""
74
+ end
75
+
76
+ def create_migration_file
77
+ say "Création de la migration pour TemporaryAttachment...", :green
78
+
79
+ migration_template "migrations/create_upload_d_n_d_temporary_attachments.rb",
80
+ "db/migrate/create_upload_d_n_d_temporary_attachments.rb"
81
+ end
82
+
83
+ def add_routes
84
+ say "Ajout des routes...", :green
85
+
86
+ route_content = <<~RUBY
87
+
88
+ # Routes UploadDND
89
+ mount UploadDND::Engine => "/upload_d_n_d"
90
+ RUBY
91
+
92
+ route route_content
93
+ end
94
+
95
+ def create_initializer
96
+ say "Création du fichier de configuration...", :green
97
+
98
+ template "initializers/upload_d_n_d.rb",
99
+ "config/initializers/upload_d_n_d.rb"
100
+ end
101
+
102
+ def copy_views
103
+ # Les vues, le ViewComponent, les JS et le CSS sont servis directement depuis la gem.
104
+ say "Assets UploadDND: servis directement depuis la gem (rien copié dans l'app).", :green
105
+ end
106
+
107
+ def setup_styles
108
+ say ""
109
+ say "🧩 Styles UploadDND", :yellow
110
+ say "Le CSS est servi directement depuis la gem.", :cyan
111
+ say ""
112
+ say "Ajoutez dans votre layout :", :yellow
113
+ say ' <%= stylesheet_link_tag "upload_d_n_d/application", media: "all" %>', :cyan
114
+ say ""
115
+ end
116
+
117
+ def generate_policy_if_pundit
118
+ if defined?(Pundit) || File.read("Gemfile").include?("pundit")
119
+ say "Pundit détecté, génération de la policy...", :green
120
+
121
+ if yes?("Voulez-vous générer la policy Pundit pour UploadDND ? (y/n)")
122
+ generate "upload_d_n_d:policy"
123
+ end
124
+ end
125
+ end
126
+
127
+ def show_readme
128
+ readme "POST_INSTALL" if File.exist?(source_paths.first + "/POST_INSTALL")
129
+ end
130
+
131
+ private
132
+
133
+ # Supprime les fichiers qui étaient copiés par les anciennes versions (< 0.4.0)
134
+ # et qui shadowent désormais le runtime de la gem.
135
+ STALE_OVERRIDES = [
136
+ "app/models/concerns/upload_d_n_d/uploadable.rb",
137
+ "app/controllers/concerns/upload_d_n_d/controllers/uploadable.rb",
138
+ "app/validators/file_validator.rb",
139
+ # Fichiers copiés par les versions < 0.5.0 (désormais servis depuis la gem)
140
+ "app/javascript/controllers/upload_d_n_d/uploads_controller.js",
141
+ "app/javascript/controllers/upload_d_n_d/preview_controller.js",
142
+ "app/assets/stylesheets/upload_d_n_d.css",
143
+ "app/components/upload_d_n_d/upload_component.rb",
144
+ "app/components/upload_d_n_d/upload_component.html.erb"
145
+ ].freeze
146
+
147
+ def remove_stale_overrides
148
+ stale = STALE_OVERRIDES.select { |f| File.exist?(f) }
149
+ return if stale.empty?
150
+
151
+ say ""
152
+ say "⚠️ Anciennes copies détectées (désormais servies depuis la gem) :", :yellow
153
+ stale.each { |f| say " • #{f}", :yellow }
154
+ say ""
155
+
156
+ if yes?("Supprimer ces fichiers obsolètes ? (recommandé) (y/n)")
157
+ stale.each do |f|
158
+ remove_file f
159
+ say " ✓ #{f} supprimé", :green
160
+ end
161
+ else
162
+ say " ⚠️ Conservées. Attention : elles prennent priorité sur la gem.", :red
163
+ end
164
+ say ""
165
+ end
166
+
167
+ def gem_installed?(gem_name)
168
+ File.read("Gemfile").include?(gem_name)
169
+ rescue
170
+ false
171
+ end
172
+ end
173
+ end
174
+ end