data-primals-engine 1.6.2 → 1.6.5

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 (39) hide show
  1. package/README.md +56 -911
  2. package/client/package-lock.json +64 -50
  3. package/client/package.json +6 -0
  4. package/client/src/App.scss +29 -0
  5. package/client/src/Dashboard.jsx +1 -1
  6. package/client/src/WorkflowEditor.jsx +101 -0
  7. package/client/src/WorkflowEditor.scss +29 -4
  8. package/client/src/_variables.scss +3 -0
  9. package/doc/automation-workflows.md +102 -0
  10. package/doc/core-concepts.md +33 -0
  11. package/doc/custom-api-endpoints.md +40 -0
  12. package/doc/dashboards-kpis-charts.md +49 -0
  13. package/doc/data-management.md +120 -0
  14. package/doc/data-models.md +75 -0
  15. package/doc/index.md +14 -0
  16. package/doc/roles-permissions.md +43 -0
  17. package/doc/users.md +30 -0
  18. package/package.json +7 -5
  19. package/src/client.js +6 -4
  20. package/src/core.js +31 -12
  21. package/src/defaultModels +1628 -0
  22. package/src/defaultModels.js +14 -0
  23. package/src/filter.js +110 -42
  24. package/src/modules/data/data.history.js +5 -1
  25. package/src/modules/data/data.js +2 -1
  26. package/src/modules/data/data.operations.js +116 -72
  27. package/src/modules/data/data.relations.js +1 -1
  28. package/src/modules/mongodb.js +2 -1
  29. package/src/modules/swagger.js +25 -5
  30. package/src/modules/user.js +138 -76
  31. package/src/modules/workflow.js +187 -177
  32. package/src/providers.js +1 -1
  33. package/swagger-en.yml +2308 -472
  34. package/swagger-fr.yml +487 -3
  35. package/test/core.test.js +341 -0
  36. package/test/data.history.integration.test.js +140 -16
  37. package/test/data.integration.test.js +137 -2
  38. package/test/user.test.js +201 -280
  39. package/test/workflow.integration.test.js +8 -0
@@ -15,7 +15,10 @@ import rateLimit from "express-rate-limit";
15
15
  import ivm from "isolated-vm";
16
16
  import {emailDefaultConfig} from "../constants.js";
17
17
  import {safeAssignObject} from "../core.js";
18
+ import {substituteVariables} from "./workflow.js";
19
+ import NodeCache from "node-cache";
18
20
  import {Config} from "../config.js";
21
+ import { Event } from '../events.js';
19
22
 
