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