@programisto/edrm-storage 1.0.4 → 1.0.6

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.
package/README.md CHANGED
@@ -21,7 +21,7 @@ The Endurance Framework is a highly modular and scalable Node.js project templat
21
21
 
22
22
  ### Installation
23
23
 
24
- 1. Install our CLI:
24
+ 1. Install our CLI (optionnal):
25
25
 
26
26
  ```sh
27
27
  npm install -g endurance
@@ -40,6 +40,7 @@ export interface IFile {
40
40
  tenantId?: string;
41
41
  entityName?: string;
42
42
  entityId?: string;
43
+ portalEntityId?: string;
43
44
  uploadedBy?: string;
44
45
  expiresAt?: Date;
45
46
  lastAccessedAt?: Date;
@@ -65,6 +66,8 @@ declare class File extends EnduranceSchema implements IFile {
65
66
  tenantId: string;
66
67
  entityName: string;
67
68
  entityId: string;
69
+ /** Identifiant de l'entité du portail (multi-entités). Optionnel pour rétrocompatibilité. */
70
+ portalEntityId: string;
68
71
  uploadedBy: string;
69
72
  expiresAt: Date;
70
73
  lastAccessedAt: Date;
@@ -56,6 +56,8 @@ let File = class File extends EnduranceSchema {
56
56
  tenantId;
57
57
  entityName;
58
58
  entityId;
59
+ /** Identifiant de l'entité du portail (multi-entités). Optionnel pour rétrocompatibilité. */
60
+ portalEntityId;
59
61
  uploadedBy;
60
62
  expiresAt;
61
63
  lastAccessedAt;
@@ -140,6 +142,10 @@ __decorate([
140
142
  EnduranceModelType.prop({ required: false }),
141
143
  __metadata("design:type", String)
142
144
  ], File.prototype, "entityId", void 0);
145
+ __decorate([
146
+ EnduranceModelType.prop({ required: false }),
147
+ __metadata("design:type", String)
148
+ ], File.prototype, "portalEntityId", void 0);
143
149
  __decorate([
144
150
  EnduranceModelType.prop({ required: false }),
145
151
  __metadata("design:type", String)
@@ -14,7 +14,60 @@ class EdrmStorageRouter extends EnduranceRouter {
14
14
  const userOptions = {
15
15
  requireAuth: true
16
16
  };
17
- // Routes publiques pour l'initialisation d'upload
17
+ /**
18
+ * @swagger
19
+ * /files/init:
20
+ * post:
21
+ * summary: Initialiser un upload de fichier
22
+ * description: Crée un enregistrement de fichier et retourne une URL signée pour commencer l’upload. Authentification utilisateur requise.
23
+ * tags: [Stockage]
24
+ * requestBody:
25
+ * required: true
26
+ * content:
27
+ * application/json:
28
+ * schema:
29
+ * type: object
30
+ * required: [originalName, mimeType, size, tenantId, entityName, entityId]
31
+ * properties:
32
+ * originalName:
33
+ * type: string
34
+ * description: Nom d’origine du fichier
35
+ * mimeType:
36
+ * type: string
37
+ * description: Type MIME du fichier
38
+ * size:
39
+ * type: integer
40
+ * description: Taille du fichier en octets
41
+ * tenantId:
42
+ * type: string
43
+ * description: Identifiant du tenant
44
+ * entityName:
45
+ * type: string
46
+ * description: Entité métier associée (ex. "invoice")
47
+ * entityId:
48
+ * type: string
49
+ * description: Identifiant de l’entité métier associée
50
+ * provider:
51
+ * type: string
52
+ * enum: [S3]
53
+ * default: S3
54
+ * description: Provider de stockage
55
+ * metadata:
56
+ * type: object
57
+ * description: Métadonnées supplémentaires
58
+ * tags:
59
+ * type: array
60
+ * items:
61
+ * type: string
62
+ * description: Liste de tags libres
63
+ * responses:
64
+ * 200:
65
+ * description: URL signée et informations d’initialisation
66
+ * 400:
67
+ * description: Paramètres manquants ou invalides
68
+ * 500:
69
+ * description: Erreur serveur
70
+ */
18
71
  this.post('/files/init', userOptions, async (req, res) => {
19
72
  try {
20
73
  const { originalName, mimeType, size, tenantId, entityName, entityId, provider = 'S3', metadata, tags } = req.body;
@@ -24,7 +77,7 @@ class EdrmStorageRouter extends EnduranceRouter {
24
77
  message: 'Paramètres manquants: originalName, mimeType, size, tenantId, entityName, entityId'
25
78
  });
26
79
  }
27
- const result = await this.storageService.initUpload(originalName, mimeType, size, tenantId, entityName, entityId, provider, metadata, tags);
80
+ const result = await this.storageService.initUpload(originalName, mimeType, size, tenantId, entityName, entityId, provider, metadata, tags, req.entity?._id?.toString());
28
81
  return res.json({
29
82
  success: true,
30
83
  data: result
@@ -38,11 +91,30 @@ class EdrmStorageRouter extends EnduranceRouter {
38
91
  });
39
92
  }
40
93
  });
41
- // Finaliser un upload
94
+ /**
95
+ * @swagger
96
+ * /files/{fileId}/complete:
97
+ * post:
98
+ * summary: Finaliser un upload
99
+ * description: Marque l’upload comme terminé et met à jour le statut du fichier. Authentification utilisateur requise.
100
+ * tags: [Stockage]
101
+ * parameters:
102
+ * - in: path
103
+ * name: fileId
104
+ * required: true
105
+ * schema:
106
+ * type: string
107
+ * description: Identifiant du fichier
108
+ * responses:
109
+ * 200:
110
+ * description: Upload finalisé avec succès
111
+ * 500:
112
+ * description: Erreur serveur
113
+ */
42
114
  this.post('/files/:fileId/complete', userOptions, async (req, res) => {
43
115
  try {
44
116
  const { fileId } = req.params;
45
- const result = await this.storageService.completeUpload(fileId);
117
+ const result = await this.storageService.completeUpload(fileId, req.entity?._id?.toString());
46
118
  return res.json({
47
119
  success: true,
48
120
  data: result
@@ -56,12 +128,42 @@ class EdrmStorageRouter extends EnduranceRouter {
56
128
  });
57
129
  }
58
130
  });
59
- // Obtenir une URL de téléchargement
131
+ /**
132
+ * @swagger
133
+ * /files/{fileId}/download:
134
+ * get:
135
+ * summary: Obtenir une URL de téléchargement
136
+ * description: Retourne une URL signée pour télécharger le fichier. Authentification utilisateur requise.
137
+ * tags: [Stockage]
138
+ * parameters:
139
+ * - in: path
140
+ * name: fileId
141
+ * required: true
142
+ * schema:
143
+ * type: string
144
+ * description: Identifiant du fichier
145
+ * - in: query
146
+ * name: filename
147
+ * schema:
148
+ * type: string
149
+ * description: Nom de fichier à proposer au téléchargement
150
+ * - in: query
151
+ * name: expiresIn
152
+ * schema:
153
+ * type: integer
154
+ * default: 3600
155
+ * description: Durée de validité du lien en secondes
156
+ * responses:
157
+ * 200:
158
+ * description: URL de téléchargement générée
159
+ * 500:
160
+ * description: Erreur serveur
161
+ */
60
162
  this.get('/files/:fileId/download', userOptions, async (req, res) => {
61
163
  try {
62
164
  const { fileId } = req.params;
63
165
  const { filename, expiresIn = 3600 } = req.query;
64
- const result = await this.storageService.getDownloadUrl(fileId, filename, parseInt(expiresIn));
166
+ const result = await this.storageService.getDownloadUrl(fileId, filename, parseInt(expiresIn), req.entity?._id?.toString());
65
167
  return res.json({
66
168
  success: true,
67
169
  data: result
@@ -75,11 +177,30 @@ class EdrmStorageRouter extends EnduranceRouter {
75
177
  });
76
178
  }
77
179
  });
78
- // Supprimer un fichier
180
+ /**
181
+ * @swagger
182
+ * /files/{fileId}:
183
+ * delete:
184
+ * summary: Supprimer un fichier
185
+ * description: Supprime un fichier et ses métadonnées. Authentification et permission Admin_ManageFiles requises.
186
+ * tags: [Stockage]
187
+ * parameters:
188
+ * - in: path
189
+ * name: fileId
190
+ * required: true
191
+ * schema:
192
+ * type: string
193
+ * description: Identifiant du fichier
194
+ * responses:
195
+ * 200:
196
+ * description: Fichier supprimé
197
+ * 500:
198
+ * description: Erreur serveur
199
+ */
79
200
  this.delete('/files/:fileId', adminOptions, async (req, res) => {
80
201
  try {
81
202
  const { fileId } = req.params;
82
- await this.storageService.deleteFile(fileId);
203
+ await this.storageService.deleteFile(fileId, req.entity?._id?.toString());
83
204
  return res.json({
84
205
  success: true,
85
206
  message: 'Fichier supprimé avec succès'
@@ -93,11 +214,57 @@ class EdrmStorageRouter extends EnduranceRouter {
93
214
  });
94
215
  }
95
216
  });
96
- // Lister les fichiers
217
+ /**
218
+ * @swagger
219
+ * /files:
220
+ * get:
221
+ * summary: Lister les fichiers
222
+ * description: Retourne une liste paginée des fichiers avec filtres. Authentification et permission Admin_ManageFiles requises.
223
+ * tags: [Stockage]
224
+ * parameters:
225
+ * - in: query
226
+ * name: tenantId
227
+ * schema:
228
+ * type: string
229
+ * description: Filtrer par tenant
230
+ * - in: query
231
+ * name: entityName
232
+ * schema:
233
+ * type: string
234
+ * description: Filtrer par entité
235
+ * - in: query
236
+ * name: entityId
237
+ * schema:
238
+ * type: string
239
+ * description: Filtrer par identifiant d’entité
240
+ * - in: query
241
+ * name: status
242
+ * schema:
243
+ * type: string
244
+ * enum: [PENDING, COMPLETED, FAILED]
245
+ * description: Filtrer par statut de fichier
246
+ * - in: query
247
+ * name: page
248
+ * schema:
249
+ * type: integer
250
+ * default: 1
251
+ * description: Numéro de page
252
+ * - in: query
253
+ * name: limit
254
+ * schema:
255
+ * type: integer
256
+ * default: 20
257
+ * description: Nombre d’éléments par page
258
+ * responses:
259
+ * 200:
260
+ * description: Liste paginée de fichiers
261
+ * 500:
262
+ * description: Erreur serveur
263
+ */
97
264
  this.get('/files', adminOptions, async (req, res) => {
98
265
  try {
99
266
  const { tenantId, entityName, entityId, status, page = 1, limit = 20 } = req.query;
100
- const result = await this.storageService.listFiles(tenantId, entityName, entityId, status, parseInt(page), parseInt(limit));
267
+ const result = await this.storageService.listFiles(tenantId, entityName, entityId, status, parseInt(page), parseInt(limit), req.entity?._id?.toString());
101
268
  return res.json({
102
269
  success: true,
103
270
  data: result
@@ -111,11 +278,30 @@ class EdrmStorageRouter extends EnduranceRouter {
111
278
  });
112
279
  }
113
280
  });
114
- // Obtenir les détails d'un fichier
281
+ /**
282
+ * @swagger
283
+ * /files/{fileId}:
284
+ * get:
285
+ * summary: Récupérer un fichier par ID
286
+ * description: Retourne les métadonnées d’un fichier. Authentification utilisateur requise.
287
+ * tags: [Stockage]
288
+ * parameters:
289
+ * - in: path
290
+ * name: fileId
291
+ * required: true
292
+ * schema:
293
+ * type: string
294
+ * description: Identifiant du fichier
295
+ * responses:
296
+ * 200:
297
+ * description: Détails du fichier
298
+ * 500:
299
+ * description: Erreur serveur
300
+ */
115
301
  this.get('/files/:fileId', userOptions, async (req, res) => {
116
302
  try {
117
303
  const { fileId } = req.params;
118
- const result = await this.storageService.getFileById(fileId);
304
+ const result = await this.storageService.getFileById(fileId, req.entity?._id?.toString());
119
305
  return res.json({
120
306
  success: true,
121
307
  data: result
@@ -129,7 +315,26 @@ class EdrmStorageRouter extends EnduranceRouter {
129
315
  });
130
316
  }
131
317
  });
132
- // Route de callback pour les événements S3 (optionnel)
318
+ /**
319
+ * @swagger
320
+ * /files/s3-callback:
321
+ * post:
322
+ * summary: Callback d’événements S3
323
+ * description: Réception et traitement des webhooks S3. Authentification non requise.
324
+ * tags: [Stockage]
325
+ * requestBody:
326
+ * required: false
327
+ * content:
328
+ * application/json:
329
+ * schema:
330
+ * type: object
331
+ * description: Payload du webhook S3
332
+ * responses:
333
+ * 200:
334
+ * description: Callback traité
335
+ * 500:
336
+ * description: Erreur serveur
337
+ */
133
338
  this.post('/files/s3-callback', { requireAuth: false, permissions: [] }, async (req, res) => {
134
339
  try {
135
340
  // Cette route peut être utilisée pour recevoir des webhooks S3
@@ -7,7 +7,7 @@ export declare class EdrmStorageService {
7
7
  private getProvider;
8
8
  private generateKey;
9
9
  private detectFileType;
10
- initUpload(originalName: string, mimeType: string, size: number, tenantId: string, entityName: string, entityId: string, provider?: FileProvider, metadata?: Record<string, any>, tags?: string[]): Promise<{
10
+ initUpload(originalName: string, mimeType: string, size: number, tenantId: string, entityName: string, entityId: string, provider?: FileProvider, metadata?: Record<string, any>, tags?: string[], portalEntityId?: string): Promise<{
11
11
  fileId: import("mongoose").Types.ObjectId;
12
12
  uploadId: string;
13
13
  presignedUrl: string;
@@ -15,14 +15,14 @@ export declare class EdrmStorageService {
15
15
  bucket: string;
16
16
  key: string;
17
17
  }>;
18
- completeUpload(fileId: string): Promise<any>;
19
- getDownloadUrl(fileId: string, filename?: string, expiresIn?: number): Promise<any>;
20
- deleteFile(fileId: string): Promise<void>;
21
- listFiles(tenantId?: string, entityName?: string, entityId?: string, status?: FileStatus, page?: number, limit?: number): Promise<{
18
+ completeUpload(fileId: string, portalEntityId?: string): Promise<any>;
19
+ getDownloadUrl(fileId: string, filename?: string, expiresIn?: number, portalEntityId?: string): Promise<any>;
20
+ deleteFile(fileId: string, portalEntityId?: string): Promise<void>;
21
+ listFiles(tenantId?: string, entityName?: string, entityId?: string, status?: FileStatus, page?: number, limit?: number, portalEntityId?: string): Promise<{
22
22
  files: any[];
23
23
  total: number;
24
24
  page: number;
25
25
  totalPages: number;
26
26
  }>;
27
- getFileById(fileId: string): Promise<any>;
27
+ getFileById(fileId: string, portalEntityId?: string): Promise<any>;
28
28
  }
@@ -41,7 +41,7 @@ export class EdrmStorageService {
41
41
  return FileType.ARCHIVE;
42
42
  return FileType.OTHER;
43
43
  }
44
- async initUpload(originalName, mimeType, size, tenantId, entityName, entityId, provider = this.defaultProvider, metadata, tags) {
44
+ async initUpload(originalName, mimeType, size, tenantId, entityName, entityId, provider = this.defaultProvider, metadata, tags, portalEntityId) {
45
45
  const bucket = process.env.S3_BUCKET || 'edrm-storage';
46
46
  const key = this.generateKey(tenantId, entityName, entityId, originalName);
47
47
  const storageProvider = this.getProvider(provider);
@@ -64,6 +64,7 @@ export class EdrmStorageService {
64
64
  tenantId,
65
65
  entityName,
66
66
  entityId,
67
+ ...(portalEntityId && { portalEntityId }),
67
68
  uploadedBy: 'system', // À remplacer par l'utilisateur connecté
68
69
  accessCount: 0
69
70
  });
@@ -77,11 +78,14 @@ export class EdrmStorageService {
77
78
  key
78
79
  };
79
80
  }
80
- async completeUpload(fileId) {
81
+ async completeUpload(fileId, portalEntityId) {
81
82
  const fileRecord = await FileModel.findById(fileId);
82
83
  if (!fileRecord) {
83
84
  throw new Error('Fichier non trouvé');
84
85
  }
86
+ if (portalEntityId && fileRecord.portalEntityId && fileRecord.portalEntityId !== portalEntityId) {
87
+ throw new Error('Fichier non trouvé');
88
+ }
85
89
  const storageProvider = this.getProvider(fileRecord.provider);
86
90
  try {
87
91
  // Vérifier que le fichier existe dans le stockage
@@ -102,11 +106,14 @@ export class EdrmStorageService {
102
106
  throw new Error(`Erreur lors de la finalisation de l'upload: ${error}`);
