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
package/src/modules/file.js
CHANGED
|
@@ -13,9 +13,12 @@ import crypto from "node:crypto";
|
|
|
13
13
|
import * as tar from "tar";
|
|
14
14
|
import {promisify} from "node:util";
|
|
15
15
|
import {calculateTotalUserStorageUsage, hasPermission} from "./user.js";
|
|
16
|
+
import {Logger} from "../gameObject.js";
|
|
17
|
+
import {deleteFromS3, getUserS3Config, uploadToS3} from "./bucket.js";
|
|
16
18
|
|
|
17
19
|
const pbkdf2Async = promisify(crypto.pbkdf2);
|
|
18
20
|
|
|
21
|
+
let engine, logger;
|
|
19
22
|
const fsPromises = fs.promises;
|
|
20
23
|
|
|
21
24
|
// Encryption settings
|
|
@@ -64,78 +67,116 @@ export const addFile = async (file, user) => {
|
|
|
64
67
|
throw new Error(i18n.t("api.data.serverStorageFull", "Le serveur a atteint sa capacité de stockage maximale. Veuillez réessayer plus tard."));
|
|
65
68
|
}
|
|
66
69
|
|
|
67
|
-
const collection = getCollection("files");
|
|
68
|
-
|
|
69
70
|
// Générer un GUID pour le fichier
|
|
70
71
|
const guid = uuidv4();
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
72
|
+
const s3Config = await getUserS3Config(user);
|
|
73
|
+
const extension = getFileExtension(file.name);
|
|
74
|
+
const newFilename = `${guid}.${extension}`;
|
|
75
|
+
|
|
76
|
+
const fileData = {
|
|
77
|
+
guid: guid,
|
|
78
|
+
filename: file.name,
|
|
79
|
+
size: file.size,
|
|
80
|
+
mimeType: file.type,
|
|
81
|
+
createdAt: new Date(),
|
|
82
|
+
user: user.username,
|
|
83
|
+
mainUser: user._user
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
if (s3Config && s3Config.bucketName && s3Config.accessKeyId && s3Config.secretAccessKey) { // Correction: bucketName au lieu de bucket
|
|
87
|
+
try {
|
|
88
|
+
// Correction: Appel manquant à la fonction de téléversement
|
|
89
|
+
await uploadToS3(s3Config, file.path, newFilename);
|
|
90
|
+
|
|
91
|
+
fileData.storage = 's3';
|
|
92
|
+
fileData.filename = newFilename; // Le nom sur S3
|
|
93
|
+
logger.info(`Fichier ${newFilename} téléversé sur le bucket S3 ${s3Config.bucketName}.`);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
logger.info(`Le téléversement S3 a échoué pour ${file.name}: ${error.message}`, 'error');
|
|
96
|
+
throw new Error("Le téléversement S3 a échoué.");
|
|
97
|
+
} finally {
|
|
98
|
+
// Nettoyer le fichier temporaire uploadé par express-formidable
|
|
99
|
+
await fsPromises.unlink(file.path).catch(e => logger.info(`Échec de la suppression du fichier temporaire ${file.path}: ${e.message}`, 'error'));
|
|
80
100
|
}
|
|
101
|
+
} else {
|
|
102
|
+
// Sauvegarde locale
|
|
103
|
+
const uploadDir = path.join(process.cwd(), "uploads", "private");
|
|
104
|
+
if (!fs.existsSync(uploadDir)) {
|
|
105
|
+
fs.mkdirSync(uploadDir, { recursive: true });
|
|
106
|
+
}
|
|
107
|
+
const newPath = path.join(uploadDir, newFilename);
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
// express-formidable place déjà le fichier dans un répertoire temporaire. Nous n'avons qu'éplacer.
|
|
111
|
+
await fsPromises.rename(file.path, newPath);
|
|
112
|
+
fileData.storage = 'local';
|
|
113
|
+
fileData.filename = newFilename; // Le nom dans le dossier uploads
|
|
114
|
+
fileData.path = newPath; // Le chemin complet pour les fichiers locaux
|
|
115
|
+
logger.info(`Fichier ${newFilename} sauvegardé localement dans ${newPath}.`);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
logger.info(`Le déplacement du fichier local a échoué pour ${file.name}: ${error.message}`, 'error');
|
|
118
|
+
// Essayer de nettoyer le fichier temporaire même si le renommage échoue
|
|
119
|
+
await fsPromises.unlink(file.path).catch(e => logger.info(`Échec de la suppression du fichier temporaire ${file.path}: ${e.message}`, 'error'));
|
|
120
|
+
throw new Error("Le stockage du fichier local a échoué.");
|
|
121
|
+
}
|
|
122
|
+
}
|
|
81
123
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
guid,
|
|
85
|
-
filename: file.originalname || file.name, // Conserver le nom original
|
|
86
|
-
mimeType: file.type,
|
|
87
|
-
size: file.size,
|
|
88
|
-
mainUser: user._user,
|
|
89
|
-
user: user.username,
|
|
90
|
-
timestamp: new Date()
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
// Insérer le fichier dans une collection dédiée (par exemple, "privateFiles")
|
|
94
|
-
const result = await collection.insertOne({...fileMetadata, _model: "privateFile"});
|
|
95
|
-
if (!result.insertedId) throw new Error("Échec de l'indexation du fichier.");
|
|
124
|
+
const filesCollection = await getCollection("files");
|
|
125
|
+
await filesCollection.insertOne(fileData);
|
|
96
126
|
|
|
97
|
-
|
|
127
|
+
return guid;
|
|
128
|
+
};
|
|
98
129
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
130
|
+
/**
|
|
131
|
+
* Récupère les métadonnées d'un fichier depuis la base de données.
|
|
132
|
+
* @param {string} guid - Le GUID du fichier.
|
|
133
|
+
* @returns {Promise<object|null>} L'objet de métadonnées du fichier ou null si non trouvé.
|
|
134
|
+
*/
|
|
135
|
+
export const getFile = async (guid) => {
|
|
136
|
+
const filesCollection = await getCollection("files");
|
|
137
|
+
return await filesCollection.findOne({ guid });
|
|
106
138
|
};
|
|
107
139
|
|
|
108
140
|
export const removeFile = async (guid, user) => {
|
|
109
141
|
if (!guid) return false;
|
|
110
142
|
if (!isGUID(guid)) throw new Error("Le GUID du fichier n'est pas valide.");
|
|
111
143
|
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (!file) throw new Error("Fichier non trouvé (" + guid + ")");
|
|
117
|
-
|
|
118
|
-
if (user.username !== 'demo' && isLocalUser(user) && !await hasPermission(["API_ADMIN", "API_EDIT_DATA", "API_EDIT_DATA_privateFile", `API_EDIT_DATA_privateFile_${guid}`], user) ) {
|
|
119
|
-
if (file._user !== (user._user || user.username)) {
|
|
120
|
-
throw new Error("Vous n'êtes pas autorisé à supprimer ce fichier.");
|
|
121
|
-
}
|
|
144
|
+
const fileData = await getFile(guid);
|
|
145
|
+
if (!fileData) {
|
|
146
|
+
logger.info(`Tentative de suppression d'un fichier inexistant avec le GUID : ${guid}`, 'warn');
|
|
147
|
+
return;
|
|
122
148
|
}
|
|
123
149
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
150
|
+
if (fileData.storage === 's3') {
|
|
151
|
+
const s3Config = await getUserS3Config(user);
|
|
152
|
+
if (s3Config && s3Config.bucketName) { // Correction: bucketName au lieu de bucket
|
|
153
|
+
try {
|
|
154
|
+
await deleteFromS3(s3Config, fileData.filename);
|
|
155
|
+
logger.info(`Fichier ${fileData.filename} supprimé du bucket S3 ${s3Config.bucketName}.`);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
logger.info(`La suppression S3 a échoué pour ${fileData.filename}: ${error.message}`, 'error');
|
|
158
|
+
throw new Error("La suppression S3 a échoué.");
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
logger.info(`Configuration S3 non trouvée pour l'utilisateur, impossible de supprimer le fichier ${fileData.filename} de S3.`, 'error');
|
|
162
|
+
throw new Error("Configuration S3 non trouvée, impossible de supprimer le fichier.");
|
|
163
|
+
}
|
|
164
|
+
} else if (fileData.storage === 'local') {
|
|
165
|
+
try {
|
|
166
|
+
if (fileData.path && fs.existsSync(fileData.path)) {
|
|
167
|
+
await fsPromises.unlink(fileData.path);
|
|
168
|
+
logger.info(`Fichier local ${fileData.path} supprimé.`);
|
|
169
|
+
} else {
|
|
170
|
+
logger.info(`Fichier local non trouvé au chemin ${fileData.path}, mais suppression de l'enregistrement en BDD.`, 'warn');
|
|
171
|
+
}
|
|
172
|
+
} catch (error) {
|
|
173
|
+
logger.info(`La suppression du fichier local a échoué pour ${fileData.path}: ${error.message}`, 'error');
|
|
174
|
+
throw new Error("La suppression du fichier local a échoué.");
|
|
133
175
|
}
|
|
134
|
-
|
|
135
|
-
return { success: true, message: "Fichier supprimé avec succès." };
|
|
136
|
-
} catch (error) {
|
|
137
|
-
throw new Error(`Erreur lors de la suppression du fichier: ${error.message}`);
|
|
138
176
|
}
|
|
177
|
+
|
|
178
|
+
const filesCollection = await getCollection("files");
|
|
179
|
+
await filesCollection.deleteOne({ guid });
|
|
139
180
|
};
|
|
140
181
|
|
|
141
182
|
|
|
@@ -190,7 +231,7 @@ export async function decryptFile(filePath, password) {
|
|
|
190
231
|
}
|
|
191
232
|
}
|
|
192
233
|
|
|
193
|
-
let engine;
|
|
194
234
|
export async function onInit(defaultEngine) {
|
|
195
235
|
engine = defaultEngine;
|
|
236
|
+
logger = engine.getComponent(Logger);
|
|
196
237
|
}
|
package/src/modules/test
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// src/modules/file.js
|
|
2
|
+
import { getCollection } from "./mongodb.js";
|
|
3
|
+
import { uuidv4, getFileExtension } from "../core.js";
|
|
4
|
+
import { getUserS3Config, uploadToS3, deleteFromS3 } from "./bucket.js";
|
|
5
|
+
import { maxFileSize } from "../constants.js";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import { promises as fsPromises } from "node:fs";
|
|
9
|
+
import { Logger } from "../gameObject.js";
|
|
10
|
+
|
|
11
|
+
let logger;
|
|
12
|
+
|
|
13
|
+
const getLogger = () => {
|
|
14
|
+
if (!logger) {
|
|
15
|
+
logger = new Logger();
|
|
16
|
+
}
|
|
17
|
+
return logger;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Ajoute un fichier au système. Le téléverse sur S3 si configuré, sinon le sauvegarde localement.
|
|
22
|
+
* @param {object} file - L'objet fichier provenant de express-formidable.
|
|
23
|
+
* @param {object} user - L'objet utilisateur.
|
|
24
|
+
* @returns {Promise<string>} Le GUID du fichier nouvellement ajouté.
|
|
25
|
+
*/
|
|
26
|
+
export const addFile = async (file, user) => {
|
|
27
|
+
if (!file) {
|
|
28
|
+
throw new Error("Aucun fichier fourni.");
|
|
29
|
+
}
|
|
30
|
+
if (file.size > maxFileSize) {
|
|
31
|
+
throw new Error(`La taille du fichier dépasse la limite de ${maxFileSize / 1024 / 1024} Mo.`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const guid = uuidv4();
|
|
35
|
+
const extension = getFileExtension(file.name);
|
|
36
|
+
const newFilename = `${guid}.${extension}`;
|
|
37
|
+
const s3Config = await getUserS3Config(user);
|
|
38
|
+
|
|
39
|
+
const fileData = {
|
|
40
|
+
_id: guid,
|
|
41
|
+
guid: guid,
|
|
42
|
+
originalFilename: file.name,
|
|
43
|
+
size: file.size,
|
|
44
|
+
mimetype: file.type,
|
|
45
|
+
createdAt: new Date(),
|
|
46
|
+
user: user.username // Bonne pratique : garder une trace de l'uploader
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
if (s3Config && s3Config.bucketName) { // Correction: bucketName au lieu de bucket
|
|
50
|
+
try {
|
|
51
|
+
// Correction: Appel manquant à la fonction de téléversement
|
|
52
|
+
await uploadToS3(s3Config, file.path, newFilename);
|
|
53
|
+
|
|
54
|
+
fileData.storage = 's3';
|
|
55
|
+
fileData.filename = newFilename; // Le nom sur S3
|
|
56
|
+
getLogger().log(`Fichier ${newFilename} téléversé sur le bucket S3 ${s3Config.bucketName}.`);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
getLogger().log(`Le téléversement S3 a échoué pour ${file.name}: ${error.message}`, 'error');
|
|
59
|
+
throw new Error("Le téléversement S3 a échoué.");
|
|
60
|
+
} finally {
|
|
61
|
+
// Nettoyer le fichier temporaire uploadé par express-formidable
|
|
62
|
+
await fsPromises.unlink(file.path).catch(e => getLogger().log(`Échec de la suppression du fichier temporaire ${file.path}: ${e.message}`, 'error'));
|
|
63
|
+
}
|
|
64
|
+
} else {
|
|
65
|
+
// Sauvegarde locale
|
|
66
|
+
const uploadDir = path.join(process.cwd(), "uploads");
|
|
67
|
+
if (!fs.existsSync(uploadDir)) {
|
|
68
|
+
fs.mkdirSync(uploadDir, { recursive: true });
|
|
69
|
+
}
|
|
70
|
+
const newPath = path.join(uploadDir, newFilename);
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
// express-formidable place déjà le fichier dans un répertoire temporaire. Nous n'avons qu'éplacer.
|
|
74
|
+
await fsPromises.rename(file.path, newPath);
|
|
75
|
+
fileData.storage = 'local';
|
|
76
|
+
fileData.filename = newFilename; // Le nom dans le dossier uploads
|
|
77
|
+
fileData.path = newPath; // Le chemin complet pour les fichiers locaux
|
|
78
|
+
getLogger().log(`Fichier ${newFilename} sauvegardé localement dans ${newPath}.`);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
getLogger().log(`Le déplacement du fichier local a échoué pour ${file.name}: ${error.message}`, 'error');
|
|
81
|
+
// Essayer de nettoyer le fichier temporaire même si le renommage échoue
|
|
82
|
+
await fsPromises.unlink(file.path).catch(e => getLogger().log(`Échec de la suppression du fichier temporaire ${file.path}: ${e.message}`, 'error'));
|
|
83
|
+
throw new Error("Le stockage du fichier local a échoué.");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const filesCollection = await getCollection("files");
|
|
88
|
+
await filesCollection.insertOne(fileData);
|
|
89
|
+
|
|
90
|
+
return guid;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Récupère les métadonnées d'un fichier depuis la base de données.
|
|
95
|
+
* @param {string} guid - Le GUID du fichier.
|
|
96
|
+
* @returns {Promise<object|null>} L'objet de métadonnées du fichier ou null si non trouvé.
|
|
97
|
+
*/
|
|
98
|
+
export const getFile = async (guid) => {
|
|
99
|
+
const filesCollection = await getCollection("files");
|
|
100
|
+
return await filesCollection.findOne({ guid });
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Supprime un fichier du système (S3 ou local) et son enregistrement en base de données.
|
|
105
|
+
* @param {string} guid - Le GUID du fichier à supprimer.
|
|
106
|
+
* @param {object} user - L'objet utilisateur.
|
|
107
|
+
* @returns {Promise<void>}
|
|
108
|
+
*/
|
|
109
|
+
export const deleteFile = async (guid, user) => {
|
|
110
|
+
const fileData = await getFile(guid);
|
|
111
|
+
if (!fileData) {
|
|
112
|
+
getLogger().log(`Tentative de suppression d'un fichier inexistant avec le GUID : ${guid}`, 'warn');
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (fileData.storage === 's3') {
|
|
117
|
+
const s3Config = await getUserS3Config(user);
|
|
118
|
+
if (s3Config && s3Config.bucketName) { // Correction: bucketName au lieu de bucket
|
|
119
|
+
try {
|
|
120
|
+
await deleteFromS3(s3Config, fileData.filename);
|
|
121
|
+
getLogger().log(`Fichier ${fileData.filename} supprimé du bucket S3 ${s3Config.bucketName}.`);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
getLogger().log(`La suppression S3 a échoué pour ${fileData.filename}: ${error.message}`, 'error');
|
|
124
|
+
throw new Error("La suppression S3 a échoué.");
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
getLogger().log(`Configuration S3 non trouvée pour l'utilisateur, impossible de supprimer le fichier ${fileData.filename} de S3.`, 'error');
|
|
128
|
+
throw new Error("Configuration S3 non trouvée, impossible de supprimer le fichier.");
|
|
129
|
+
}
|
|
130
|
+
} else if (fileData.storage === 'local') {
|
|
131
|
+
try {
|
|
132
|
+
if (fileData.path && fs.existsSync(fileData.path)) {
|
|
133
|
+
await fsPromises.unlink(fileData.path);
|
|
134
|
+
getLogger().log(`Fichier local ${fileData.path} supprimé.`);
|
|
135
|
+
} else {
|
|
136
|
+
getLogger().log(`Fichier local non trouvé au chemin ${fileData.path}, mais suppression de l'enregistrement en BDD.`, 'warn');
|
|
137
|
+
}
|
|
138
|
+
} catch (error) {
|
|
139
|
+
getLogger().log(`La suppression du fichier local a échoué pour ${fileData.path}: ${error.message}`, 'error');
|
|
140
|
+
throw new Error("La suppression du fichier local a échoué.");
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const filesCollection = await getCollection("files");
|
|
145
|
+
await filesCollection.deleteOne({ guid });
|
|
146
|
+
getLogger().log(`Enregistrement du fichier ${guid} supprimé de la base de données.`);
|
|
147
|
+
};
|
package/src/modules/user.js
CHANGED
|
@@ -88,73 +88,128 @@ export async function onInit(defaultEngine) {
|
|
|
88
88
|
engine = defaultEngine;
|
|
89
89
|
logger = engine.getComponent(Logger);
|
|
90
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Calcule et retourne l'ensemble des permissions actives pour un utilisateur.
|
|
93
|
+
* Cette fonction interne est la pierre angulaire de la nouvelle logique de permission.
|
|
94
|
+
* 1. Elle récupère toutes les permissions de base issues des rôles de l'utilisateur.
|
|
95
|
+
* 2. Elle applique ensuite les "exceptions" (ajouts ou retraits de permissions) qui sont valides (non expirées).
|
|
96
|
+
* @param {object} user - L'objet utilisateur pour lequel calculer les permissions.
|
|
97
|
+
* @returns {Promise<Set<string>>} Un Set contenant les noms de toutes les permissions actives.
|
|
98
|
+
* @private
|
|
99
|
+
*/
|
|
100
|
+
export async function getUserActivePermissions(user) {
|
|
101
|
+
const datasCollection = await getCollectionForUser(user);
|
|
102
|
+
const now = new Date();
|
|
103
|
+
const activePermissions = new Set();
|
|
91
104
|
|
|
92
|
-
|
|
93
|
-
if
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
try {
|
|
97
|
-
// Si on a une string on le transforme en tableau.
|
|
98
|
-
const permissionNamesArray = Array.isArray(permissionNames) ? permissionNames : [permissionNames];
|
|
99
|
-
const collection = await getCollectionForUser(user);
|
|
105
|
+
// --- ÉTAPE 1: Récupérer les permissions de base des rôles ---
|
|
106
|
+
if (user.roles && user.roles.length > 0) {
|
|
107
|
+
const roleIds = user.roles.map(id => new ObjectId(id));
|
|
100
108
|
|
|
101
|
-
const
|
|
109
|
+
const rolePermissions = await datasCollection.aggregate([
|
|
110
|
+
{ $match: { _id: { $in: roleIds }, _model: "role" } },
|
|
111
|
+
{ $unwind: "$permissions" },
|
|
112
|
+
{ $addFields: { "permissionId": { "$toObjectId": "$permissions" } } },
|
|
102
113
|
{
|
|
103
114
|
$lookup: {
|
|
104
|
-
from:
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
$match: {
|
|
109
|
-
$expr: {
|
|
110
|
-
$and: [
|
|
111
|
-
{ $in: ['$_id', '$$rolesIds'] }
|
|
112
|
-
]
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
},
|
|
116
|
-
{
|
|
117
|
-
$lookup: {
|
|
118
|
-
from: 'datas',
|
|
119
|
-
let: { rolePermissions: {
|
|
120
|
-
"$map": {
|
|
121
|
-
"input": "$permissions",
|
|
122
|
-
"in": { "$toObjectId": "$$this" }
|
|
123
|
-
}
|
|
124
|
-
} },
|
|
125
|
-
pipeline: [
|
|
126
|
-
{
|
|
127
|
-
$match: {
|
|
128
|
-
$expr: {
|
|
129
|
-
$and: [
|
|
130
|
-
{ $in: ['$_id', '$$rolePermissions'] }
|
|
131
|
-
]
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
},
|
|
135
|
-
{ $limit: 1 }
|
|
136
|
-
],
|
|
137
|
-
as: 'permissions'
|
|
138
|
-
}
|
|
139
|
-
},
|
|
140
|
-
{ $unwind: { path: '$permissions', preserveNullAndEmptyArrays: true } }
|
|
141
|
-
],
|
|
142
|
-
as: 'roles'
|
|
115
|
+
from: datasCollection.collectionName, // Utiliser la même collection
|
|
116
|
+
localField: "permissionId",
|
|
117
|
+
foreignField: "_id",
|
|
118
|
+
as: "permissionDoc"
|
|
143
119
|
}
|
|
144
120
|
},
|
|
145
|
-
{ $unwind:
|
|
146
|
-
{ $
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
121
|
+
{ $unwind: "$permissionDoc" },
|
|
122
|
+
{ $group: { _id: "$permissionDoc.name" } }
|
|
123
|
+
]).toArray();
|
|
124
|
+
|
|
125
|
+
rolePermissions.forEach(p => p._id && activePermissions.add(p._id));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// --- ÉTAPE 2: Appliquer les exceptions de permission ---
|
|
129
|
+
const exceptions = await datasCollection.aggregate([
|
|
130
|
+
{
|
|
131
|
+
$match: {
|
|
132
|
+
_model: "userPermission",
|
|
133
|
+
user: user._id, // Pas besoin de convertir en ObjectId si c'est déjà une string
|
|
134
|
+
$or: [
|
|
135
|
+
{ expiresAt: { $exists: false } },
|
|
136
|
+
{ expiresAt: { $gt: now } }
|
|
137
|
+
]
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
$lookup: {
|
|
142
|
+
from: datasCollection.collectionName,
|
|
143
|
+
let: { permissionId: "$permission" },
|
|
144
|
+
pipeline: [
|
|
145
|
+
{
|
|
146
|
+
$match: {
|
|
147
|
+
$expr: {
|
|
148
|
+
$eq: [
|
|
149
|
+
"$_id",
|
|
150
|
+
{ $toObjectId: "$$permissionId" } // Conversion ici
|
|
151
|
+
]
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
{ $project: { name: 1 } }
|
|
156
|
+
],
|
|
157
|
+
as: 'permissionDoc'
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
{ $unwind: '$permissionDoc' }
|
|
161
|
+
]).toArray();
|
|
162
|
+
|
|
163
|
+
// Appliquer les exceptions
|
|
164
|
+
for (const exception of exceptions) {
|
|
165
|
+
const permissionName = exception.permissionDoc?.name;
|
|
166
|
+
if (!permissionName) continue;
|
|
167
|
+
|
|
168
|
+
if (exception.isGranted) {
|
|
169
|
+
activePermissions.add(permissionName);
|
|
170
|
+
} else {
|
|
171
|
+
activePermissions.delete(permissionName);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return activePermissions;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Vérifie si un utilisateur possède au moins une des permissions spécifiées.
|
|
180
|
+
* Cette fonction utilise la nouvelle logique basée sur les rôles et les exceptions de permission.
|
|
181
|
+
* @param {string|string[]} permissionNames - Le nom de la permission ou un tableau de noms.
|
|
182
|
+
* @param {object} user - L'objet utilisateur authentifié.
|
|
183
|
+
* @returns {Promise<boolean>} - True si l'utilisateur a la permission, sinon false.
|
|
184
|
+
*/
|
|
185
|
+
export async function hasPermission(permissionNames, user) {
|
|
186
|
+
// Garde la compatibilité pour les utilisateurs non-locaux (ex: système)
|
|
187
|
+
if (!isLocalUser(user)) {
|
|
188
|
+
const userRoles = new Set(user.roles || []);
|
|
189
|
+
const requiredPermissions = Array.isArray(permissionNames) ? permissionNames : [permissionNames];
|
|
190
|
+
return requiredPermissions.some(p => userRoles.has(p));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const requiredPermissions = Array.isArray(permissionNames) ? permissionNames : [permissionNames];
|
|
195
|
+
// Si aucune permission n'est requise, on autorise
|
|
196
|
+
if (requiredPermissions.length === 0) {
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// 1. Obtenir l'ensemble final et à jour des permissions de l'utilisateur
|
|
201
|
+
const activePermissions = await getUserActivePermissions(user);
|
|
202
|
+
|
|
203
|
+
// 2. Vérifier si au moins une des permissions requises est dans l'ensemble des permissions actives
|
|
204
|
+
return requiredPermissions.some(pName => activePermissions.has(pName));
|
|
205
|
+
|
|
152
206
|
} catch (e) {
|
|
153
|
-
logger.error(e);
|
|
207
|
+
logger.error("Erreur lors de la vérification des permissions :", e);
|
|
154
208
|
return false;
|
|
155
209
|
}
|
|
156
210
|
}
|
|
157
211
|
|
|
212
|
+
|
|
158
213
|
/**
|
|
159
214
|
* Calcule l'utilisation totale de l'espace de stockage pour un utilisateur en octets.
|
|
160
215
|
* Cela inclut la taille des documents dans sa collection de données et la taille de ses fichiers uploadés.
|
|
@@ -70,7 +70,7 @@ beforeAll(async () => {
|
|
|
70
70
|
fs.readdirSync(backupDir).forEach(file => {
|
|
71
71
|
fs.unlinkSync(path.join(backupDir, file));
|
|
72
72
|
});
|
|
73
|
-
vi.stubEnv('
|
|
73
|
+
vi.stubEnv('ENCRYPTION_KEY', '00000000000000000000000000000000');
|
|
74
74
|
vi.stubEnv('OPENAI_API_KEY', '00000000000000000000000000000000');
|
|
75
75
|
// You might need to create a model first if your dumpUserData requires it
|
|
76
76
|
await createModel(testModelDefinition);
|