data-primals-engine 1.1.8 → 1.2.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 +72 -25
- package/client/src/App.scss +11 -6
- package/client/src/AssistantChat.scss +0 -1
- package/client/src/ChartConfigModal.jsx +0 -1
- package/client/src/ConditionBuilder.jsx +0 -2
- package/client/src/ConditionBuilder2.jsx +47 -6
- package/client/src/ContentView.jsx +0 -1
- package/client/src/CronBuilder.scss +0 -1
- package/client/src/CronPartBuilder.jsx +0 -1
- package/client/src/Dashboard.jsx +0 -1
- package/client/src/Dashboard.scss +0 -1
- package/client/src/DashboardChart.jsx +0 -1
- package/client/src/DashboardView.jsx +0 -1
- package/client/src/DataEditor.jsx +0 -4
- package/client/src/DisplayFlexNodeRenderer.jsx +0 -1
- package/client/src/DocumentationPageLayout.jsx +0 -2
- package/client/src/DocumentationPageLayout.scss +0 -1
- package/client/src/Field.jsx +1 -1
- package/client/src/FlexNode.jsx +0 -1
- package/client/src/KanbanView.scss +0 -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/RestoreDialog.jsx +0 -1
- package/client/src/TourSpotlight.jsx +0 -1
- package/client/src/filter.js +2 -0
- package/client/src/hooks/useDragAndDrop.js +0 -1
- package/issues.txt +0 -0
- package/package.json +3 -2
- package/src/constants.js +6 -0
- package/src/core.js +6 -0
- package/src/data.js +0 -2
- package/src/defaultModels.js +33 -0
- package/src/migrate.js +0 -1
- package/src/modules/assistant.js +211 -133
- package/src/modules/data.js +88 -69
- package/src/modules/user.js +111 -56
- package/src/modules/workflow.js +10 -12
- package/src/providers.js +1 -1
- package/src/workers/crypto-worker.js +0 -1
- package/test/data.integration.test.js +53 -70
- package/test/user.test.js +203 -0
- package/test/workflow.integration.test.js +1 -1
package/src/modules/data.js
CHANGED
|
@@ -10,7 +10,7 @@ import process from "node:process";
|
|
|
10
10
|
import {randomColor} from "randomcolor";
|
|
11
11
|
import cronstrue from 'cronstrue/i18n.js';
|
|
12
12
|
import { setTimeoutMiddleware } from '../middlewares/timeout.js';
|
|
13
|
-
|
|
13
|
+
import { mkdir } from 'node:fs/promises';
|
|
14
14
|
import {
|
|
15
15
|
anonymizeText,
|
|
16
16
|
encryptValue,
|
|
@@ -981,13 +981,10 @@ async function runStatefulAlertJob(alertId) {
|
|
|
981
981
|
// 3. Evaluate the trigger condition
|
|
982
982
|
const apiFilter = (alertDoc.triggerCondition);
|
|
983
983
|
const { count } = await searchData({
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
limit: 1
|
|
989
|
-
}
|
|
990
|
-
});
|
|
984
|
+
model: alertDoc.targetModel,
|
|
985
|
+
filter: apiFilter,
|
|
986
|
+
limit: 1
|
|
987
|
+
}, { username: alertDoc._user });
|
|
991
988
|
|
|
992
989
|
// 4. If condition is met, send notification and update state
|
|
993
990
|
if (count > 0) {
|
|
@@ -1233,17 +1230,14 @@ export async function handleCustomEndpointRequest(req, res) {
|
|
|
1233
1230
|
try {
|
|
1234
1231
|
// 1. Trouver l'endpoint correspondant dans la base de données
|
|
1235
1232
|
const endpointSearch = await searchData({
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
limit: 1
|
|
1245
|
-
}
|
|
1246
|
-
});
|
|
1233
|
+
model: 'endpoint',
|
|
1234
|
+
filter: {
|
|
1235
|
+
path: path,
|
|
1236
|
+
method: method,
|
|
1237
|
+
isActive: true
|
|
1238
|
+
},
|
|
1239
|
+
limit: 1
|
|
1240
|
+
}, user);
|
|
1247
1241
|
|
|
1248
1242
|
if (endpointSearch.count === 0) {
|
|
1249
1243
|
logger.warn(`[Endpoint] 404 - No active endpoint found for user '${user.username}', path '${path}', method '${method}'.`);
|
|
@@ -1382,6 +1376,9 @@ export async function onInit(defaultEngine) {
|
|
|
1382
1376
|
}
|
|
1383
1377
|
|
|
1384
1378
|
|
|
1379
|
+
// Create the uploads directories
|
|
1380
|
+
await mkdir(path.join("uploads", "tmp"),{ recursive: true});
|
|
1381
|
+
|
|
1385
1382
|
}else {
|
|
1386
1383
|
modelsCollection = getCollection("models");
|
|
1387
1384
|
datasCollection = getCollection("datas");
|
|
@@ -1396,7 +1393,7 @@ export async function onInit(defaultEngine) {
|
|
|
1396
1393
|
schedule.scheduleJob("0 0 * * *", async () => {
|
|
1397
1394
|
const dt = new Date();
|
|
1398
1395
|
dt.setTime(dt.getTime()-1000*3600*24*14);
|
|
1399
|
-
await deleteData("request",
|
|
1396
|
+
await deleteData("request", {"$lt": ["$timestamp",dt.toISOString()]}, null, false);
|
|
1400
1397
|
});
|
|
1401
1398
|
await scheduleWorkflowTriggers();
|
|
1402
1399
|
|
|
@@ -1527,14 +1524,11 @@ export async function onInit(defaultEngine) {
|
|
|
1527
1524
|
|
|
1528
1525
|
// On utilise la fonction de recherche interne de l'application
|
|
1529
1526
|
const searchResult = await searchData({
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
},
|
|
1536
|
-
user: user
|
|
1537
|
-
});
|
|
1527
|
+
model,
|
|
1528
|
+
filter: processedFilter,
|
|
1529
|
+
limit, // Optimisation : pas besoin de plus de résultats
|
|
1530
|
+
page: 1
|
|
1531
|
+
}, user);
|
|
1538
1532
|
|
|
1539
1533
|
// searchData devrait renvoyer un `count` total des documents correspondants
|
|
1540
1534
|
const isCompleted = searchResult.count >= limit;
|
|
@@ -1710,6 +1704,48 @@ export async function onInit(defaultEngine) {
|
|
|
1710
1704
|
});
|
|
1711
1705
|
|
|
1712
1706
|
|
|
1707
|
+
engine.post('/api/model/search', [middlewareAuthenticator, userInitiator], async (req, res) => {
|
|
1708
|
+
const { query } = req.fields;
|
|
1709
|
+
const user = req.me;
|
|
1710
|
+
|
|
1711
|
+
if (!query || typeof query !== 'string') {
|
|
1712
|
+
return res.status(400).json({ success: false, error: 'A search query string is required.' });
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
try {
|
|
1716
|
+
// Utilise une expression régulière pour une recherche insensible à la casse
|
|
1717
|
+
const searchRegex = new RegExp(query, 'i');
|
|
1718
|
+
|
|
1719
|
+
const results = await modelsCollection.find({
|
|
1720
|
+
// Cherche dans les modèles de l'utilisateur OU les modèles partagés
|
|
1721
|
+
$or: [
|
|
1722
|
+
{ _user: user.username },
|
|
1723
|
+
{ _user: { $exists: false } }
|
|
1724
|
+
],
|
|
1725
|
+
// Cherche dans le nom OU la description
|
|
1726
|
+
$and: [
|
|
1727
|
+
{ $or: [
|
|
1728
|
+
{ name: { $regex: searchRegex } },
|
|
1729
|
+
{ description: { $regex: searchRegex } }
|
|
1730
|
+
]}
|
|
1731
|
+
]
|
|
1732
|
+
}, {
|
|
1733
|
+
// On ne retourne que les informations utiles pour l'IA, pas tout le modèle
|
|
1734
|
+
projection: {
|
|
1735
|
+
name: 1,
|
|
1736
|
+
description: 1,
|
|
1737
|
+
_id: 0
|
|
1738
|
+
}
|
|
1739
|
+
}).limit(10).toArray(); // Limite à 10 résultats pour ne pas surcharger
|
|
1740
|
+
|
|
1741
|
+
res.json({ success: true, models: results });
|
|
1742
|
+
|
|
1743
|
+
} catch (error) {
|
|
1744
|
+
logger.error(`[Model Search] Error searching models for query "${query}":`, error);
|
|
1745
|
+
res.status(500).json({ success: false, error: 'An internal server error occurred.' });
|
|
1746
|
+
}
|
|
1747
|
+
});
|
|
1748
|
+
|
|
1713
1749
|
engine.post('/api/model/generate', [middlewareAuthenticator, userInitiator, assistantGlobalLimiter, generateLimiter, setTimeoutMiddleware(30000)], async (req, res) => {
|
|
1714
1750
|
// --- NOUVELLE LOGIQUE : Accepter le prompt ET un modèle existant ---
|
|
1715
1751
|
const { prompt, history = [], existingModel } = req.fields;
|
|
@@ -1820,7 +1856,7 @@ export async function onInit(defaultEngine) {
|
|
|
1820
1856
|
const { pack } = req.fields;
|
|
1821
1857
|
|
|
1822
1858
|
try {
|
|
1823
|
-
const {data, count} = await searchData({
|
|
1859
|
+
const {data, count} = await searchData({...req.query, model: req.fields.model || req.query.model, filter: req.fields.filter, pack}, req.me);
|
|
1824
1860
|
|
|
1825
1861
|
if( req.query.attachment ) {
|
|
1826
1862
|
res.attachment(req.query.attachment);
|
|
@@ -1845,7 +1881,7 @@ export async function onInit(defaultEngine) {
|
|
|
1845
1881
|
|
|
1846
1882
|
engine.delete('/api/data/:ids', [throttle, middlewareAuthenticator, userInitiator, middlewareLogger, setTimeoutMiddleware(15000)], async (req, res) => {
|
|
1847
1883
|
const ids = req.params.ids.split(',');
|
|
1848
|
-
const r = await deleteData(req.fields.model, ids,
|
|
1884
|
+
const r = await deleteData(req.fields.model, ids, req.me);
|
|
1849
1885
|
if( r.error) {
|
|
1850
1886
|
return res.status(r.statusCode || 400).json(r);
|
|
1851
1887
|
}else{
|
|
@@ -1854,7 +1890,7 @@ export async function onInit(defaultEngine) {
|
|
|
1854
1890
|
});
|
|
1855
1891
|
|
|
1856
1892
|
engine.delete('/api/data', [throttle, middlewareAuthenticator, userInitiator, middlewareLogger, setTimeoutMiddleware(15000)], async (req, res) => {
|
|
1857
|
-
const r = await deleteData(req.fields.model,
|
|
1893
|
+
const r = await deleteData(req.fields.model, req.fields.filter, req.me);
|
|
1858
1894
|
if( r.error) {
|
|
1859
1895
|
return res.status(r.statusCode || 400).json(r);
|
|
1860
1896
|
}else{
|
|
@@ -2344,12 +2380,6 @@ export async function onInit(defaultEngine) {
|
|
|
2344
2380
|
* }
|
|
2345
2381
|
* Returns: Array of aggregated data points (e.g., [{ label: '...', value: ... }]) or an error.
|
|
2346
2382
|
*/
|
|
2347
|
-
// Dans server/src/modules/data.js, modifiez la route POST /api/charts/aggregate
|
|
2348
|
-
|
|
2349
|
-
// Dans server/src/modules/data.js, modifiez la route POST /api/charts/aggregate
|
|
2350
|
-
// C:/Dev/hackersonline-engine/server/src/modules/data.js
|
|
2351
|
-
|
|
2352
|
-
// ... (autres imports et code)
|
|
2353
2383
|
|
|
2354
2384
|
engine.post('/api/charts/aggregate', [throttle, middlewareAuthenticator, userInitiator, ...userMiddlewares, setTimeoutMiddleware(15000)], async (req, res) => {
|
|
2355
2385
|
// --- Récupérer groupByLabelField ---
|
|
@@ -3236,13 +3266,10 @@ async function processRelations(docToProcess, model, collection, me, idMap) {
|
|
|
3236
3266
|
batchFinds.push({
|
|
3237
3267
|
field: field.name,
|
|
3238
3268
|
promise: searchData({
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
model: field.relation
|
|
3244
|
-
}
|
|
3245
|
-
}),
|
|
3269
|
+
filter: value.$find,
|
|
3270
|
+
limit: field.multiple ? 0 : 1,
|
|
3271
|
+
model: field.relation
|
|
3272
|
+
}, me),
|
|
3246
3273
|
multiple: field.multiple
|
|
3247
3274
|
});
|
|
3248
3275
|
}
|
|
@@ -3655,7 +3682,7 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
|
|
|
3655
3682
|
}
|
|
3656
3683
|
|
|
3657
3684
|
// 2. Récupération des documents existants et de leur hash original
|
|
3658
|
-
const existingDocs = (await searchData({
|
|
3685
|
+
const existingDocs = (await searchData({model: modelName, filter}, user))?.data;
|
|
3659
3686
|
if (!existingDocs || existingDocs.length === 0) {
|
|
3660
3687
|
return {success: false, error: i18n.t("api.data.notFound")};
|
|
3661
3688
|
}
|
|
@@ -3856,7 +3883,8 @@ async function handleScheduledJobs(modelName, existingDocs, collection, updateDa
|
|
|
3856
3883
|
}
|
|
3857
3884
|
}
|
|
3858
3885
|
|
|
3859
|
-
|
|
3886
|
+
|
|
3887
|
+
export const deleteData = async (modelName, filter, user ={}, triggerWorkflow, waitForWorkflow = false) => {
|
|
3860
3888
|
|
|
3861
3889
|
try {
|
|
3862
3890
|
const collection = await getCollectionForUser(user);
|
|
@@ -3871,8 +3899,8 @@ export const deleteData = async (modelName, ids = [], filter, user ={}, triggerW
|
|
|
3871
3899
|
});
|
|
3872
3900
|
|
|
3873
3901
|
// Ajouter le filtre par IDs si fourni
|
|
3874
|
-
if (
|
|
3875
|
-
findFilter.push({"$in": ["$_id",
|
|
3902
|
+
if (Array.isArray(filter) && filter.length > 0) {
|
|
3903
|
+
findFilter.push({"$in": ["$_id", filter.map(m => new ObjectId(m))]});
|
|
3876
3904
|
}
|
|
3877
3905
|
|
|
3878
3906
|
// Ajouter le filtre par nom de modèle si fourni (utile si 'filter' est utilisé seul)
|
|
@@ -4071,7 +4099,7 @@ export const deleteData = async (modelName, ids = [], filter, user ={}, triggerW
|
|
|
4071
4099
|
|
|
4072
4100
|
// ... (le reste du fichier data.js)
|
|
4073
4101
|
|
|
4074
|
-
export const searchData = async (
|
|
4102
|
+
export const searchData = async (query, user) => {
|
|
4075
4103
|
const { page, limit, sort, model, ids, timeout, pack } = query; // Les filtres de la requête (attention aux injections MongoDB !)
|
|
4076
4104
|
|
|
4077
4105
|
if( user.username !== 'demo' && isLocalUser(user) && (
|
|
@@ -4610,8 +4638,10 @@ export const importData = async(options, files, user) => {
|
|
|
4610
4638
|
// let allProcessedData = [];
|
|
4611
4639
|
// let modelNameForImport = '';
|
|
4612
4640
|
// --- FIN DE LA MODIFICATION ---
|
|
4613
|
-
|
|
4614
|
-
|
|
4641
|
+
if( !file.name ){
|
|
4642
|
+
throw new Error("No file provided.");
|
|
4643
|
+
}
|
|
4644
|
+
if (file.type === 'application/json' || file.name.endsWith('.json')) {
|
|
4615
4645
|
fileProcessed = true;
|
|
4616
4646
|
const jsonData = await runImportExportWorker('parse-json', { fileContent: fileContent.toString() });
|
|
4617
4647
|
|
|
@@ -4752,7 +4782,7 @@ export const importData = async(options, files, user) => {
|
|
|
4752
4782
|
}
|
|
4753
4783
|
// --- FIN DE LA MODIFICATION PRINCIPALE ---
|
|
4754
4784
|
|
|
4755
|
-
} else if (file.
|
|
4785
|
+
} else if (file.type === 'text/csv' || file.name.endsWith('.csv')) {
|
|
4756
4786
|
// --- Logique CSV (inchangée, mais maintenant elle est séparée de la logique JSON) ---
|
|
4757
4787
|
fileProcessed = true;
|
|
4758
4788
|
const modelNameForImport = options.model;
|
|
@@ -4959,16 +4989,13 @@ export const exportData= async (options, user) =>{
|
|
|
4959
4989
|
|
|
4960
4990
|
// --- Fetch Data using searchData ---
|
|
4961
4991
|
const searchParams = {
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
depth: parsedDepth,
|
|
4967
|
-
limit: remainingLimit
|
|
4968
|
-
}
|
|
4992
|
+
model: modelName,
|
|
4993
|
+
filter: modelSpecificFilter,
|
|
4994
|
+
depth: parsedDepth,
|
|
4995
|
+
limit: remainingLimit
|
|
4969
4996
|
};
|
|
4970
4997
|
|
|
4971
|
-
const { data: resultData, count } = await searchData(searchParams);
|
|
4998
|
+
const { data: resultData, count } = await searchData(searchParams, user);
|
|
4972
4999
|
|
|
4973
5000
|
if (resultData && resultData.length > 0) {
|
|
4974
5001
|
exportResults[modelName] = resultData;
|
|
@@ -5073,7 +5100,6 @@ function isValidAggregationOperator(operator) {
|
|
|
5073
5100
|
|
|
5074
5101
|
return [...arithmeticOperators, ...comparisonOperators, ...stringOperators, ...conditionalOperators].includes(operator);
|
|
5075
5102
|
}
|
|
5076
|
-
// C:/Dev/hackersonline-engine/server/src/modules/data.js
|
|
5077
5103
|
|
|
5078
5104
|
/**
|
|
5079
5105
|
* Gère les traductions spécifiques à l'utilisateur et traite les données de manière récursive
|
|
@@ -5357,8 +5383,6 @@ const readKeyFromFile = (user) => {
|
|
|
5357
5383
|
return null;
|
|
5358
5384
|
};
|
|
5359
5385
|
|
|
5360
|
-
// C:/Dev/data-primals-engine/src/modules/data.js
|
|
5361
|
-
|
|
5362
5386
|
export const dumpUserData = async (user) => {
|
|
5363
5387
|
const s3Config = user.configS3;
|
|
5364
5388
|
const backupDir = getBackupDir();
|
|
@@ -5777,7 +5801,7 @@ export async function installPack(packId, user, lang) {
|
|
|
5777
5801
|
const finalSelector = { ...linkSelector };
|
|
5778
5802
|
delete finalSelector['_model']; // nécessaire
|
|
5779
5803
|
// CORRECTION 4: Appel corrigé à searchData
|
|
5780
|
-
const { data: targetDocs } = await searchData({
|
|
5804
|
+
const { data: targetDocs } = await searchData({model: targetModelName, filter: finalSelector }, user);
|
|
5781
5805
|
|
|
5782
5806
|
if (targetDocs && targetDocs.length > 0) {
|
|
5783
5807
|
targetIds = targetDocs.map(d => d._id); // Récupère un tableau d'ObjectIds
|
|
@@ -5832,11 +5856,6 @@ export const installAllPacks = async () => {
|
|
|
5832
5856
|
await packsCollection.insertMany(packs);
|
|
5833
5857
|
}
|
|
5834
5858
|
|
|
5835
|
-
// Dans C:/Dev/data-primals-engine/src/modules/data.js
|
|
5836
|
-
// Dans C:/Dev/data-primals-engine/src/modules/data.js
|
|
5837
|
-
|
|
5838
|
-
// ... (imports inchangés)
|
|
5839
|
-
|
|
5840
5859
|
export async function handleDemoInitialization(req, res) {
|
|
5841
5860
|
const user = req.me;
|
|
5842
5861
|
const body = req.fields;
|
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.
|
package/src/modules/workflow.js
CHANGED
|
@@ -197,15 +197,15 @@ export async function executeSafeJavascript(actionDef, context, user) {
|
|
|
197
197
|
await jail.set('db', createJailFunction({
|
|
198
198
|
create: (modelName, dataObject) => insertData(modelName, dataObject, {}, user),
|
|
199
199
|
find: async (modelName, filter) => {
|
|
200
|
-
const result = await searchData({
|
|
200
|
+
const result = await searchData({ model: modelName, filter }, user);
|
|
201
201
|
return new ivm.ExternalCopy(result.data).copyInto();
|
|
202
202
|
},
|
|
203
203
|
findOne: async (modelName, filter) => {
|
|
204
|
-
const result = await searchData({
|
|
204
|
+
const result = await searchData({ model: modelName, filter, limit: 1 }, user);
|
|
205
205
|
return new ivm.ExternalCopy(result.data?.[0] || null).copyInto();
|
|
206
206
|
},
|
|
207
207
|
update: (modelName, filter, updateObject) => patchData(modelName, filter, updateObject, {}, user),
|
|
208
|
-
delete: (modelName, filter) => deleteData(modelName,
|
|
208
|
+
delete: (modelName, filter) => deleteData(modelName, filter, user)
|
|
209
209
|
}));
|
|
210
210
|
|
|
211
211
|
const createLoggerFunction = (level) => {
|
|
@@ -228,11 +228,11 @@ export async function executeSafeJavascript(actionDef, context, user) {
|
|
|
228
228
|
await jail.set('env', createJailFunction({
|
|
229
229
|
get: async (variableName) => {
|
|
230
230
|
if (!variableName) return null;
|
|
231
|
-
const result = await searchData({
|
|
231
|
+
const result = await searchData({ model: 'env', filter: { name: variableName }, limit: 1 }, user);
|
|
232
232
|
return new ivm.ExternalCopy(result.data?.[0]?.value || null).copyInto();
|
|
233
233
|
},
|
|
234
234
|
getAll: async () => {
|
|
235
|
-
const result = await searchData({
|
|
235
|
+
const result = await searchData({ model: 'env' }, user);
|
|
236
236
|
const envObject = result.data.reduce((acc, v) => {
|
|
237
237
|
acc[v.name] = v.value;
|
|
238
238
|
return acc;
|
|
@@ -708,7 +708,7 @@ async function handleDeleteDataAction(actionDef, contextData, user, dbCollection
|
|
|
708
708
|
// 5. Call the centralized deleteData function (à créer dans data.js)
|
|
709
709
|
// Cette fonction devra gérer la recherche préalable pour les workflows 'DataDeleted' et la suppression des fichiers.
|
|
710
710
|
const deleteResult = await deleteData(
|
|
711
|
-
targetModel,
|
|
711
|
+
targetModel,
|
|
712
712
|
selectorObject,
|
|
713
713
|
user
|
|
714
714
|
);
|
|
@@ -1115,7 +1115,7 @@ export async function triggerWorkflows(triggerData, user, eventType) {
|
|
|
1115
1115
|
|
|
1116
1116
|
// Exécuter la vérification dans la base de données
|
|
1117
1117
|
// Utilisation de countDocuments pour une vérification rapide
|
|
1118
|
-
const matchCount = await searchData({
|
|
1118
|
+
const matchCount = await searchData({ model: targetModelName, filter: finalFilter, limit: 1 }, user);
|
|
1119
1119
|
|
|
1120
1120
|
if (!matchCount.count) {
|
|
1121
1121
|
console.debug(`[Workflow Trigger] Trigger ${trigger._id}: dataFilter non satisfait par la donnée ${dataId}. WorkflowRun non créé.`);
|
|
@@ -1198,7 +1198,6 @@ export async function triggerWorkflows(triggerData, user, eventType) {
|
|
|
1198
1198
|
* @param {object} user - The user context for database access.
|
|
1199
1199
|
* @returns {Promise<void>}
|
|
1200
1200
|
*/
|
|
1201
|
-
// C:/Dev/hackersonline-engine/server/src/modules/workflow.js
|
|
1202
1201
|
|
|
1203
1202
|
export async function processWorkflowRun(workflowRunId, user) {
|
|
1204
1203
|
const dbCollection = await getCollectionForUser(user);
|
|
@@ -1280,7 +1279,7 @@ export async function processWorkflowRun(workflowRunId, user) {
|
|
|
1280
1279
|
try {
|
|
1281
1280
|
// --- 7. Évaluation des conditions de l'étape ---
|
|
1282
1281
|
if (currentStepDef.conditions && Object.keys(currentStepDef.conditions).length > 0) {
|
|
1283
|
-
const searchResult = await searchData({
|
|
1282
|
+
const searchResult = await searchData({ model: contextData.triggerDataModel, filter: currentStepDef.conditions, limit: 1}, user);
|
|
1284
1283
|
conditionsMet = searchResult && searchResult.count > 0;
|
|
1285
1284
|
logger.info(`[processWorkflowRun] Run ID: ${runId}, Step ID: ${currentStepId}: Conditions evaluated. Found ${searchResult ? searchResult.count : 0} match(es). Result: ${conditionsMet}`);
|
|
1286
1285
|
}
|
|
@@ -1478,9 +1477,8 @@ async function handleSendEmailAction(action, triggerData, user) {
|
|
|
1478
1477
|
|
|
1479
1478
|
// 1. Récupérer la configuration SMTP depuis le modèle 'env' de l'utilisateur
|
|
1480
1479
|
const envVars = await searchData({
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
});
|
|
1480
|
+
model: 'env', limit: 100 } // Limite raisonnable pour les variables d'env
|
|
1481
|
+
, user);
|
|
1484
1482
|
|
|
1485
1483
|
if (!envVars.data || envVars.data.length === 0) {
|
|
1486
1484
|
throw new Error("Aucune variable d'environnement (modèle 'env') trouvée pour la configuration SMTP.");
|
package/src/providers.js
CHANGED
|
@@ -91,7 +91,7 @@ export class UserProvider {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
async hasFeature(user, feature) {
|
|
94
|
-
return this.plans[user
|
|
94
|
+
return this.plans[user?.userPlan]?.features.some(f => f === feature);
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
// Ajoutez ici d'autres méthodes nécessaires : findUserById, createUser, etc.
|