data-primals-engine 1.2.0 → 1.2.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.
- package/README.md +78 -14
- package/client/src/App.scss +11 -6
- package/client/src/ConditionBuilder2.jsx +47 -6
- package/client/src/DataLayout.jsx +4 -21
- package/client/src/DataTable.jsx +3 -1
- package/client/src/Field.jsx +1 -1
- package/client/src/ModelCreator.jsx +4 -4
- package/client/src/ModelCreator.scss +10 -0
- package/client/src/ModelCreatorField.jsx +34 -34
- package/client/src/RTE.jsx +2 -1
- package/client/src/RestoreConfirmationModal.jsx +0 -137
- package/client/src/filter.js +2 -0
- package/client/src/hooks/useTutorials.jsx +3 -3
- package/package.json +4 -3
- package/src/core.js +6 -0
- package/src/data.js +4 -4
- package/src/defaultModels.js +33 -0
- package/src/email.js +2 -2
- package/src/engine.js +10 -7
- package/src/events.js +25 -3
- package/src/modules/bucket.js +115 -5
- package/src/modules/data.js +166 -93
- package/src/modules/file.js +98 -57
- package/src/modules/test +147 -0
- package/src/modules/user.js +111 -56
- package/test/data.backup.integration.test.js +1 -1
- package/test/events.test.js +202 -0
- package/test/file.test.js +196 -0
- package/test/user.test.js +203 -0
|
@@ -15,80 +15,6 @@ const RestoreConfirmationModal = ({ isOpen, onClose, onConfirm, showS3Config = f
|
|
|
15
15
|
const { me, fetchMe } = useAuthContext(); // fetchMe pour recharger les données utilisateur après sauvegarde
|
|
16
16
|
const { addNotification } = useNotificationContext();
|
|
17
17
|
|
|
18
|
-
// États pour les champs de configuration S3
|
|
19
|
-
const [s3Config, setS3Config] = useState({
|
|
20
|
-
bucketName: '',
|
|
21
|
-
accessKeyId: '',
|
|
22
|
-
secretAccessKey: '', // Ne sera pas affiché directement mais envoyé
|
|
23
|
-
region: ''
|
|
24
|
-
});
|
|
25
|
-
const [isSavingS3Config, setIsSavingS3Config] = useState(false);
|
|
26
|
-
const [showConfig, setConfigVisible] = useState(false);
|
|
27
|
-
|
|
28
|
-
// Charger la configuration S3 existante de l'utilisateur au montage si me.s3Config existe
|
|
29
|
-
useEffect(() => {
|
|
30
|
-
if (me?.s3Config && showS3Config) {
|
|
31
|
-
setS3Config({
|
|
32
|
-
bucketName: me.s3Config.bucketName || '',
|
|
33
|
-
accessKeyId: me.s3Config.accessKeyId || '',
|
|
34
|
-
secretAccessKey: '', // Ne pas pré-remplir la clé secrète pour la sécurité
|
|
35
|
-
region: me.s3Config.region || '',
|
|
36
|
-
pathPrefix: me.s3Config.pathPrefix || ''
|
|
37
|
-
});
|
|
38
|
-
} else if (showS3Config) {
|
|
39
|
-
// Réinitialiser si pas de config ou si on quitte la section S3
|
|
40
|
-
setS3Config({ bucketName: '', accessKeyId: '', secretAccessKey: '', region: '', pathPrefix: '' });
|
|
41
|
-
}
|
|
42
|
-
}, [me, showS3Config, isOpen]); // Ajouter isOpen pour recharger si la modale est rouverte
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const handleS3ConfigChange = (e) => {
|
|
46
|
-
const { name, value } = e.target;
|
|
47
|
-
setS3Config(prev => ({ ...prev, [name]: value }));
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const handleSaveS3Config = async () => {
|
|
51
|
-
setIsSavingS3Config(true);
|
|
52
|
-
// Validation basique côté client
|
|
53
|
-
if (!s3Config.bucketName || !s3Config.accessKeyId || !s3Config.region) {
|
|
54
|
-
addNotification({ type: 'error', message: t('backup.s3config.validationError', 'Le nom du bucket, l\'Access Key ID et la Région sont requis.') });
|
|
55
|
-
setIsSavingS3Config(false);
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
try {
|
|
60
|
-
const payload = { ...s3Config };
|
|
61
|
-
// N'envoyer la clé secrète que si elle a été modifiée
|
|
62
|
-
if (!payload.secretAccessKey) {
|
|
63
|
-
delete payload.secretAccessKey;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const response = await fetch('/api/user/s3-config', { // Endpoint pour sauvegarder la config S3
|
|
67
|
-
method: 'POST',
|
|
68
|
-
headers: {
|
|
69
|
-
'Content-Type': 'application/json',
|
|
70
|
-
// Les en-têtes d'authentification (_user, Authorization) sont gérés globalement par le fetch wrapper si tu en as un, sinon ajoute-les ici
|
|
71
|
-
// Exemple:
|
|
72
|
-
// '_user': me?.username,
|
|
73
|
-
// 'Authorization': `Bearer ${me?.token}`,
|
|
74
|
-
},
|
|
75
|
-
body: JSON.stringify(payload),
|
|
76
|
-
});
|
|
77
|
-
const result = await response.json();
|
|
78
|
-
if (response.ok && result.success) {
|
|
79
|
-
addNotification({ status: 'completed', title: t('backup.s3config.saveSuccess', 'Configuration S3 enregistrée avec succès.') });
|
|
80
|
-
} else {
|
|
81
|
-
addNotification({ status: 'error', title: result.error || t('backup.s3config.saveError', 'Erreur lors de l\'enregistrement de la configuration S3.') });
|
|
82
|
-
}
|
|
83
|
-
} catch (error) {
|
|
84
|
-
addNotification({ status: 'error', title: t('backup.s3config.saveError', 'Erreur lors de l\'enregistrement de la configuration S3.') });
|
|
85
|
-
console.error("Error saving S3 config:", error);
|
|
86
|
-
} finally {
|
|
87
|
-
setIsSavingS3Config(false);
|
|
88
|
-
}
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
|
|
92
18
|
if (!isOpen) {
|
|
93
19
|
return null;
|
|
94
20
|
}
|
|
@@ -110,71 +36,8 @@ const RestoreConfirmationModal = ({ isOpen, onClose, onConfirm, showS3Config = f
|
|
|
110
36
|
<Trans i18nKey="btns.cancel">Annuler</Trans>
|
|
111
37
|
</Button>
|
|
112
38
|
</div>
|
|
113
|
-
<div className={"flex flex-centered actions"}>
|
|
114
|
-
{!showConfig && (<NavLink onClick={() => setConfigVisible(true)}><Trans i18nKey={"backup.s3config.title"}></Trans></NavLink>)}
|
|
115
|
-
</div>
|
|
116
39
|
</>
|
|
117
40
|
|
|
118
|
-
{showConfig && (<>
|
|
119
|
-
<h2>{t('backup.s3config.title', 'Configuration du stockage S3')}</h2>
|
|
120
|
-
<p><Trans i18nKey="backup.prez"></Trans></p>
|
|
121
|
-
<form className="s3-config-form space-y-4"> {/* Ajout de space-y pour l'espacement vertical */}
|
|
122
|
-
<TextField
|
|
123
|
-
name="bucketName"
|
|
124
|
-
className={"flex flex-1"}
|
|
125
|
-
label={t('backup.s3config.bucketName', 'Nom du Bucket S3')}
|
|
126
|
-
value={s3Config.bucketName}
|
|
127
|
-
onChange={handleS3ConfigChange}
|
|
128
|
-
placeholder="my-bucket-name"
|
|
129
|
-
required
|
|
130
|
-
/>
|
|
131
|
-
<TextField
|
|
132
|
-
name="accessKeyId"
|
|
133
|
-
className={"flex flex-1"}
|
|
134
|
-
label={t('backup.s3config.accessKeyId', 'Access Key ID AWS')}
|
|
135
|
-
value={s3Config.accessKeyId}
|
|
136
|
-
onChange={handleS3ConfigChange}
|
|
137
|
-
placeholder="AKIAIOSFODNN7EXAMPLE"
|
|
138
|
-
required
|
|
139
|
-
/>
|
|
140
|
-
<TextField
|
|
141
|
-
name="secretAccessKey"
|
|
142
|
-
className={"flex flex-1"}
|
|
143
|
-
label={t('backup.s3config.secretAccessKey', 'Secret Access Key AWS')}
|
|
144
|
-
type="password" // Important pour masquer la clé
|
|
145
|
-
value={s3Config.secretAccessKey}
|
|
146
|
-
onChange={handleS3ConfigChange}
|
|
147
|
-
placeholder={t('backup.s3config.secretPlaceholder', 'Laisser vide pour ne pas modifier')} />
|
|
148
|
-
<TextField
|
|
149
|
-
name="region"
|
|
150
|
-
className={"flex flex-1"}
|
|
151
|
-
label={t('backup.s3config.region', 'Région AWS')}
|
|
152
|
-
value={s3Config.region}
|
|
153
|
-
onChange={handleS3ConfigChange}
|
|
154
|
-
placeholder="eu-west-3"
|
|
155
|
-
required
|
|
156
|
-
/>
|
|
157
|
-
<TextField
|
|
158
|
-
name="pathPrefix"
|
|
159
|
-
label={t('backup.s3config.pathPrefix', 'Préfixe de chemin (Optionnel)')}
|
|
160
|
-
value={s3Config.pathPrefix}
|
|
161
|
-
onChange={handleS3ConfigChange}
|
|
162
|
-
placeholder="saves/my-app/"
|
|
163
|
-
help={t('backup.s3config.pathPrefixHelp', 'Ex: "mon-dossier/". Laissez vide pour la racine du bucket.')}
|
|
164
|
-
/>
|
|
165
|
-
<div
|
|
166
|
-
className="modal-actions flex justify-end space-x-2 mt-4"> {/* Flex pour aligner les boutons */}
|
|
167
|
-
<Button onClick={handleSaveS3Config} disabled={isSavingS3Config}
|
|
168
|
-
className="btn-primary"> {/* Classe btn-primary pour le bouton principal */}
|
|
169
|
-
{isSavingS3Config ? t('btns.saving', 'Enregistrement...') : t('btns.save', 'Enregistrer la configuration S3')}
|
|
170
|
-
</Button>
|
|
171
|
-
<Button onClick={onClose} className="btn-secondary">
|
|
172
|
-
<Trans i18nKey="btns.cancel">Annuler</Trans>
|
|
173
|
-
</Button>
|
|
174
|
-
</div>
|
|
175
|
-
</form>
|
|
176
|
-
</>)}
|
|
177
|
-
|
|
178
41
|
</div>
|
|
179
42
|
</Dialog>
|
|
180
43
|
);
|
package/client/src/filter.js
CHANGED
|
@@ -53,6 +53,8 @@ export const MONGO_CALC_OPERATORS = {
|
|
|
53
53
|
$ln: { label: 'ln', multi: false },
|
|
54
54
|
$log10: { label: 'log', multi: false },
|
|
55
55
|
$concat: { label: 'concat', multi: true },
|
|
56
|
+
|
|
57
|
+
// Date-specific
|
|
56
58
|
$year: { label: 'year', multi: false, isDate: true },
|
|
57
59
|
$month: { label: 'month', multi: false, isDate: true },
|
|
58
60
|
$dayOfMonth: { label: 'dayOfMonth', multi: false, isDate: true },
|
|
@@ -6,7 +6,7 @@ import { tutorialsConfig } from '../tutorials.js';
|
|
|
6
6
|
import { useTranslation } from 'react-i18next';
|
|
7
7
|
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
|
8
8
|
import useLocalStorage from "./useLocalStorage.js";
|
|
9
|
-
import {
|
|
9
|
+
import {Event} from "../../../src/events.js";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Hook pour gérer la logique des tutoriels multi-étapes.
|
|
@@ -205,10 +205,10 @@ export const useTutorials = () => {
|
|
|
205
205
|
|
|
206
206
|
console.log(me.activeTutorial + new Date().getMilliseconds());
|
|
207
207
|
const eventTypes = ['API_ADD_DATA', 'API_EDIT_DATA', 'API_DELETE_DATA'];
|
|
208
|
-
eventTypes.forEach(type =>
|
|
208
|
+
eventTypes.forEach(type => Event.Listen(type, handleDataChange, "custom", "data"));
|
|
209
209
|
|
|
210
210
|
return () => {
|
|
211
|
-
eventTypes.forEach(type =>
|
|
211
|
+
eventTypes.forEach(type => Event.RemoveCallback(type, handleDataChange, "custom", "data"));
|
|
212
212
|
};
|
|
213
213
|
}, [me?.activeTutorial]);
|
|
214
214
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "data-primals-engine",
|
|
3
|
-
"version": "1.2.
|
|
4
|
-
"description": "data-primals-engine is
|
|
3
|
+
"version": "1.2.2",
|
|
4
|
+
"description": "data-primals-engine is a package responsible from handling large amount of data using MongoDB in a practical and performant way. It can also get workflow models working (for automation), and fully supports internationalisation. It also has integrated AI assistant.",
|
|
5
5
|
"main": "src/engine.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"scripts": {
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
"resolutions": {
|
|
31
31
|
"tar-fs": "3.0.9",
|
|
32
32
|
"on-headers": "1.1.0",
|
|
33
|
-
"brace-expansion": "2.0.2"
|
|
33
|
+
"brace-expansion": "2.0.2",
|
|
34
|
+
"prismjs": "1.30.0"
|
|
34
35
|
},
|
|
35
36
|
"repository": {
|
|
36
37
|
"type": "git",
|
package/src/core.js
CHANGED
|
@@ -9,6 +9,12 @@ export function escapeRegex(string) {
|
|
|
9
9
|
return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
export function escapeHtml(string){
|
|
13
|
+
return string.replace(/(javascript|data|vbscript):/gi, '').replace(/[^\w-_. ]/gi, function (c) {
|
|
14
|
+
return `&#${c.charCodeAt(0)};`;
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
12
18
|
export const isDate = dt => String(new Date(dt)) !== 'Invalid Date'
|
|
13
19
|
|
|
14
20
|
export const safeAssignObject = (obj, key, value) => {
|
package/src/data.js
CHANGED
|
@@ -5,21 +5,21 @@ import {mainFieldsTypes} from "./constants.js";
|
|
|
5
5
|
|
|
6
6
|
const IV_LENGTH = 16;
|
|
7
7
|
export function encryptValue(text) {
|
|
8
|
-
if (!process.env.
|
|
8
|
+
if (!process.env.ENCRYPTION_KEY) throw new Error("ENCRYPTION_KEY is not set");
|
|
9
9
|
let iv = crypto.randomBytes(IV_LENGTH);
|
|
10
|
-
let cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(process.env.
|
|
10
|
+
let cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(process.env.ENCRYPTION_KEY), iv);
|
|
11
11
|
let encrypted = cipher.update(text);
|
|
12
12
|
encrypted = Buffer.concat([encrypted, cipher.final()]);
|
|
13
13
|
return iv.toString('hex') + ':' + encrypted.toString('hex');
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export function decryptValue(text) {
|
|
17
|
-
if (!process.env.
|
|
17
|
+
if (!process.env.ENCRYPTION_KEY) throw new Error("ENCRYPTION_KEY is not set");
|
|
18
18
|
if (!text || typeof text !== 'string' || !text.includes(':')) return text; // ou throw error
|
|
19
19
|
let textParts = text.split(':');
|
|
20
20
|
let iv = Buffer.from(textParts.shift(), 'hex');
|
|
21
21
|
let encryptedText = Buffer.from(textParts.join(':'), 'hex');
|
|
22
|
-
let decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(process.env.
|
|
22
|
+
let decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(process.env.ENCRYPTION_KEY), iv);
|
|
23
23
|
let decrypted = decipher.update(encryptedText);
|
|
24
24
|
decrypted = Buffer.concat([decrypted, decipher.final()]);
|
|
25
25
|
return decrypted.toString();
|
package/src/defaultModels.js
CHANGED
|
@@ -46,6 +46,39 @@ export const defaultModels = {
|
|
|
46
46
|
{ name: 'tokens', type: 'relation', multiple: true, relation: 'token' }
|
|
47
47
|
]
|
|
48
48
|
},
|
|
49
|
+
userPermission: {
|
|
50
|
+
"name": "userPermission",
|
|
51
|
+
"description": "Gère les exceptions aux permissions des rôles pour un utilisateur (ajouts ou retraits, permanents ou temporaires).",
|
|
52
|
+
"fields": [
|
|
53
|
+
{
|
|
54
|
+
"name": "user",
|
|
55
|
+
"type": "relation",
|
|
56
|
+
"relation": "user",
|
|
57
|
+
"required": true,
|
|
58
|
+
"index": true
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"name": "permission",
|
|
62
|
+
"type": "relation",
|
|
63
|
+
"relation": "permission",
|
|
64
|
+
"required": true,
|
|
65
|
+
"index": true
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"name": "isGranted",
|
|
69
|
+
"type": "boolean",
|
|
70
|
+
"required": true,
|
|
71
|
+
"hint": "True pour accorder la permission, False pour la révoquer explicitement."
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"name": "expiresAt",
|
|
75
|
+
"type": "datetime",
|
|
76
|
+
"required": false,
|
|
77
|
+
"index": true,
|
|
78
|
+
"hint": "Si défini, l'exception (l'octroi ou la révocation) est temporaire."
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
},
|
|
49
82
|
token: {
|
|
50
83
|
name: 'token',
|
|
51
84
|
"description": "",
|
package/src/email.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import process from "node:process";
|
|
2
2
|
import nodemailer from "nodemailer";
|
|
3
3
|
import juice from "juice";
|
|
4
|
-
import {
|
|
4
|
+
import {Event} from "./events.js";
|
|
5
5
|
import {emailDefaultConfig} from "./constants.js";
|
|
6
6
|
|
|
7
7
|
// Le transporteur par défaut, utilisé si aucune config spécifique n'est fournie.
|
|
@@ -52,7 +52,7 @@ export const sendEmail = async (email = "", data, smtpConfig = null, lang, tpl =
|
|
|
52
52
|
// Choisir le transporteur à utiliser
|
|
53
53
|
const transporter = smtpConfig ? createTransporter(smtpConfig||emailDefaultConfig) : defaultTransporter;
|
|
54
54
|
|
|
55
|
-
if (tpl === null) tpl =
|
|
55
|
+
if (tpl === null) tpl = Event.Trigger("sendEmail:template", "system", "calls", data, lang);
|
|
56
56
|
let html = tpl;
|
|
57
57
|
try {
|
|
58
58
|
html = juice(tpl);
|
package/src/engine.js
CHANGED
|
@@ -19,6 +19,7 @@ import {DefaultUserProvider} from "./providers.js";
|
|
|
19
19
|
import formidableMiddleware from 'express-formidable';
|
|
20
20
|
import sirv from "sirv";
|
|
21
21
|
import * as tls from "node:tls";
|
|
22
|
+
import {Event} from "./events.js";
|
|
22
23
|
|
|
23
24
|
// Constants
|
|
24
25
|
const isProduction = process.env.NODE_ENV === 'production'
|
|
@@ -79,13 +80,13 @@ export const Engine = {
|
|
|
79
80
|
Create: async (options = { app : null}) => {
|
|
80
81
|
const engine = GameObject.Create("Engine");
|
|
81
82
|
console.log("Creating engine", Config.Get('modules'));
|
|
82
|
-
engine.addComponent(Logger);
|
|
83
|
+
const logger = engine.addComponent(Logger);
|
|
83
84
|
|
|
84
85
|
engine.userProvider = new DefaultUserProvider(engine);
|
|
85
86
|
|
|
86
87
|
engine.setUserProvider = (providerInstance) => {
|
|
87
88
|
engine.userProvider = providerInstance;
|
|
88
|
-
|
|
89
|
+
logger.info(`Custom UserProvider '${providerInstance.constructor.name}' has been set.`);
|
|
89
90
|
};
|
|
90
91
|
|
|
91
92
|
if (!options.app) {
|
|
@@ -130,9 +131,6 @@ export const Engine = {
|
|
|
130
131
|
return engine._modules.find(m => m.module === module);
|
|
131
132
|
};
|
|
132
133
|
|
|
133
|
-
|
|
134
|
-
const logger = engine.getComponent(Logger);
|
|
135
|
-
|
|
136
134
|
const importModule = async (module) => {
|
|
137
135
|
const moduleA = await import(module);
|
|
138
136
|
if (moduleA.onInit){
|
|
@@ -147,13 +145,13 @@ export const Engine = {
|
|
|
147
145
|
|
|
148
146
|
await Promise.all(Config.Get('modules', []).map(async module => {
|
|
149
147
|
try {
|
|
150
|
-
if( fs.existsSync(module)){
|
|
148
|
+
if( fs.existsSync(module) ){
|
|
151
149
|
return await importModule(module);
|
|
152
150
|
}else {
|
|
153
151
|
return await importModule('./modules/' + module + ".js");
|
|
154
152
|
}
|
|
155
153
|
} catch (e){
|
|
156
|
-
|
|
154
|
+
logger.info('ERROR at loading module '+ module, e.stack);
|
|
157
155
|
}
|
|
158
156
|
})).then(async e => {
|
|
159
157
|
engine._modules = e;
|
|
@@ -202,10 +200,13 @@ export const Engine = {
|
|
|
202
200
|
});
|
|
203
201
|
process.exit(1);
|
|
204
202
|
});
|
|
203
|
+
|
|
204
|
+
Event.Trigger("OnServerStart", "event", "system", engine);
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
engine.stop = async () => {
|
|
208
208
|
await server.close();
|
|
209
|
+
Event.Trigger("OnServerStop", "event", "system", engine);
|
|
209
210
|
};
|
|
210
211
|
|
|
211
212
|
async function setupInitialModels() {
|
|
@@ -228,9 +229,11 @@ export const Engine = {
|
|
|
228
229
|
logger.info('Model loaded (' + model.name + ')');
|
|
229
230
|
}
|
|
230
231
|
logger.info("All models loaded.");
|
|
232
|
+
Event.Trigger("OnModelsLoaded", "event", "system", engine, dbModels);
|
|
231
233
|
}
|
|
232
234
|
engine.resetModels = async () => {
|
|
233
235
|
await deleteModels();
|
|
236
|
+
Event.Trigger("OnModelsDeleted", "event", "system", engine);
|
|
234
237
|
};
|
|
235
238
|
return engine;
|
|
236
239
|
}
|
package/src/events.js
CHANGED
|
@@ -4,7 +4,9 @@ const events = {};
|
|
|
4
4
|
const eventLayerSystems = {
|
|
5
5
|
"priority": ["high", "medium", "low"],
|
|
6
6
|
"log": ["info", "debug", "warn", "error", "critical"],
|
|
7
|
-
"system": ["calls"]
|
|
7
|
+
"system": ["calls", "users"],
|
|
8
|
+
"event": ["system","user"],
|
|
9
|
+
"custom": ["data"]
|
|
8
10
|
};
|
|
9
11
|
|
|
10
12
|
|
|
@@ -13,11 +15,12 @@ export const Event = {
|
|
|
13
15
|
Trigger: (name, system = "priority", layer = "medium", ...args) => { // Ajout des arguments system et layer
|
|
14
16
|
if (!events[system] || !events[system][name] || (layer && !events[system][name][layer])) {
|
|
15
17
|
//console.warn(`No trigger found for ${name} in system ${system} layer ${layer}`);
|
|
16
|
-
return;
|
|
18
|
+
return null;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
const systemsToProcess = system ? [system] : Object.keys(events); // Si system est spécifié, on cible ce système uniquement, sinon tous les systèmes
|
|
20
22
|
|
|
23
|
+
let ret = null;
|
|
21
24
|
for (const currentSystem of systemsToProcess) {
|
|
22
25
|
if (events[currentSystem] && events[currentSystem][name]) {
|
|
23
26
|
const layersToProcess = layer ? [layer] : eventLayerSystems[currentSystem] || Object.keys(events[currentSystem][name]); // Si layer est spécifié, on cible cette couche, sinon toutes les couches ou celles définies dans eventLayerSystems
|
|
@@ -27,7 +30,25 @@ export const Event = {
|
|
|
27
30
|
if (events[currentSystem][name][currentLayer]) {
|
|
28
31
|
for (const callback of events[currentSystem][name][currentLayer]) {
|
|
29
32
|
try {
|
|
30
|
-
callback(...args);
|
|
33
|
+
const res = callback(...args);
|
|
34
|
+
if (typeof res === "object" && !Array.isArray(res)) {
|
|
35
|
+
if (typeof ret !== "object") ret = {};
|
|
36
|
+
ret = {...ret, ...res};
|
|
37
|
+
} else if (Array.isArray(res)) {
|
|
38
|
+
if (!Array.isArray(ret)) ret = [];
|
|
39
|
+
ret = ret.concat(res);
|
|
40
|
+
} else if (typeof res === "string") {
|
|
41
|
+
if (typeof ret !== "string") ret = "";
|
|
42
|
+
ret += res;
|
|
43
|
+
} else if (typeof res === "number") {
|
|
44
|
+
if (typeof ret !== "number") ret = 0;
|
|
45
|
+
ret += res;
|
|
46
|
+
} else if (typeof res === "boolean") {
|
|
47
|
+
if (typeof ret !== "boolean") ret = true;
|
|
48
|
+
ret = res && ret;
|
|
49
|
+
} else {
|
|
50
|
+
ret = res || ret;
|
|
51
|
+
}
|
|
31
52
|
} catch (error) {
|
|
32
53
|
console.error(`Error in callback for event ${name} in system ${currentSystem} layer ${currentLayer}:`, error);
|
|
33
54
|
}
|
|
@@ -37,6 +58,7 @@ export const Event = {
|
|
|
37
58
|
}
|
|
38
59
|
}
|
|
39
60
|
}
|
|
61
|
+
return ret;
|
|
40
62
|
},
|
|
41
63
|
Listen: (name = "", callback = () => {}, system = "priority", layer = "medium") => {
|
|
42
64
|
const validSystems = Object.keys(eventLayerSystems); // Récupération des clés pour une vérification plus performante
|
package/src/modules/bucket.js
CHANGED
|
@@ -3,7 +3,6 @@ import AWS from "aws-sdk";
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import {decryptValue, encryptValue} from "../data.js";
|
|
6
|
-
import {MongoClient} from "../engine.js";
|
|
7
6
|
import {loadFromDump, validateRestoreRequest} from "./data.js";
|
|
8
7
|
import {Logger} from "../gameObject.js";
|
|
9
8
|
import {middlewareAuthenticator, userInitiator} from "./user.js";
|
|
@@ -12,6 +11,7 @@ import crypto from "node:crypto";
|
|
|
12
11
|
import i18n from "../../src/i18n.js";
|
|
13
12
|
import {sendEmail} from "../email.js";
|
|
14
13
|
import {throttleMiddleware} from "../middlewares/throttle.js";
|
|
14
|
+
import {getCollectionForUser} from "./mongodb.js";
|
|
15
15
|
|
|
16
16
|
const restoreRequests = {};
|
|
17
17
|
|
|
@@ -54,14 +54,77 @@ const getDefaultS3Config = () => {
|
|
|
54
54
|
bucketName: process.env.AWS_BUCKET || awsDefaultConfig.bucketName
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
|
-
const getS3Client = (s3Config) => {
|
|
58
57
|
|
|
59
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Récupère la configuration S3 depuis les variables d'environnement de l'utilisateur en base de données.
|
|
60
|
+
* @param {object} user - L'objet utilisateur.
|
|
61
|
+
* @returns {Promise<object>} - Un objet contenant la configuration S3 trouvée pour l'utilisateur.
|
|
62
|
+
*/
|
|
63
|
+
async function _getUserS3ConfigFromDb(user) {
|
|
64
|
+
if (!user || !user.username) {
|
|
65
|
+
return {}; // Pas d'utilisateur, pas de configuration spécifique.
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const collection = await getCollectionForUser(user);
|
|
70
|
+
const userEnvVars = await collection.find({
|
|
71
|
+
_model: 'env',
|
|
72
|
+
_user: user.username,
|
|
73
|
+
// On ne cherche que les clés pertinentes pour optimiser la requête
|
|
74
|
+
key: { $in: ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_REGION', 'AWS_BUCKET_NAME'] }
|
|
75
|
+
}).toArray();
|
|
76
|
+
|
|
77
|
+
// Transforme le tableau de documents [{key, value}, ...] en un objet de configuration.
|
|
78
|
+
return userEnvVars.reduce((config, envVar) => {
|
|
79
|
+
if (envVar.key === 'AWS_ACCESS_KEY_ID') config.accessKeyId = envVar.value;
|
|
80
|
+
if (envVar.key === 'AWS_SECRET_ACCESS_KEY') config.secretAccessKey = envVar.value; // La clé est déjà chiffrée en BDD
|
|
81
|
+
if (envVar.key === 'AWS_REGION') config.region = envVar.value;
|
|
82
|
+
if (envVar.key === 'AWS_BUCKET_NAME') config.bucketName = envVar.value;
|
|
83
|
+
return config;
|
|
84
|
+
}, {});
|
|
60
85
|
|
|
61
|
-
|
|
86
|
+
} catch (error) {
|
|
87
|
+
logger.error(`Failed to fetch user S3 config from DB for ${user.username}:`, error);
|
|
88
|
+
return {}; // Retourne un objet vide en cas d'erreur pour utiliser le fallback.
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Récupère la configuration S3 effective en priorisant celle de l'utilisateur
|
|
94
|
+
* puis en se rabattant sur la configuration globale de l'environnement.
|
|
95
|
+
* C'est la fonction à utiliser pour toute opération S3.
|
|
96
|
+
* @param {object} user - L'objet utilisateur.
|
|
97
|
+
* @returns {Promise<object>} - L'objet de configuration S3 final.
|
|
98
|
+
*/
|
|
99
|
+
export async function getUserS3Config(user) {
|
|
100
|
+
// 1. Récupérer la configuration globale par défaut
|
|
101
|
+
const defaultConfig = {
|
|
102
|
+
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
103
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
|
104
|
+
region: process.env.AWS_REGION || awsDefaultConfig.region,
|
|
105
|
+
bucketName: process.env.AWS_BUCKET_NAME || awsDefaultConfig.bucketName
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// 2. Récupérer la configuration spécifique de l'utilisateur
|
|
109
|
+
const userConfig = await _getUserS3ConfigFromDb(user);
|
|
110
|
+
|
|
111
|
+
if( userConfig.bucketName && userConfig.accessKeyId && userConfig.secretAccessKey) {
|
|
112
|
+
// 3. Fusionner les configurations, en donnant la priorité à celle de l'utilisateur
|
|
113
|
+
return {
|
|
114
|
+
accessKeyId: userConfig.accessKeyId || defaultConfig.accessKeyId,
|
|
115
|
+
secretAccessKey: userConfig.secretAccessKey || defaultConfig.secretAccessKey,
|
|
116
|
+
region: userConfig.region || defaultConfig.region,
|
|
117
|
+
bucketName: userConfig.bucketName || defaultConfig.bucketName
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
console.log({defaultConfig})
|
|
121
|
+
return defaultConfig;
|
|
122
|
+
}
|
|
123
|
+
const getS3Client = (s3Config) => {
|
|
124
|
+
|
|
125
|
+
const decryptedSecretAccessKey = s3Config.secretAccessKey ? decryptValue(s3Config.secretAccessKey): undefined;
|
|
62
126
|
|
|
63
127
|
return new AWS.S3({
|
|
64
|
-
...defaultConfig,
|
|
65
128
|
accessKeyId: s3Config.accessKeyId || defaultConfig.accessKeyId,
|
|
66
129
|
secretAccessKey: decryptedSecretAccessKey ? decryptedSecretAccessKey : defaultConfig.secretAccessKey,
|
|
67
130
|
region: s3Config.region || defaultConfig.region,
|
|
@@ -74,6 +137,10 @@ export const uploadToS3 = async (s3Config, filePath, remoteFilename) => {
|
|
|
74
137
|
const fileContent = fs.readFileSync(filePath);
|
|
75
138
|
const bucketPath = s3Config.pathPrefix ? `${s3Config.pathPrefix.replace(/\/$/, "")}/${remoteFilename}` : remoteFilename;
|
|
76
139
|
|
|
140
|
+
if( !s3.accessKeyId || !s3.secretAccessKey || !s3.bucketName) {
|
|
141
|
+
throw new Error('Missing S3 configuration');
|
|
142
|
+
}
|
|
143
|
+
|
|
77
144
|
const params = {
|
|
78
145
|
Bucket: s3Config.bucketName,
|
|
79
146
|
Key: bucketPath,
|
|
@@ -90,6 +157,7 @@ export const uploadToS3 = async (s3Config, filePath, remoteFilename) => {
|
|
|
90
157
|
}
|
|
91
158
|
};
|
|
92
159
|
|
|
160
|
+
|
|
93
161
|
export const listS3Backups = async (s3Config) => {
|
|
94
162
|
const s3 = getS3Client(s3Config);
|
|
95
163
|
const bucketPathPrefix = s3Config.pathPrefix ? `${s3Config.pathPrefix.replace(/\/$/, "")}/` : '';
|
|
@@ -133,6 +201,48 @@ export const downloadFromS3 = async (s3Config, s3FileKey, downloadPath) => {
|
|
|
133
201
|
}
|
|
134
202
|
};
|
|
135
203
|
|
|
204
|
+
export const deleteFromS3 = async (s3Config, remoteFilename) => {
|
|
205
|
+
const s3 = getS3Client(s3Config);
|
|
206
|
+
const bucketPath = s3Config.pathPrefix ? `${s3Config.pathPrefix.replace(/\/$/, "")}/${remoteFilename}` : remoteFilename;
|
|
207
|
+
|
|
208
|
+
const params = {
|
|
209
|
+
Bucket: s3Config.bucketName,
|
|
210
|
+
Key: bucketPath
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
await s3.deleteObject(params).promise();
|
|
215
|
+
console.log(`Fichier supprimé avec succès de S3 : ${bucketPath}`);
|
|
216
|
+
} catch (err) {
|
|
217
|
+
console.error("Erreur lors de la suppression sur S3 :", err);
|
|
218
|
+
throw err;
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Crée un flux de lecture pour un objet depuis un bucket S3.
|
|
224
|
+
* @param {object} s3Config - La configuration S3 (bucketName, accessKeyId, secretAccessKey, region).
|
|
225
|
+
* @param {string} s3Key - La clé (nom du fichier) de l'objet sur S3.
|
|
226
|
+
* @returns {import('stream').Readable} - Le flux de lecture de l'objet S3.
|
|
227
|
+
*/
|
|
228
|
+
export const getS3Stream = (s3Config, s3Key) => {
|
|
229
|
+
if (!s3Config || !s3Config.bucketName || !s3Key) {
|
|
230
|
+
throw new Error("La configuration S3 et la clé de l'objet sont requises pour créer un flux.");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// On utilise la fonction existante qui gère le déchiffrement !
|
|
234
|
+
const s3 = getS3Client(s3Config);
|
|
235
|
+
|
|
236
|
+
const params = {
|
|
237
|
+
Bucket: s3Config.bucketName,
|
|
238
|
+
Key: s3Key
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
// getObject().createReadStream() retourne directement un flux lisible.
|
|
242
|
+
// Les erreurs (ex: objet non trouvé) seront émises sur l'événement 'error' de ce flux.
|
|
243
|
+
return s3.getObject(params).createReadStream();
|
|
244
|
+
};
|
|
245
|
+
|
|
136
246
|
const throttle = throttleMiddleware(maxBytesPerSecondThrottleData);
|
|
137
247
|
|
|
138
248
|
let engine, logger;
|