data-primals-engine 1.7.2 → 1.7.3
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 +160 -160
- package/client/package-lock.json +8430 -8430
- package/client/src/APIInfo.jsx +1 -1
- package/client/src/App.jsx +2 -1
- package/client/src/App.scss +1635 -1626
- package/client/src/AssistantChat.jsx +1 -0
- package/client/src/CalendarView.jsx +1 -0
- package/client/src/ContentView.jsx +3 -3
- package/client/src/Dashboard.jsx +5 -2
- package/client/src/DashboardChart.jsx +1 -0
- package/client/src/DashboardFlexViewItem.jsx +1 -0
- package/client/src/DashboardHtmlViewItem.jsx +1 -0
- package/client/src/DashboardView.jsx +2 -0
- package/client/src/DataEditor.jsx +0 -1
- package/client/src/DataImporter.jsx +489 -468
- package/client/src/DataLayout.jsx +6 -3
- package/client/src/DataTable.jsx +4 -3
- package/client/src/Dialog.jsx +92 -90
- package/client/src/Dialog.scss +122 -116
- package/client/src/DisplayFlexNodeRenderer.jsx +1 -0
- package/client/src/FlexBuilderControls.jsx +1 -0
- package/client/src/FlexBuilderPreview.jsx +1 -1
- package/client/src/FlexNode.jsx +1 -1
- package/client/src/HistoryDialog.jsx +3 -2
- package/client/src/KPIWidget.jsx +1 -1
- package/client/src/KanbanView.jsx +1 -0
- package/client/src/ModelCreator.jsx +4 -0
- package/client/src/ModelList.jsx +1 -0
- package/client/src/PackGallery.jsx +5 -4
- package/client/src/RTETrans.jsx +1 -1
- package/client/src/RelationField.jsx +2 -0
- package/client/src/RelationSelectorWidget.jsx +2 -0
- package/client/src/RelationValue.jsx +1 -0
- package/client/src/RestoreDialog.jsx +1 -0
- package/client/src/WorkflowEditor.jsx +3 -0
- package/client/src/contexts/CommandContext.jsx +2 -1
- package/client/src/contexts/ModelContext.jsx +3 -3
- package/client/src/hooks/data.js +1 -0
- package/client/src/hooks/useValidation.js +1 -0
- package/client/src/translations.js +24 -24
- package/doc/AI-assistance.md +87 -63
- package/doc/Concepts.md +122 -0
- package/doc/Custom-Endpoints.md +31 -0
- package/doc/Event-system.md +13 -14
- package/doc/Home.md +33 -0
- package/doc/Modules.md +83 -0
- package/doc/Packs-gallery.md +8 -23
- package/doc/Workflows.md +32 -0
- package/doc/automation-workflows.md +141 -102
- package/doc/dashboards-kpis-charts.md +47 -49
- package/doc/data-management.md +126 -120
- package/doc/data-models.md +68 -75
- package/doc/roles-permissions.md +144 -43
- package/doc/sharding-replication.md +158 -0
- package/doc/users.md +54 -30
- package/package.json +1 -1
- package/server.js +37 -37
- package/src/constants.js +7 -8
- package/src/data.js +521 -520
- package/src/email.js +157 -154
- package/src/engine.js +117 -7
- package/src/gameObject.js +6 -0
- package/src/i18n.js +0 -1
- package/src/modules/auth-google/index.js +53 -50
- package/src/modules/bucket.js +346 -339
- package/src/modules/data/data.backup.js +400 -376
- package/src/modules/data/data.cluster.js +410 -42
- package/src/modules/data/data.operations.js +3666 -3635
- package/src/modules/data/data.replication.js +106 -64
- package/src/modules/data/data.routes.js +87 -64
- package/src/modules/data/data.scheduling.js +2 -1
- package/src/modules/file.js +248 -247
- package/src/modules/user.js +11 -1
- package/src/sso.js +91 -25
- package/test/cluster.test.js +221 -0
- package/test/core.test.js +0 -2
- package/test/replication.test.js +163 -0
- package/doc/core-concepts.md +0 -33
package/src/modules/bucket.js
CHANGED
|
@@ -1,340 +1,347 @@
|
|
|
1
|
-
import process from "node:process";
|
|
2
|
-
import AWS from "aws-sdk";
|
|
3
|
-
import fs from "node:fs";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import {decryptValue, encryptValue} from "../data.js";
|
|
6
|
-
import {loadFromDump, validateRestoreRequest} from "./data/index.js";
|
|
7
|
-
import {Logger} from "../gameObject.js";
|
|
8
|
-
import {middlewareAuthenticator, userInitiator} from "./user.js";
|
|
9
|
-
import {awsDefaultConfig, getHost, maxBytesPerSecondThrottleData} from "../constants.js";
|
|
10
|
-
import crypto from "node:crypto";
|
|
11
|
-
import i18n from "../../src/i18n.js";
|
|
12
|
-
import {sendEmail} from "../email.js";
|
|
13
|
-
import {throttleMiddleware} from "../middlewares/throttle.js";
|
|
14
|
-
import {getCollectionForUser} from "./mongodb.js";
|
|
15
|
-
import {safeAssignObject} from "../core.js";
|
|
16
|
-
import {Config} from "../config.js";
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
// On utilise une nouvelle clé de traduction pour l'email
|
|
36
|
-
await sendEmail(user.email, {
|
|
37
|
-
title: i18n.t('
|
|
38
|
-
content: i18n.t('
|
|
39
|
-
user: user?.username,
|
|
40
|
-
fullToken: fullRestoreToken,
|
|
41
|
-
host: getHost(),
|
|
42
|
-
modelsToken: modelsRestoreToken
|
|
43
|
-
})
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
*
|
|
55
|
-
* @
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (envVar.key === '
|
|
75
|
-
if (envVar.key === '
|
|
76
|
-
if (envVar.key === '
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
* @
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
engine
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import AWS from "aws-sdk";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import {decryptValue, encryptValue} from "../data.js";
|
|
6
|
+
import {loadFromDump, validateRestoreRequest} from "./data/index.js";
|
|
7
|
+
import {Logger} from "../gameObject.js";
|
|
8
|
+
import {getInternalSmtpConfig, getSmtpConfig, middlewareAuthenticator, userInitiator} from "./user.js";
|
|
9
|
+
import {awsDefaultConfig, getHost, maxBytesPerSecondThrottleData} from "../constants.js";
|
|
10
|
+
import crypto from "node:crypto";
|
|
11
|
+
import i18n from "../../src/i18n.js";
|
|
12
|
+
import { sendEmail, changeLanguage } from "../email.js";
|
|
13
|
+
import {throttleMiddleware} from "../middlewares/throttle.js";
|
|
14
|
+
import {getCollectionForUser} from "./mongodb.js";
|
|
15
|
+
import {safeAssignObject} from "../core.js";
|
|
16
|
+
import {Config} from "../config.js";
|
|
17
|
+
const restoreRequests = {};
|
|
18
|
+
|
|
19
|
+
export const requestRestore = async (user, lang) => {
|
|
20
|
+
// On génère deux tokens uniques
|
|
21
|
+
const fullRestoreToken = crypto.randomBytes(32).toString('hex');
|
|
22
|
+
const modelsRestoreToken = crypto.randomBytes(32).toString('hex');
|
|
23
|
+
const expiration = new Date(Date.now() + 30 * 60 * 1000); // 30 minutes
|
|
24
|
+
|
|
25
|
+
safeAssignObject(restoreRequests, user?.username, {
|
|
26
|
+
fullToken: fullRestoreToken,
|
|
27
|
+
modelsToken: modelsRestoreToken,
|
|
28
|
+
expiresAt: expiration
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const smtpConfig = await getInternalSmtpConfig(user); // Fetch SMTP config for the user
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
await changeLanguage(lang);
|
|
35
|
+
// On utilise une nouvelle clé de traduction pour l'email
|
|
36
|
+
await sendEmail(user.email, {
|
|
37
|
+
title: i18n.t('email_backup_restoreRequest_subject'),
|
|
38
|
+
content: i18n.t('email_backup_restoreRequest_content', {
|
|
39
|
+
user: user?.username,
|
|
40
|
+
fullToken: fullRestoreToken,
|
|
41
|
+
host: getHost(),
|
|
42
|
+
modelsToken: modelsRestoreToken
|
|
43
|
+
})
|
|
44
|
+
}, smtpConfig, lang).catch(e => logger.error(e.message));
|
|
45
|
+
|
|
46
|
+
return { message: 'Restore links sent to your email.' };
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error('Error sending email:', error);
|
|
49
|
+
throw new Error('Error sending email.');
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Récupère la configuration S3 depuis les variables d'environnement de l'utilisateur en base de données.
|
|
55
|
+
* @param {object} user - L'objet utilisateur.
|
|
56
|
+
* @returns {Promise<object>} - Un objet contenant la configuration S3 trouvée pour l'utilisateur.
|
|
57
|
+
*/
|
|
58
|
+
async function _getUserS3ConfigFromDb(user) {
|
|
59
|
+
if (!user || !user.username) {
|
|
60
|
+
return {}; // Pas d'utilisateur, pas de configuration spécifique.
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const collection = await getCollectionForUser(user);
|
|
65
|
+
const userEnvVars = await collection.find({
|
|
66
|
+
_model: 'env',
|
|
67
|
+
_user: user.username,
|
|
68
|
+
// On ne cherche que les clés pertinentes pour optimiser la requête
|
|
69
|
+
key: { $in: ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_REGION', 'AWS_BUCKET_NAME'] }
|
|
70
|
+
}).toArray();
|
|
71
|
+
|
|
72
|
+
// Transforme le tableau de documents [{key, value}, ...] en un objet de configuration.
|
|
73
|
+
return userEnvVars.reduce((config, envVar) => {
|
|
74
|
+
if (envVar.key === 'AWS_ACCESS_KEY_ID') config.accessKeyId = envVar.value;
|
|
75
|
+
if (envVar.key === 'AWS_SECRET_ACCESS_KEY') config.secretAccessKey = envVar.value; // La clé est déjà chiffrée en BDD
|
|
76
|
+
if (envVar.key === 'AWS_REGION') config.region = envVar.value;
|
|
77
|
+
if (envVar.key === 'AWS_BUCKET_NAME') config.bucketName = envVar.value;
|
|
78
|
+
return config;
|
|
79
|
+
}, {});
|
|
80
|
+
|
|
81
|
+
} catch (error) {
|
|
82
|
+
logger.error(`Failed to fetch user S3 config from DB for ${user.username}:`, error);
|
|
83
|
+
return {}; // Retourne un objet vide en cas d'erreur pour utiliser le fallback.
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Récupère la configuration S3 effective en priorisant celle de l'utilisateur
|
|
89
|
+
* puis en se rabattant sur la configuration globale de l'environnement.
|
|
90
|
+
* C'est la fonction à utiliser pour toute opération S3.
|
|
91
|
+
* @param {object} user - L'objet utilisateur.
|
|
92
|
+
* @returns {Promise<object>} - L'objet de configuration S3 final.
|
|
93
|
+
*/
|
|
94
|
+
export async function getUserS3Config(user) {
|
|
95
|
+
// NOUVEAU : Vérification de l'interrupteur principal pour S3
|
|
96
|
+
// Si cette configuration n'est pas explicitement à `true`, on désactive S3.
|
|
97
|
+
if (Config.Get('storageS3', false) !== true) {
|
|
98
|
+
logger.debug("[S3] Le stockage S3 est désactivé via la configuration 'storageS3'.");
|
|
99
|
+
return null; // Retourne null pour indiquer que S3 n'est pas configuré.
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const adc = awsDefaultConfig;
|
|
103
|
+
const dc = Config.Get('awsDefaultConfig', {});
|
|
104
|
+
|
|
105
|
+
// 1. Récupérer la configuration globale par défaut
|
|
106
|
+
const defaultConfig = {
|
|
107
|
+
accessKeyId: dc.accessKeyId || process.env.AWS_ACCESS_KEY_ID,
|
|
108
|
+
secretAccessKey: dc.secretAccessKey || process.env.AWS_SECRET_ACCESS_KEY,
|
|
109
|
+
region: dc.region || process.env.AWS_REGION || adc.region,
|
|
110
|
+
bucketName: dc.bucketName || process.env.AWS_BUCKET_NAME || adc.bucketName
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// 2. Récupérer la configuration spécifique de l'utilisateur
|
|
114
|
+
const userConfig = await _getUserS3ConfigFromDb(user);
|
|
115
|
+
|
|
116
|
+
if( userConfig.bucketName && userConfig.accessKeyId && userConfig.secretAccessKey) {
|
|
117
|
+
// 3. Fusionner les configurations, en donnant la priorité à celle de l'utilisateur
|
|
118
|
+
return {
|
|
119
|
+
accessKeyId: userConfig.accessKeyId || defaultConfig.accessKeyId,
|
|
120
|
+
secretAccessKey: userConfig.secretAccessKey || defaultConfig.secretAccessKey,
|
|
121
|
+
region: userConfig.region || defaultConfig.region,
|
|
122
|
+
bucketName: userConfig.bucketName || defaultConfig.bucketName
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
return defaultConfig;
|
|
126
|
+
}
|
|
127
|
+
const getS3Client = (s3Config) => {
|
|
128
|
+
|
|
129
|
+
const decryptedSecretAccessKey = s3Config.secretAccessKey ? decryptValue(s3Config.secretAccessKey): undefined;
|
|
130
|
+
|
|
131
|
+
return new AWS.S3({
|
|
132
|
+
accessKeyId: s3Config.accessKeyId || defaultConfig.accessKeyId,
|
|
133
|
+
secretAccessKey: decryptedSecretAccessKey ? decryptedSecretAccessKey : defaultConfig.secretAccessKey,
|
|
134
|
+
region: s3Config.region || defaultConfig.region,
|
|
135
|
+
bucket: s3Config.bucketName || defaultConfig.bucketName
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export const uploadToS3 = async (s3Config, filePath, remoteFilename) => {
|
|
140
|
+
const s3 = getS3Client(s3Config);
|
|
141
|
+
const fileContent = fs.readFileSync(filePath);
|
|
142
|
+
const bucketPath = s3Config.pathPrefix ? `${s3Config.pathPrefix.replace(/\/$/, "")}/${remoteFilename}` : remoteFilename;
|
|
143
|
+
|
|
144
|
+
if( !s3.accessKeyId || !s3.secretAccessKey || !s3.bucketName) {
|
|
145
|
+
throw new Error('Missing S3 configuration');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const params = {
|
|
149
|
+
Bucket: s3Config.bucketName,
|
|
150
|
+
Key: bucketPath,
|
|
151
|
+
Body: fileContent
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const data = await s3.upload(params).promise();
|
|
156
|
+
console.log(`File uploaded successfully to S3. ${data.Location}`);
|
|
157
|
+
return data;
|
|
158
|
+
} catch (err) {
|
|
159
|
+
console.error("Error uploading to S3:", err);
|
|
160
|
+
throw err;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
export const listS3Backups = async (s3Config) => {
|
|
166
|
+
const s3 = getS3Client(s3Config);
|
|
167
|
+
const bucketPathPrefix = s3Config.pathPrefix ? `${s3Config.pathPrefix.replace(/\/$/, "")}/` : '';
|
|
168
|
+
|
|
169
|
+
const params = {
|
|
170
|
+
Bucket: s3Config.bucketName,
|
|
171
|
+
Prefix: bucketPathPrefix
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const data = await s3.listObjectsV2(params).promise();
|
|
176
|
+
return data.Contents.map(item => ({
|
|
177
|
+
key: item.Key,
|
|
178
|
+
lastModified: item.LastModified,
|
|
179
|
+
size: item.Size,
|
|
180
|
+
// Tu peux extraire le nom du fichier et le timestamp de la clé si nécessaire
|
|
181
|
+
filename: path.basename(item.Key),
|
|
182
|
+
timestamp: parseInt(path.basename(item.Key).split('_').pop().split('.')[0], 10) || 0
|
|
183
|
+
})).sort((a, b) => b.timestamp - a.timestamp); // Trier par timestamp descendant
|
|
184
|
+
} catch (err) {
|
|
185
|
+
console.error("Error listing S3 backups:", err);
|
|
186
|
+
throw err;
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
export const downloadFromS3 = async (s3Config, s3FileKey, downloadPath) => {
|
|
191
|
+
const s3 = getS3Client(s3Config);
|
|
192
|
+
const params = {
|
|
193
|
+
Bucket: s3Config.bucketName,
|
|
194
|
+
Key: s3FileKey
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
const data = await s3.getObject(params).promise();
|
|
199
|
+
fs.writeFileSync(downloadPath, data.Body);
|
|
200
|
+
console.log(`File downloaded successfully from S3 to ${downloadPath}`);
|
|
201
|
+
return downloadPath;
|
|
202
|
+
} catch (err) {
|
|
203
|
+
console.error("Error downloading from S3:", err);
|
|
204
|
+
throw err;
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
export const deleteFromS3 = async (s3Config, remoteFilename) => {
|
|
209
|
+
const s3 = getS3Client(s3Config);
|
|
210
|
+
const bucketPath = s3Config.pathPrefix ? `${s3Config.pathPrefix.replace(/\/$/, "")}/${remoteFilename}` : remoteFilename;
|
|
211
|
+
|
|
212
|
+
const params = {
|
|
213
|
+
Bucket: s3Config.bucketName,
|
|
214
|
+
Key: bucketPath
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
await s3.deleteObject(params).promise();
|
|
219
|
+
console.log(`File successfully removed from S3 : ${bucketPath}`);
|
|
220
|
+
} catch (err) {
|
|
221
|
+
console.error("Error when deleting S3 file :", err);
|
|
222
|
+
throw err;
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Crée un flux de lecture pour un objet depuis un bucket S3.
|
|
228
|
+
* @param {object} s3Config - La configuration S3 (bucketName, accessKeyId, secretAccessKey, region).
|
|
229
|
+
* @param {string} s3Key - La clé (nom du fichier) de l'objet sur S3.
|
|
230
|
+
* @returns {import('stream').Readable} - Le flux de lecture de l'objet S3.
|
|
231
|
+
*/
|
|
232
|
+
export const getS3Stream = (s3Config, s3Key) => {
|
|
233
|
+
if (!s3Config || !s3Config.bucketName || !s3Key) {
|
|
234
|
+
throw new Error("La configuration S3 et la clé de l'objet sont requises pour créer un flux.");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// On utilise la fonction existante qui gère le déchiffrement !
|
|
238
|
+
const s3 = getS3Client(s3Config);
|
|
239
|
+
|
|
240
|
+
const params = {
|
|
241
|
+
Bucket: s3Config.bucketName,
|
|
242
|
+
Key: s3Key
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// getObject().createReadStream() retourne directement un flux lisible.
|
|
246
|
+
// Les erreurs (ex: objet non trouvé) seront émises sur l'événement 'error' de ce flux.
|
|
247
|
+
return s3.getObject(params).createReadStream();
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
let engine, logger;
|
|
251
|
+
export async function onInit(defaultEngine) {
|
|
252
|
+
engine = defaultEngine;
|
|
253
|
+
logger = engine.getComponent(Logger);
|
|
254
|
+
|
|
255
|
+
const m = Config.Get('maxBytesPerSecondThrottleData', maxBytesPerSecondThrottleData);
|
|
256
|
+
const throttle = throttleMiddleware(m);
|
|
257
|
+
|
|
258
|
+
const userMiddlewares = await engine.userProvider.getMiddlewares();
|
|
259
|
+
engine.post('/api/backup/request-restore', [throttle, middlewareAuthenticator, userInitiator, ...userMiddlewares], async (req, res) => {
|
|
260
|
+
const user = req.me; // Assuming you have user authentication middleware
|
|
261
|
+
if (!user) {
|
|
262
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
const result = await requestRestore(user, req.query.lang);
|
|
267
|
+
res.json(result);
|
|
268
|
+
} catch (error) {
|
|
269
|
+
console.error('Error requesting restore:', error);
|
|
270
|
+
res.status(500).json({ error: error.message });
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
engine.get('/api/backup/restore', [throttle, middlewareAuthenticator, userInitiator], async (req, res) => {
|
|
274
|
+
const { token, username } = req.query;
|
|
275
|
+
|
|
276
|
+
if (!((req.me?.roles || []).includes("admin"))) {
|
|
277
|
+
return res.status(403).json({success: false, error: 'Cannot dump data.'})
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (!token || !username) {
|
|
281
|
+
return res.status(400).json({ error: 'Token and username are required.' });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const validationResult = validateRestoreRequest(username, token);
|
|
285
|
+
|
|
286
|
+
if (validationResult.error) {
|
|
287
|
+
return res.status(400).json({ error: validationResult.error });
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
await loadFromDump({username});
|
|
292
|
+
res.json({ message: 'Backup restoration successful.' });
|
|
293
|
+
delete restoreRequests[username];
|
|
294
|
+
} catch (error) {
|
|
295
|
+
console.error('Error restoring backup:', error);
|
|
296
|
+
res.status(500).json({ error: 'Error restoring backup.' });
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
engine.post('/api/user/s3-config', [middlewareAuthenticator, userInitiator, ...userMiddlewares], async (req, res) => {
|
|
301
|
+
const user = req.me;
|
|
302
|
+
const { bucketName, accessKeyId, secretAccessKey, region, pathPrefix } = req.fields;
|
|
303
|
+
|
|
304
|
+
// Validation basique des entrées
|
|
305
|
+
if (!bucketName || !accessKeyId || !region) {
|
|
306
|
+
return res.status(400).json({ success: false, error: "Bucket name, Access Key ID, and Region are required." });
|
|
307
|
+
}
|
|
308
|
+
// Autres validations possibles (longueur, format de la région, etc.)
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
const updateData = {
|
|
312
|
+
's3Config.bucketName': bucketName,
|
|
313
|
+
's3Config.accessKeyId': accessKeyId, // Stocké en clair (généralement acceptable)
|
|
314
|
+
's3Config.region': region,
|
|
315
|
+
's3Config.pathPrefix': pathPrefix || '' // S'assurer qu'il y a une valeur par défaut si vide
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
// Chiffrer et mettre à jour la clé secrète uniquement si elle est fournie
|
|
319
|
+
if (secretAccessKey) {
|
|
320
|
+
updateData['s3Config.secretAccessKey'] = encryptValue(secretAccessKey); // Chiffrer avant de stocker
|
|
321
|
+
} else {
|
|
322
|
+
// Si la clé secrète n'est pas fournie, on ne la modifie pas.
|
|
323
|
+
// Si tu veux permettre de la supprimer, il faudrait une logique explicite.
|
|
324
|
+
// Pour l'instant, on ne touche pas à s3Config.secretAccessKey si req.fields.secretAccessKey est vide.
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const result = await engine.userProvider.updateUser(
|
|
328
|
+
{ username: user.username }, // ou user._id si c'est ce que tu utilises comme identifiant unique
|
|
329
|
+
updateData
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
if (result) { // Succès même si rien n'a été modifié (déjà les bonnes valeurs)
|
|
333
|
+
res.json({ success: true, message: "S3 configuration updated successfully." });
|
|
334
|
+
} else {
|
|
335
|
+
const userExists = engine.userProvider.findUserByUsername(user.username);
|
|
336
|
+
if (!userExists) {
|
|
337
|
+
return res.status(404).json({ success: false, error: "User not found." });
|
|
338
|
+
}
|
|
339
|
+
res.json({ success: true, message: "S3 configuration already up to date." });
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
} catch (error) {
|
|
343
|
+
logger.error(`Error updating S3 configuration for user ${user.username}:`, error);
|
|
344
|
+
res.status(500).json({ success: false, error: "An internal server error occurred while updating S3 configuration." });
|
|
345
|
+
}
|
|
346
|
+
});
|
|
340
347
|
}
|