20
23
  export const userInitiator = async (req, res, next) => {
21
24
 
@@ -98,127 +101,186 @@ let logger,engine;
98
101
  export async function onInit(defaultEngine) {
99
102
  engine = defaultEngine;
100
103
  logger = engine.getComponent(Logger);
104
+
105
+ const invalidatePermissionsCache = (engine, {modelName, insertedDocs, user}) => {
106
+ const modelsToWatch = ['role', 'permission', 'userPermission'];
107
+
108
+ // Le payload contient le nom du modèle qui a été modifié.
109
+ if (modelName && modelsToWatch.includes(modelName)) {
110
+ logger.info(`[Permissions] Invalidating permissions cache due to change in '${modelName}'.`);
111
+ permissionsCache.flushAll();
112
+ }
113
+ };
114
+
115
+ // On attache notre fonction d'invalidation aux événements système.
116
+ Event.Listen("OnDataAdded", invalidatePermissionsCache, "event", "system");
117
+ Event.Listen("OnDataEdited", invalidatePermissionsCache, "event", "system");
118
+ Event.Listen("OnDataDeleted", invalidatePermissionsCache, "event", "system");
101
119
  }
120
+ /**
121
+ * Cache pour les permissions des utilisateurs.
122
+ * TTL de 10 minutes, vérification toutes les 2 minutes.
123
+ */
124
+ const permissionsCache = new NodeCache({ stdTTL: 600, checkperiod: 120, useClones: false });
125
+
102
126
  /**
103
127
  * Calcule et retourne l'ensemble des permissions actives pour un utilisateur.
104
128
  * Cette fonction interne est la pierre angulaire de la nouvelle logique de permission.
105
129
  * 1. Elle récupère toutes les permissions de base issues des rôles de l'utilisateur.
106
130
  * 2. Elle applique ensuite les "exceptions" (ajouts ou retraits de permissions) qui sont valides (non expirées).
107
131
  * @param {object} user - L'objet utilisateur pour lequel calculer les permissions.
108
- * @returns {Promise<Set<string>>} Un Set contenant les noms de toutes les permissions actives.
132
+ * @returns {Promise<Map<string, object|null>>} Une Map contenant les noms des permissions actives et leur filtre associé.
109
133
  * @private
110
134
  */
111
135
  export async function getUserActivePermissions(user, env = null) {
136
+ // Clé de cache unique pour l'utilisateur et l'environnement
137
+ const cacheKey = `${user._user || user.username}:${user._id.toString()}:${env || 'global'}`;
138
+ const cachedPermissions = permissionsCache.get(cacheKey);
139
+ if (cachedPermissions) {
140
+ logger.debug(`[Permissions] Cache hit for user ${user.username}`);
141
+ return cachedPermissions;
142
+ }
143
+ logger.debug(`[Permissions] Cache miss for user ${user.username}. Calculating permissions...`);
112
144
  const datasCollection = await getCollectionForUser(user);
113
145
  const now = new Date();
114
- const activePermissions = new Set();
146
+ const activePermissions = new Map();
115
147
 
116
148
  // --- ÉTAPE 1: Récupérer les permissions de base des rôles ---
117
149
  if (user.roles && user.roles.length > 0) {
118
150
  const roleIds = user.roles.map(id => new ObjectId(id));
119
151
 
120
- const rolePermissions = await datasCollection.aggregate([
121
- { $match: { _id: { $in: roleIds }, _model: "role" } },
122
- { $unwind: "$permissions" },
123
- { $addFields: { "permissionId": { "$toObjectId": "$permissions" } } },
124
- {
125
- $lookup: {
126
- from: datasCollection.collectionName, // Utiliser la même collection
127
- localField: "permissionId",
128
- foreignField: "_id",
129
- as: "permissionDoc"
130
- }
131
- },
132
- { $unwind: "$permissionDoc" },
133
- { $group: { _id: "$permissionDoc.name" } }
134
- ]).toArray();
135
-
136
- rolePermissions.forEach(p => p._id && activePermissions.add(p._id));
152
+ // Étape 1: Récupérer tous les IDs de permission des rôles de l'utilisateur.
153
+ const roles = await datasCollection.find(
154
+ { _id: { $in: roleIds }, _model: "role", _user: user._user || user.username },
155
+ { projection: { permissions: 1, _user: 1 } }
156
+ ).toArray();
157
+
158
+ // Aplatir tous les tableaux de permissions en un seul et supprimer les doublons.
159
+ const permissionIdStrings = [...new Set(roles.flatMap(role => role.permissions || []))];
160
+ const permissionObjectIds = permissionIdStrings.map(id => new ObjectId(id));
161
+
162
+ // Étape 2: Récupérer tous les documents de permission correspondants en une seule requête.
163
+ const rolePermissions = await datasCollection.find(
164
+ { _id: { $in: permissionObjectIds }, _model: "permission", _user: user._user || user.username },
165
+ { projection: { name: 1, filter: 1 } }
166
+ ).toArray();
167
+
168
+ rolePermissions.forEach(p => {
169
+ if (p.name) {
170
+ // On stocke l'objet permission complet pour un accès ultérieur
171
+ activePermissions.set(p.name, { filter: p.filter ?? null, details: p });
172
+ }
173
+ });
137
174
  }
138
175
 
139
- // --- ÉTAPE 2: Appliquer les exceptions de permission ---
140
- const exceptions = await datasCollection.aggregate([
141
- {
142
- $match: { // Filtre de base pour les exceptions de l'utilisateur
143
- _model: "userPermission",
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 } }] }
153
- ]
154
- }
155
- },
156
- {
157
- $lookup: {
158
- from: datasCollection.collectionName,
159
- let: { permissionId: "$permission" },
160
- pipeline: [
161
- {
162
- $match: {
163
- $expr: {
164
- $eq: [
165
- "$_id",
166
- { $toObjectId: "$$permissionId" } // Conversion ici
167
- ]
168
- }
169
- }
170
- },
171
- { $project: { name: 1 } }
172
- ],
173
- as: 'permissionDoc'
174
- }
175
- },
176
- { $unwind: '$permissionDoc' }
177
- ]).toArray();
176
+ // --- NOUVELLE ÉTAPE : Pré-charger les détails de toutes les permissions d'exception ---
177
+ // Cela garantit que nous avons les noms de permission même pour les révocations.
178
+ const exceptionPermissionIds = (await datasCollection.distinct("permission", {
179
+ _model: "userPermission",
180
+ user: user._id.toString(),
181
+ _user: user._user || user.username
182
+ })).map(id => new ObjectId(id));
178
183
 