103
107
  }
104
108
  }
105
- async getDownloadUrl(fileId, filename, expiresIn = 3600) {
109
+ async getDownloadUrl(fileId, filename, expiresIn = 3600, portalEntityId) {
106
110
  const fileRecord = await FileModel.findById(fileId);
107
111
  if (!fileRecord) {
108
112
  throw new Error('Fichier non trouvé');
109
113
  }
114
+ if (portalEntityId && fileRecord.portalEntityId && fileRecord.portalEntityId !== portalEntityId) {
115
+ throw new Error('Fichier non trouvé');
116
+ }
110
117
  if (fileRecord.status !== FileStatus.COMPLETED) {
111
118
  throw new Error('Fichier non encore finalisé');
112
119
  }
@@ -123,11 +130,14 @@ export class EdrmStorageService {
123
130
  contentType: downloadResponse.contentType
124
131
  };
125
132
  }
126
- async deleteFile(fileId) {
133
+ async deleteFile(fileId, portalEntityId) {
127
134
  const fileRecord = await FileModel.findById(fileId);
128
135
  if (!fileRecord) {
129
136
  throw new Error('Fichier non trouvé');
130
137
  }
138
+ if (portalEntityId && fileRecord.portalEntityId && fileRecord.portalEntityId !== portalEntityId) {
139
+ throw new Error('Fichier non trouvé');
140
+ }
131
141
  const storageProvider = this.getProvider(fileRecord.provider);
132
142
  try {
133
143
  // Supprimer du stockage
@@ -141,7 +151,7 @@ export class EdrmStorageService {
141
151
  throw new Error(`Erreur lors de la suppression: ${error}`);
142
152
  }
143
153
  }
144
- async listFiles(tenantId, entityName, entityId, status, page = 1, limit = 20) {
154
+ async listFiles(tenantId, entityName, entityId, status, page = 1, limit = 20, portalEntityId) {
145
155
  const query = {};
146
156
  if (tenantId)
147
157
  query.tenantId = tenantId;
@@ -149,6 +159,8 @@ export class EdrmStorageService {
149
159
  query.entityName = entityName;
150
160
  if (entityId)
151
161
  query.entityId = entityId;
162
+ if (portalEntityId)
163
+ query.portalEntityId = portalEntityId;
152
164
  if (status)
153
165
  query.status = status;
154
166
  const skip = (page - 1) * limit;
@@ -167,11 +179,14 @@ export class EdrmStorageService {
167
179
  totalPages: Math.ceil(total / limit)
168
180
  };
169
181
  }
170
- async getFileById(fileId) {
182
+ async getFileById(fileId, portalEntityId) {
171
183
  const fileRecord = await FileModel.findById(fileId);
172
184
  if (!fileRecord) {
173
185
  throw new Error('Fichier non trouvé');
174
186
  }
187
+ if (portalEntityId && fileRecord.portalEntityId && fileRecord.portalEntityId !== portalEntityId) {
188
+ throw new Error('Fichier non trouvé');
189
+ }
175
190
  return fileRecord;
176
191
  }
177
192
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@programisto/edrm-storage",
4
- "version": "1.0.4",
4
+ "version": "1.0.6",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },