data-primals-engine 1.5.2 → 1.6.1
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 +938 -915
- package/client/README.md +136 -136
- package/client/index.js +0 -1
- package/client/public/doc/API.postman_collection.json +213 -213
- package/client/public/react.svg +9 -9
- package/client/src/AddWidgetTypeModal.jsx +47 -47
- package/client/src/App.css +42 -42
- package/client/src/App.jsx +92 -150
- package/client/src/App.scss +6 -0
- package/client/src/AssistantChat.jsx +362 -363
- package/client/src/Button.jsx +12 -12
- package/client/src/Dashboard.jsx +480 -480
- package/client/src/DashboardHtmlViewItem.jsx +146 -146
- package/client/src/DashboardView.jsx +654 -654
- package/client/src/DataEditor.jsx +383 -383
- package/client/src/DataLayout.jsx +779 -808
- package/client/src/DataTable.jsx +782 -822
- package/client/src/DataTable.scss +186 -186
- package/client/src/GeolocationField.jsx +93 -93
- package/client/src/HistoryDialog.jsx +307 -307
- package/client/src/HistoryDialog.scss +319 -319
- package/client/src/HtmlViewBuilderModal.jsx +90 -90
- package/client/src/HtmlViewBuilderModal.scss +17 -17
- package/client/src/HtmlViewCard.jsx +43 -43
- package/client/src/ModelCreator.jsx +683 -686
- package/client/src/ModelCreator.scss +1 -1
- package/client/src/ModelCreatorField.jsx +950 -950
- package/client/src/ModelImporter.jsx +3 -0
- package/client/src/ModelList.jsx +280 -280
- package/client/src/Notification.jsx +136 -136
- package/client/src/PackGallery.jsx +391 -391
- package/client/src/PackGallery.scss +231 -231
- package/client/src/Pagination.jsx +143 -143
- package/client/src/RelationField.jsx +354 -354
- package/client/src/RelationSelectorWidget.jsx +172 -172
- package/client/src/RelationValue.jsx +2 -1
- package/client/src/contexts/CommandContext.jsx +260 -0
- package/client/src/contexts/ModelContext.jsx +4 -1
- package/client/src/contexts/UIContext.jsx +72 -72
- package/client/src/filter.js +263 -263
- package/client/src/index.css +90 -90
- package/package.json +6 -5
- package/perf/artillery-hooks.js +37 -37
- package/perf/setup.yml +25 -25
- package/src/constants.js +4 -0
- package/src/defaultModels.js +7 -0
- package/src/engine.js +335 -335
- package/src/events.js +232 -137
- package/src/filter.js +274 -274
- package/src/index.js +1 -0
- package/src/modules/assistant/assistant.js +225 -83
- package/src/modules/auth-google/index.js +50 -50
- package/src/modules/auth-microsoft/index.js +81 -81
- package/src/modules/data/data.core.js +118 -118
- package/src/modules/data/data.history.js +555 -555
- package/src/modules/data/data.operations.js +112 -9
- package/src/modules/data/data.relations.js +686 -686
- package/src/modules/data/data.routes.js +1879 -1879
- package/src/modules/data/data.validation.js +2 -2
- package/src/modules/file.js +247 -247
- package/src/modules/user.js +14 -9
- package/src/packs.js +2 -2
- package/src/providers.js +2 -2
- package/src/services/stripe.js +196 -196
- package/src/sso.js +193 -193
- package/test/data.history.integration.test.js +264 -264
- package/test/data.integration.test.js +1281 -1206
- package/test/events.test.js +108 -10
- package/test/model.integration.test.js +377 -377
- package/test/user.test.js +106 -1
|
@@ -17,7 +17,7 @@ export function onInit(defaultEngine) {
|
|
|
17
17
|
|
|
18
18
|
const objectKeys = Object.keys(modelStructure);
|
|
19
19
|
|
|
20
|
-
if( objectKeys.find(o => !["name", "_user", "icon", "history", "locked", "_id", "description", "maxRequestData", "fields", "tags"].includes(o)) ){
|
|
20
|
+
if( objectKeys.find(o => !["name", "_user", "icon", "history", "locked", "_id", "description", "maxRequestData", "fields", "tags", "constraints"].includes(o)) ){
|
|
21
21
|
throw new Error(i18n.t('api.model.invalidStructure'));
|
|
22
22
|
}
|
|
23
23
|
|
|
@@ -51,7 +51,7 @@ export function onInit(defaultEngine) {
|
|
|
51
51
|
if (!Array.isArray(modelStructure.constraints)) {
|
|
52
52
|
throw new Error('Model "constraints" property must be an array.');
|
|
53
53
|
}
|
|
54
|
-
const fieldNames = new Set(modelStructure.fields.map(f => f.name));
|
|
54
|
+
const fieldNames = new Set(modelStructure.fields.map(f => f.name).concat(['_user']));
|
|
55
55
|
for (const constraint of modelStructure.constraints) {
|
|
56
56
|
if (constraint.type === 'unique') {
|
|
57
57
|
if (!constraint.name || !Array.isArray(constraint.keys) || constraint.keys.length === 0) {
|
package/src/modules/file.js
CHANGED
|
@@ -1,248 +1,248 @@
|
|
|
1
|
-
|
|
2
|
-
// Nouvelle fonction pour ajouter un fichier privé
|
|
3
|
-
import {maxPrivateFileSize, megabytes} from "../constants.js";
|
|
4
|
-
import {isLocalUser} from "../data.js";
|
|
5
|
-
import i18n from "../../src/i18n.js";
|
|
6
|
-
import {getCollection} from "./mongodb.js";
|
|
7
|
-
import {getFileExtension, isGUID, uuidv4} from "../core.js";
|
|
8
|
-
import path from "node:path";
|
|
9
|
-
import process from "node:process";
|
|
10
|
-
import fs from "node:fs";
|
|
11
|
-
import { checkServerCapacity} from "./data/index.js";
|
|
12
|
-
import crypto from "node:crypto";
|
|
13
|
-
import * as tar from "tar";
|
|
14
|
-
import {promisify} from "node:util";
|
|
15
|
-
import {calculateTotalUserStorageUsage, hasPermission} from "./user.js";
|
|
16
|
-
import {Logger} from "../gameObject.js";
|
|
17
|
-
import {deleteFromS3, getUserS3Config, uploadToS3} from "./bucket.js";
|
|
18
|
-
import {Config} from "../config.js";
|
|
19
|
-
|
|
20
|
-
const pbkdf2Async = promisify(crypto.pbkdf2);
|
|
21
|
-
|
|
22
|
-
let engine, logger;
|
|
23
|
-
const fsPromises = fs.promises;
|
|
24
|
-
|
|
25
|
-
// Encryption settings
|
|
26
|
-
const algorithm = 'aes-256-cbc'; // Algorithm to use
|
|
27
|
-
const iterations = 100000; // Number of iterations for PBKDF2
|
|
28
|
-
const keyLength = 32; // Key length for AES-256
|
|
29
|
-
const ivLength = 16; // IV length for AES
|
|
30
|
-
|
|
31
|
-
// Function to derive a key and IV from a passphrase and salt
|
|
32
|
-
async function deriveKeyAndIV(password, salt) {
|
|
33
|
-
const key = await pbkdf2Async(password, salt, iterations, keyLength, 'sha256');
|
|
34
|
-
// L'IV n'est plus dérivé ici.
|
|
35
|
-
return { key };
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
export const unzip = async (file) => {
|
|
40
|
-
await tar.extract({ file: file, gzip: true, sync: true });
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export const zip = async (filename) => {
|
|
44
|
-
await tar.create({ gzip: true, sync: true, file: filename+'.gz' }, [filename])
|
|
45
|
-
}
|
|
46
|
-
export const addFile = async (file, user) => {
|
|
47
|
-
if (!file) throw new Error("Le fichier est requis");
|
|
48
|
-
|
|
49
|
-
// Rendre compatible avec formidable v2 (path, name, type) et v3 (filepath, originalFilename, mimetype)
|
|
50
|
-
const filePath = file.path || file.filepath;
|
|
51
|
-
const originalName = file.name || file.originalFilename;
|
|
52
|
-
const mimeType = file.type || file.mimetype;
|
|
53
|
-
|
|
54
|
-
if (!filePath || !originalName) {
|
|
55
|
-
throw new Error("Les informations du fichier (nom, chemin) sont incomplètes.");
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const m = Config.Get('maxPrivateFileSize', maxPrivateFileSize);
|
|
59
|
-
if (file.size > m) {
|
|
60
|
-
throw new Error(`La taille du fichier dépasse la limite autorisée (${m / megabytes} Mo).`);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (user.username !== 'demo' && isLocalUser(user) && !await hasPermission(["API_ADMIN", "API_UPLOAD_FILE"], user)) {
|
|
64
|
-
throw new Error(i18n.t("api.permission.uploadFile"));
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const incomingDataSize = file.size;
|
|
68
|
-
|
|
69
|
-
const userStorageLimit = await engine.userProvider.getUserStorageLimit(user);
|
|
70
|
-
const currentStorageUsage = await calculateTotalUserStorageUsage(user);
|
|
71
|
-
|
|
72
|
-
if (currentStorageUsage + incomingDataSize > userStorageLimit) {
|
|
73
|
-
throw new Error(i18n.t("api.data.storageLimitExceeded", { limit: Math.round(userStorageLimit / megabytes) }));
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const serverCapacity = await checkServerCapacity(incomingDataSize);
|
|
77
|
-
if (!serverCapacity.isSufficient) {
|
|
78
|
-
throw new Error(i18n.t("api.data.serverStorageFull", "Le serveur a atteint sa capacité de stockage maximale. Veuillez réessayer plus tard."));
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Générer un GUID pour le fichier
|
|
82
|
-
const guid = uuidv4();
|
|
83
|
-
const s3Config = await getUserS3Config(user);
|
|
84
|
-
const extension = getFileExtension(originalName);
|
|
85
|
-
const newFilename = `${guid}.${extension}`;
|
|
86
|
-
|
|
87
|
-
const fileData = {
|
|
88
|
-
guid: guid,
|
|
89
|
-
name: originalName, // Nom original du fichier
|
|
90
|
-
filename: newFilename, // Nom du fichier stocké (GUID + extension)
|
|
91
|
-
size: file.size,
|
|
92
|
-
mimeType: mimeType,
|
|
93
|
-
createdAt: new Date(),
|
|
94
|
-
user: user.username,
|
|
95
|
-
mainUser: user._user
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
if (s3Config && s3Config.bucketName && s3Config.accessKeyId && s3Config.secretAccessKey) {
|
|
99
|
-
try {
|
|
100
|
-
await uploadToS3(s3Config, filePath, newFilename);
|
|
101
|
-
|
|
102
|
-
fileData.storage = 's3';
|
|
103
|
-
logger.info(`Fichier ${newFilename} téléversé sur le bucket S3 ${s3Config.bucketName}.`);
|
|
104
|
-
} catch (error) {
|
|
105
|
-
logger.error(`Le téléversement S3 a échoué pour ${originalName}: ${error.message}`, error);
|
|
106
|
-
throw new Error("Le téléversement S3 a échoué.");
|
|
107
|
-
} finally {
|
|
108
|
-
// Nettoyer le fichier temporaire uploadé par express-formidable
|
|
109
|
-
await fsPromises.unlink(filePath).catch(e => logger.warn(`Échec de la suppression du fichier temporaire ${filePath}: ${e.message}`));
|
|
110
|
-
}
|
|
111
|
-
} else {
|
|
112
|
-
// Sauvegarde locale
|
|
113
|
-
const uploadDir = path.join(process.cwd(), "uploads", "private");
|
|
114
|
-
if (!fs.existsSync(uploadDir)) {
|
|
115
|
-
fs.mkdirSync(uploadDir, { recursive: true });
|
|
116
|
-
}
|
|
117
|
-
const newPath = path.join(uploadDir, newFilename);
|
|
118
|
-
|
|
119
|
-
try {
|
|
120
|
-
// Utiliser copyFile puis unlink au lieu de rename pour éviter les erreurs cross-device (EXDEV)
|
|
121
|
-
// qui peuvent survenir dans des environnements conteneurisés comme GitHub Actions.
|
|
122
|
-
await fsPromises.copyFile(filePath, newPath);
|
|
123
|
-
await fsPromises.unlink(filePath);
|
|
124
|
-
fileData.storage = 'local';
|
|
125
|
-
fileData.path = newPath; // Le chemin complet pour les fichiers locaux
|
|
126
|
-
logger.info(`Fichier ${newFilename} sauvegardé localement dans ${newPath}.`);
|
|
127
|
-
} catch (error) {
|
|
128
|
-
logger.error(`Le stockage du fichier local a échoué pour ${originalName}: ${error.message}`, error);
|
|
129
|
-
// Essayer de nettoyer le fichier temporaire même si le renommage échoue
|
|
130
|
-
await fsPromises.unlink(filePath).catch(e => logger.warn(`Échec de la suppression du fichier temporaire ${filePath}: ${e.message}`));
|
|
131
|
-
throw new Error("Le stockage du fichier local a échoué.");
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const filesCollection = await getCollection("files");
|
|
136
|
-
await filesCollection.insertOne(fileData);
|
|
137
|
-
|
|
138
|
-
return guid;
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Récupère les métadonnées d'un fichier depuis la base de données.
|
|
143
|
-
* @param {string} guid - Le GUID du fichier.
|
|
144
|
-
* @returns {Promise<object|null>} L'objet de métadonnées du fichier ou null si non trouvé.
|
|
145
|
-
*/
|
|
146
|
-
export const getFile = async (guid) => {
|
|
147
|
-
const filesCollection = await getCollection("files");
|
|
148
|
-
return await filesCollection.findOne({ guid });
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
export const removeFile = async (guid, user) => {
|
|
152
|
-
if (!guid) return false;
|
|
153
|
-
if (!isGUID(guid)) throw new Error("Le GUID du fichier n'est pas valide.");
|
|
154
|
-
|
|
155
|
-
const fileData = await getFile(guid);
|
|
156
|
-
if (!fileData) {
|
|
157
|
-
logger.info(`Tentative de suppression d'un fichier inexistant avec le GUID : ${guid}`, 'warn');
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
if (fileData.storage === 's3') {
|
|
162
|
-
const s3Config = await getUserS3Config(user);
|
|
163
|
-
if (s3Config && s3Config.bucketName) { // Correction: bucketName au lieu de bucket
|
|
164
|
-
try {
|
|
165
|
-
await deleteFromS3(s3Config, fileData.filename);
|
|
166
|
-
logger.info(`Fichier ${fileData.filename} supprimé du bucket S3 ${s3Config.bucketName}.`);
|
|
167
|
-
} catch (error) {
|
|
168
|
-
logger.info(`La suppression S3 a échoué pour ${fileData.filename}: ${error.message}`, 'error');
|
|
169
|
-
throw new Error("La suppression S3 a échoué.");
|
|
170
|
-
}
|
|
171
|
-
} else {
|
|
172
|
-
logger.info(`Configuration S3 non trouvée pour l'utilisateur, impossible de supprimer le fichier ${fileData.filename} de S3.`, 'error');
|
|
173
|
-
throw new Error("Configuration S3 non trouvée, impossible de supprimer le fichier.");
|
|
174
|
-
}
|
|
175
|
-
} else if (fileData.storage === 'local') {
|
|
176
|
-
try {
|
|
177
|
-
if (fileData.path && fs.existsSync(fileData.path)) {
|
|
178
|
-
await fsPromises.unlink(fileData.path);
|
|
179
|
-
logger.info(`Fichier local ${fileData.path} supprimé.`);
|
|
180
|
-
} else {
|
|
181
|
-
logger.info(`Fichier local non trouvé au chemin ${fileData.path}, mais suppression de l'enregistrement en BDD.`, 'warn');
|
|
182
|
-
}
|
|
183
|
-
} catch (error) {
|
|
184
|
-
logger.info(`La suppression du fichier local a échoué pour ${fileData.path}: ${error.message}`, 'error');
|
|
185
|
-
throw new Error("La suppression du fichier local a échoué.");
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const filesCollection = await getCollection("files");
|
|
190
|
-
await filesCollection.deleteOne({ guid });
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
// Function to encrypt the file content
|
|
197
|
-
export async function encryptFile(filePath, password) {
|
|
198
|
-
try {
|
|
199
|
-
const salt = crypto.randomBytes(16);
|
|
200
|
-
const iv = crypto.randomBytes(ivLength);
|
|
201
|
-
|
|
202
|
-
// On ne dérive que la clé
|
|
203
|
-
const { key } = await deriveKeyAndIV(password, salt);
|
|
204
|
-
|
|
205
|
-
const fileData = await fs.promises.readFile(filePath);
|
|
206
|
-
const cipher = crypto.createCipheriv(algorithm, key, iv);
|
|
207
|
-
const encryptedData = Buffer.concat([cipher.update(fileData), cipher.final()]);
|
|
208
|
-
|
|
209
|
-
// On it : [salt][iv][données chiffrées]
|
|
210
|
-
await fs.promises.writeFile(filePath, Buffer.concat([salt, iv, encryptedData]));
|
|
211
|
-
|
|
212
|
-
console.log('File encrypted successfully.', filePath);
|
|
213
|
-
} catch (error) {
|
|
214
|
-
console.error('Error during encryption:', error.message);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
// Function to decrypt the file content
|
|
218
|
-
export async function decryptFile(filePath, password) {
|
|
219
|
-
try {
|
|
220
|
-
const fileData = await fs.promises.readFile(filePath);
|
|
221
|
-
|
|
222
|
-
// Extraire le sel, l'IV et les données
|
|
223
|
-
const salt = fileData.slice(0, 16);
|
|
224
|
-
// NOUVEAU: Extraire l'IV qui suit le sel
|
|
225
|
-
const iv = fileData.slice(16, 16 + ivLength);
|
|
226
|
-
const encryptedData = fileData.slice(16 + ivLength);
|
|
227
|
-
|
|
228
|
-
// On dérive la même clé en utilisant le sel extrait
|
|
229
|
-
const { key } = await deriveKeyAndIV(password, salt);
|
|
230
|
-
|
|
231
|
-
// On utilise l'IV extrait pour le déchiffrement
|
|
232
|
-
const decipher = crypto.createDecipheriv(algorithm, key, iv);
|
|
233
|
-
const decryptedData = Buffer.concat([decipher.update(encryptedData), decipher.final()]);
|
|
234
|
-
|
|
235
|
-
await fs.promises.writeFile(filePath, decryptedData);
|
|
236
|
-
|
|
237
|
-
console.log('File decrypted successfully.');
|
|
238
|
-
} catch (error) {
|
|
239
|
-
console.error('Error during decryption:', error.message);
|
|
240
|
-
// Relancer l'erreur peut être utile pour que l'appelant sache que ça a échoué
|
|
241
|
-
throw new Error(`Decryption failed: ${error.message}`);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
export async function onInit(defaultEngine) {
|
|
246
|
-
engine = defaultEngine;
|
|
247
|
-
logger = engine.getComponent(Logger);
|
|
1
|
+
|
|
2
|
+
// Nouvelle fonction pour ajouter un fichier privé
|
|
3
|
+
import {maxPrivateFileSize, megabytes} from "../constants.js";
|
|
4
|
+
import {isLocalUser} from "../data.js";
|
|
5
|
+
import i18n from "../../src/i18n.js";
|
|
6
|
+
import {getCollection} from "./mongodb.js";
|
|
7
|
+
import {getFileExtension, isGUID, uuidv4} from "../core.js";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import process from "node:process";
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import { checkServerCapacity} from "./data/index.js";
|
|
12
|
+
import crypto from "node:crypto";
|
|
13
|
+
import * as tar from "tar";
|
|
14
|
+
import {promisify} from "node:util";
|
|
15
|
+
import {calculateTotalUserStorageUsage, hasPermission} from "./user.js";
|
|
16
|
+
import {Logger} from "../gameObject.js";
|
|
17
|
+
import {deleteFromS3, getUserS3Config, uploadToS3} from "./bucket.js";
|
|
18
|
+
import {Config} from "../config.js";
|
|
19
|
+
|
|
20
|
+
const pbkdf2Async = promisify(crypto.pbkdf2);
|
|
21
|
+
|
|
22
|
+
let engine, logger;
|
|
23
|
+
const fsPromises = fs.promises;
|
|
24
|
+
|
|
25
|
+
// Encryption settings
|
|
26
|
+
const algorithm = 'aes-256-cbc'; // Algorithm to use
|
|
27
|
+
const iterations = 100000; // Number of iterations for PBKDF2
|
|
28
|
+
const keyLength = 32; // Key length for AES-256
|
|
29
|
+
const ivLength = 16; // IV length for AES
|
|
30
|
+
|
|
31
|
+
// Function to derive a key and IV from a passphrase and salt
|
|
32
|
+
async function deriveKeyAndIV(password, salt) {
|
|
33
|
+
const key = await pbkdf2Async(password, salt, iterations, keyLength, 'sha256');
|
|
34
|
+
// L'IV n'est plus dérivé ici.
|
|
35
|
+
return { key };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
export const unzip = async (file) => {
|
|
40
|
+
await tar.extract({ file: file, gzip: true, sync: true });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const zip = async (filename) => {
|
|
44
|
+
await tar.create({ gzip: true, sync: true, file: filename+'.gz' }, [filename])
|
|
45
|
+
}
|
|
46
|
+
export const addFile = async (file, user) => {
|
|
47
|
+
if (!file) throw new Error("Le fichier est requis");
|
|
48
|
+
|
|
49
|
+
// Rendre compatible avec formidable v2 (path, name, type) et v3 (filepath, originalFilename, mimetype)
|
|
50
|
+
const filePath = file.path || file.filepath;
|
|
51
|
+
const originalName = file.name || file.originalFilename;
|
|
52
|
+
const mimeType = file.type || file.mimetype;
|
|
53
|
+
|
|
54
|
+
if (!filePath || !originalName) {
|
|
55
|
+
throw new Error("Les informations du fichier (nom, chemin) sont incomplètes.");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const m = Config.Get('maxPrivateFileSize', maxPrivateFileSize);
|
|
59
|
+
if (file.size > m) {
|
|
60
|
+
throw new Error(`La taille du fichier dépasse la limite autorisée (${m / megabytes} Mo).`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (user.username !== 'demo' && isLocalUser(user) && !await hasPermission(["API_ADMIN", "API_UPLOAD_FILE"], user)) {
|
|
64
|
+
throw new Error(i18n.t("api.permission.uploadFile"));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const incomingDataSize = file.size;
|
|
68
|
+
|
|
69
|
+
const userStorageLimit = await engine.userProvider.getUserStorageLimit(user);
|
|
70
|
+
const currentStorageUsage = await calculateTotalUserStorageUsage(user);
|
|
71
|
+
|
|
72
|
+
if (currentStorageUsage + incomingDataSize > userStorageLimit) {
|
|
73
|
+
throw new Error(i18n.t("api.data.storageLimitExceeded", { limit: Math.round(userStorageLimit / megabytes) }));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const serverCapacity = await checkServerCapacity(incomingDataSize);
|
|
77
|
+
if (!serverCapacity.isSufficient) {
|
|
78
|
+
throw new Error(i18n.t("api.data.serverStorageFull", "Le serveur a atteint sa capacité de stockage maximale. Veuillez réessayer plus tard."));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Générer un GUID pour le fichier
|
|
82
|
+
const guid = uuidv4();
|
|
83
|
+
const s3Config = await getUserS3Config(user);
|
|
84
|
+
const extension = getFileExtension(originalName);
|
|
85
|
+
const newFilename = `${guid}.${extension}`;
|
|
86
|
+
|
|
87
|
+
const fileData = {
|
|
88
|
+
guid: guid,
|
|
89
|
+
name: originalName, // Nom original du fichier
|
|
90
|
+
filename: newFilename, // Nom du fichier stocké (GUID + extension)
|
|
91
|
+
size: file.size,
|
|
92
|
+
mimeType: mimeType,
|
|
93
|
+
createdAt: new Date(),
|
|
94
|
+
user: user.username,
|
|
95
|
+
mainUser: user._user
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
if (s3Config && s3Config.bucketName && s3Config.accessKeyId && s3Config.secretAccessKey) {
|
|
99
|
+
try {
|
|
100
|
+
await uploadToS3(s3Config, filePath, newFilename);
|
|
101
|
+
|
|
102
|
+
fileData.storage = 's3';
|
|
103
|
+
logger.info(`Fichier ${newFilename} téléversé sur le bucket S3 ${s3Config.bucketName}.`);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
logger.error(`Le téléversement S3 a échoué pour ${originalName}: ${error.message}`, error);
|
|
106
|
+
throw new Error("Le téléversement S3 a échoué.");
|
|
107
|
+
} finally {
|
|
108
|
+
// Nettoyer le fichier temporaire uploadé par express-formidable
|
|
109
|
+
await fsPromises.unlink(filePath).catch(e => logger.warn(`Échec de la suppression du fichier temporaire ${filePath}: ${e.message}`));
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
// Sauvegarde locale
|
|
113
|
+
const uploadDir = path.join(process.cwd(), "uploads", "private");
|
|
114
|
+
if (!fs.existsSync(uploadDir)) {
|
|
115
|
+
fs.mkdirSync(uploadDir, { recursive: true });
|
|
116
|
+
}
|
|
117
|
+
const newPath = path.join(uploadDir, newFilename);
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
// Utiliser copyFile puis unlink au lieu de rename pour éviter les erreurs cross-device (EXDEV)
|
|
121
|
+
// qui peuvent survenir dans des environnements conteneurisés comme GitHub Actions.
|
|
122
|
+
await fsPromises.copyFile(filePath, newPath);
|
|
123
|
+
await fsPromises.unlink(filePath);
|
|
124
|
+
fileData.storage = 'local';
|
|
125
|
+
fileData.path = newPath; // Le chemin complet pour les fichiers locaux
|
|
126
|
+
logger.info(`Fichier ${newFilename} sauvegardé localement dans ${newPath}.`);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
logger.error(`Le stockage du fichier local a échoué pour ${originalName}: ${error.message}`, error);
|
|
129
|
+
// Essayer de nettoyer le fichier temporaire même si le renommage échoue
|
|
130
|
+
await fsPromises.unlink(filePath).catch(e => logger.warn(`Échec de la suppression du fichier temporaire ${filePath}: ${e.message}`));
|
|
131
|
+
throw new Error("Le stockage du fichier local a échoué.");
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const filesCollection = await getCollection("files");
|
|
136
|
+
await filesCollection.insertOne(fileData);
|
|
137
|
+
|
|
138
|
+
return guid;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Récupère les métadonnées d'un fichier depuis la base de données.
|
|
143
|
+
* @param {string} guid - Le GUID du fichier.
|
|
144
|
+
* @returns {Promise<object|null>} L'objet de métadonnées du fichier ou null si non trouvé.
|
|
145
|
+
*/
|
|
146
|
+
export const getFile = async (guid) => {
|
|
147
|
+
const filesCollection = await getCollection("files");
|
|
148
|
+
return await filesCollection.findOne({ guid });
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export const removeFile = async (guid, user) => {
|
|
152
|
+
if (!guid) return false;
|
|
153
|
+
if (!isGUID(guid)) throw new Error("Le GUID du fichier n'est pas valide.");
|
|
154
|
+
|
|
155
|
+
const fileData = await getFile(guid);
|
|
156
|
+
if (!fileData) {
|
|
157
|
+
logger.info(`Tentative de suppression d'un fichier inexistant avec le GUID : ${guid}`, 'warn');
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (fileData.storage === 's3') {
|
|
162
|
+
const s3Config = await getUserS3Config(user);
|
|
163
|
+
if (s3Config && s3Config.bucketName) { // Correction: bucketName au lieu de bucket
|
|
164
|
+
try {
|
|
165
|
+
await deleteFromS3(s3Config, fileData.filename);
|
|
166
|
+
logger.info(`Fichier ${fileData.filename} supprimé du bucket S3 ${s3Config.bucketName}.`);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
logger.info(`La suppression S3 a échoué pour ${fileData.filename}: ${error.message}`, 'error');
|
|
169
|
+
throw new Error("La suppression S3 a échoué.");
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
logger.info(`Configuration S3 non trouvée pour l'utilisateur, impossible de supprimer le fichier ${fileData.filename} de S3.`, 'error');
|
|
173
|
+
throw new Error("Configuration S3 non trouvée, impossible de supprimer le fichier.");
|
|
174
|
+
}
|
|
175
|
+
} else if (fileData.storage === 'local') {
|
|
176
|
+
try {
|
|
177
|
+
if (fileData.path && fs.existsSync(fileData.path)) {
|
|
178
|
+
await fsPromises.unlink(fileData.path);
|
|
179
|
+
logger.info(`Fichier local ${fileData.path} supprimé.`);
|
|
180
|
+
} else {
|
|
181
|
+
logger.info(`Fichier local non trouvé au chemin ${fileData.path}, mais suppression de l'enregistrement en BDD.`, 'warn');
|
|
182
|
+
}
|
|
183
|
+
} catch (error) {
|
|
184
|
+
logger.info(`La suppression du fichier local a échoué pour ${fileData.path}: ${error.message}`, 'error');
|
|
185
|
+
throw new Error("La suppression du fichier local a échoué.");
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const filesCollection = await getCollection("files");
|
|
190
|
+
await filesCollection.deleteOne({ guid });
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
// Function to encrypt the file content
|
|
197
|
+
export async function encryptFile(filePath, password) {
|
|
198
|
+
try {
|
|
199
|
+
const salt = crypto.randomBytes(16);
|
|
200
|
+
const iv = crypto.randomBytes(ivLength);
|
|
201
|
+
|
|
202
|
+
// On ne dérive que la clé
|
|
203
|
+
const { key } = await deriveKeyAndIV(password, salt);
|
|
204
|
+
|
|
205
|
+
const fileData = await fs.promises.readFile(filePath);
|
|
206
|
+
const cipher = crypto.createCipheriv(algorithm, key, iv);
|
|
207
|
+
const encryptedData = Buffer.concat([cipher.update(fileData), cipher.final()]);
|
|
208
|
+
|
|
209
|
+
// On it : [salt][iv][données chiffrées]
|
|
210
|
+
await fs.promises.writeFile(filePath, Buffer.concat([salt, iv, encryptedData]));
|
|
211
|
+
|
|
212
|
+
console.log('File encrypted successfully.', filePath);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
console.error('Error during encryption:', error.message);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// Function to decrypt the file content
|
|
218
|
+
export async function decryptFile(filePath, password) {
|
|
219
|
+
try {
|
|
220
|
+
const fileData = await fs.promises.readFile(filePath);
|
|
221
|
+
|
|
222
|
+
// Extraire le sel, l'IV et les données
|
|
223
|
+
const salt = fileData.slice(0, 16);
|
|
224
|
+
// NOUVEAU: Extraire l'IV qui suit le sel
|
|
225
|
+
const iv = fileData.slice(16, 16 + ivLength);
|
|
226
|
+
const encryptedData = fileData.slice(16 + ivLength);
|
|
227
|
+
|
|
228
|
+
// On dérive la même clé en utilisant le sel extrait
|
|
229
|
+
const { key } = await deriveKeyAndIV(password, salt);
|
|
230
|
+
|
|
231
|
+
// On utilise l'IV extrait pour le déchiffrement
|
|
232
|
+
const decipher = crypto.createDecipheriv(algorithm, key, iv);
|
|
233
|
+
const decryptedData = Buffer.concat([decipher.update(encryptedData), decipher.final()]);
|
|
234
|
+
|
|
235
|
+
await fs.promises.writeFile(filePath, decryptedData);
|
|
236
|
+
|
|
237
|
+
console.log('File decrypted successfully.');
|
|
238
|
+
} catch (error) {
|
|
239
|
+
console.error('Error during decryption:', error.message);
|
|
240
|
+
// Relancer l'erreur peut être utile pour que l'appelant sache que ça a échoué
|
|
241
|
+
throw new Error(`Decryption failed: ${error.message}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export async function onInit(defaultEngine) {
|
|
246
|
+
engine = defaultEngine;
|
|
247
|
+
logger = engine.getComponent(Logger);
|
|
248
248
|
}
|
package/src/modules/user.js
CHANGED
|
@@ -108,7 +108,7 @@ export async function onInit(defaultEngine) {
|
|
|
108
108
|
* @returns {Promise<Set<string>>} Un Set contenant les noms de toutes les permissions actives.
|
|
109
109
|
* @private
|
|
110
110
|
*/
|
|
111
|
-
export async function getUserActivePermissions(user) {
|
|
111
|
+
export async function getUserActivePermissions(user, env = null) {
|
|
112
112
|
const datasCollection = await getCollectionForUser(user);
|
|
113
113
|
const now = new Date();
|
|
114
114
|
const activePermissions = new Set();
|
|
@@ -139,12 +139,17 @@ export async function getUserActivePermissions(user) {
|
|
|
139
139
|
// --- ÉTAPE 2: Appliquer les exceptions de permission ---
|
|
140
140
|
const exceptions = await datasCollection.aggregate([
|
|
141
141
|
{
|
|
142
|
-
$match: {
|
|
142
|
+
$match: { // Filtre de base pour les exceptions de l'utilisateur
|
|
143
143
|
_model: "userPermission",
|
|
144
|
-
user: user._id,
|
|
145
|
-
$
|
|
146
|
-
{
|
|
147
|
-
|
|
144
|
+
user: user._id,
|
|
145
|
+
$and: [ // Filtre sur l'environnement et l'expiration
|
|
146
|
+
{
|
|
147
|
+
$or: [ // La permission est soit globale, soit spécifique à l'environnement demandé
|
|
148
|
+
{ env: { $exists: false } },
|
|
149
|
+
{ env: env }
|
|
150
|
+
]
|
|
151
|
+
},
|
|
152
|
+
{ $or: [{ expiresAt: { $exists: false } }, { expiresAt: { $gt: now } }] }
|
|
148
153
|
]
|
|
149
154
|
}
|
|
150
155
|
},
|
|
@@ -193,7 +198,7 @@ export async function getUserActivePermissions(user) {
|
|
|
193
198
|
* @param {object} user - L'objet utilisateur authentifié.
|
|
194
199
|
* @returns {Promise<boolean>} - True si l'utilisateur a la permission, sinon false.
|
|
195
200
|
*/
|
|
196
|
-
export async function hasPermission(permissionNames, user) {
|
|
201
|
+
export async function hasPermission(permissionNames, user, env = null) {
|
|
197
202
|
// Garde la compatibilité pour les utilisateurs non-locaux (ex: système)
|
|
198
203
|
if (!isLocalUser(user)) {
|
|
199
204
|
const userRoles = new Set(user.roles || []);
|
|
@@ -209,10 +214,10 @@ export async function hasPermission(permissionNames, user) {
|
|
|
209
214
|
}
|
|
210
215
|
|
|
211
216
|
// 1. Obtenir l'ensemble final et à jour des permissions de l'utilisateur
|
|
212
|
-
const activePermissions = await getUserActivePermissions(user);
|
|
217
|
+
const activePermissions = await getUserActivePermissions(user, env);
|
|
213
218
|
|
|
214
219
|
// 2. Vérifier si au moins une des permissions requises est dans l'ensemble des permissions actives
|
|
215
|
-
return requiredPermissions.some(pName => activePermissions.has(pName));
|
|
220
|
+
return requiredPermissions.some(pName => activePermissions.has(pName)) || false;
|
|
216
221
|
|
|
217
222
|
} catch (e) {
|
|
218
223
|
logger.error("Erreur lors de la vérification des permissions :", e);
|
package/src/packs.js
CHANGED
|
@@ -348,8 +348,8 @@ This pack sets up an automated ecosystem for your store:
|
|
|
348
348
|
"env":envSmtp,
|
|
349
349
|
"taxonomy": [
|
|
350
350
|
{ "name": "E-commerce", "type": "category" },
|
|
351
|
-
{ "name": "Clothes", "type": "category", "parent": { "$
|
|
352
|
-
{ "name": "Electronics", "type": "category", "parent": { "$
|
|
351
|
+
{ "name": "Clothes", "type": "category", "parent": { "$link": { "name": "E-commerce" } } },
|
|
352
|
+
{ "name": "Electronics", "type": "category", "parent": { "$link": { "name": "E-commerce" } } }
|
|
353
353
|
],
|
|
354
354
|
"brand": [
|
|
355
355
|
{ "name": "Brand A" },
|
package/src/providers.js
CHANGED
|
@@ -169,7 +169,7 @@ export class DefaultUserProvider extends UserProvider {
|
|
|
169
169
|
}
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
// Priorité 2:
|
|
172
|
+
// Priorité 2: Pas de session, mais on vérifie la présence d'un cookie "username" pour la démo.
|
|
173
173
|
const demoUsername = req.cookies?.username;
|
|
174
174
|
if (demoUsername && typeof demoUsername === 'string' && this.isDemoUser(demoUsername)) {
|
|
175
175
|
// On a trouvé un cookie de démo. On crée l'objet utilisateur correspondant.
|
|
@@ -180,7 +180,7 @@ export class DefaultUserProvider extends UserProvider {
|
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
-
// Priorité 2: PasF de session, mais on vérifie la présence d'un
|
|
183
|
+
// Priorité 2: PasF de session, mais on vérifie la présence d'un user dans la requete
|
|
184
184
|
const user = req.query._user;
|
|
185
185
|
if (user && typeof user === 'string' && this.isDemoUser(user)) {
|
|
186
186
|
const demoUser = await this.findUserByUsername(user);
|