179
- // Appliquer les exceptions
180
- for (const exception of exceptions) {
181
- const permissionName = exception.permissionDoc?.name;
182
- if (!permissionName) continue;
184
+ const exceptionPermsDetails = await datasCollection.find(
185
+ { _id: { $in: exceptionPermissionIds }, _model: "permission" },
186
+ { projection: { name: 1, filter: 1 } }
187
+ ).toArray();
183
188
 
184
- if (exception.isGranted) {
185
- activePermissions.add(permissionName);
186
- } else {
187
- activePermissions.delete(permissionName);
189
+ const allPermDetailsMap = new Map(exceptionPermsDetails.map(p => [p._id.toString(), p]));
190
+
191
+ // --- ÉTAPE 2: Appliquer les exceptions de permission ---
192
+ const exceptionsQuery = {
193
+ _model: "userPermission",
194
+ user: user._id.toString(),
195
+ _user: user._user || user.username,
196
+ $and: [
197
+ { $or: [{ expiresAt: { $exists: false } }, { expiresAt: { $gt: now } }] },
198
+ { $or: [{ env: { $exists: false } }, { env: env }] }
199
+ ]
200
+ };
201
+
202
+ const exceptions = await datasCollection.find(exceptionsQuery, {
203
+ projection: { permission: 1, isGranted: 1, filter: 1 } // Le filtre sur l'exception
204
+ }).toArray();
205
+
206
+ if (exceptions.length > 0) {
207
+ for (const exception of exceptions) {
208
+ // Utiliser la map pré-chargée qui contient toutes les permissions d'exception
209
+ const permDetails = allPermDetailsMap.get(exception.permission);
210
+ if (!permDetails?.name) continue;
211
+
212
+ if (exception.isGranted) {
213
+ // Priorité 1: Le filtre défini sur l'exception elle-même.
214
+ // Priorité 2: Le filtre défini sur la permission de base.
215
+ const finalFilter = exception.filter ?? permDetails.filter ?? null;
216
+ // On met à jour l'entrée existante ou on en crée une nouvelle
217
+ activePermissions.set(permDetails.name, { filter: finalFilter, details: permDetails });
218
+ } else {
219
+ activePermissions.delete(permDetails.name);
220
+ }
188
221
  }
189
222
  }
190
223
 
191
- return activePermissions;
224
+ // Mettre le résultat en cache avant de le retourner
225
+ // On ne stocke que le nom et le filtre, pas l'objet 'details' complet
226
+ const finalPermissionsToCache = new Map();
227
+ for (const [name, permData] of activePermissions.entries()) {
228
+ finalPermissionsToCache.set(name, permData.filter);
229
+ }
230
+ permissionsCache.set(cacheKey, finalPermissionsToCache);
231
+ return finalPermissionsToCache;
192
232
  }
193
233
 
194
234
  /**
195
235
  * Vérifie si un utilisateur possède au moins une des permissions spécifiées.
196
236
  * Cette fonction utilise la nouvelle logique basée sur les rôles et les exceptions de permission.
197
237
  * @param {string|string[]} permissionNames - Le nom de la permission ou un tableau de noms.
198
- * @param {object} user - L'objet utilisateur authentifié.
199
- * @returns {Promise<boolean>} - True si l'utilisateur a la permission, sinon false.
238
+ * @param {object} user - L'objet utilisateur.
239
+ * @param {string|null} env - L'environnement de la permission.
240
+ * @param {object|null} req - L'objet requête Express optionnel.
241
+ * @returns {Promise<boolean|object>} - `false` si aucune permission n'est trouvée. Sinon, retourne le filtre de la première permission trouvée (ou `true` si aucun filtre n'est défini).
200
242
  */
201
- export async function hasPermission(permissionNames, user, env = null) {
243
+ export async function hasPermission(permissionNames, user, env = null, req = null) {
202
244
  // Garde la compatibilité pour les utilisateurs non-locaux (ex: système)
203
245
  if (!isLocalUser(user)) {
204
246
  const userRoles = new Set(user.roles || []);
205
247
  const requiredPermissions = Array.isArray(permissionNames) ? permissionNames : [permissionNames];
206
- return requiredPermissions.some(p => userRoles.has(p));
248
+ const hasPerm = requiredPermissions.some(p => userRoles.has(p));
249
+ // Pour les non-locaux, on ne gère pas les filtres complexes, on retourne juste true/false.
250
+ return hasPerm ? true : false;
207
251
  }
208
252
 
209
253
  try {
254
+ // 1. Obtenir l'ensemble final et à jour des permissions de l'utilisateur
255
+ const activePermissions = await getUserActivePermissions(user, env);
256
+
210
257
  const requiredPermissions = Array.isArray(permissionNames) ? permissionNames : [permissionNames];
211
258
  // Si aucune permission n'est requise, on autorise
212
259
  if (requiredPermissions.length === 0) {
213
260
  return true;
214
261
  }
215
262
 
216
- // 1. Obtenir l'ensemble final et à jour des permissions de l'utilisateur
217
- const activePermissions = await getUserActivePermissions(user, env);
218
-
219
- // 2. Vérifier si au moins une des permissions requises est dans l'ensemble des permissions actives
220
- return requiredPermissions.some(pName => activePermissions.has(pName)) || false;
263
+ // 2. Chercher la première permission correspondante
264
+ for (const pName of requiredPermissions) {
265
+ if (activePermissions.has(pName)) {
266
+ const filter = activePermissions.get(pName);
267
+ // Si un filtre existe, on substitue les variables
268
+ if (filter && typeof filter === 'object' && Object.keys(filter).length > 0) {
269
+ // Le contexte de substitution est enrichi avec des données pertinentes
270
+ const context = {
271
+ user,
272
+ permissionName: pName,
273
+ env,
274
+ now: new Date(),
275
+ req: req ? { query: req.query, body: req.body, params: req.params, ip: req.ip } : null
276
+ };
277
+ return await substituteVariables(filter, context, user);
278
+ }
279
+ return true; // Pas de filtre ou filtre vide, on autorise sans condition supplémentaire
280
+ }
281
+ }
221
282
 
283
+ return false; // Aucune permission trouvée
222
284
  } catch (e) {
223
285
  logger.error("Erreur lors de la vérification des permissions :", e);
224
286
  return false;