data-primals-engine 1.4.0 → 1.4.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 +852 -797
- package/client/src/PackGallery.jsx +391 -290
- package/client/src/PackGallery.scss +231 -210
- package/client/src/translations.js +16750 -16630
- package/package.json +14 -11
- package/src/gameObject.js +1 -1
- package/src/modules/auth-google/index.js +51 -0
- package/src/modules/auth-microsoft/index.js +82 -0
- package/src/modules/auth-saml/index.js +90 -0
- package/src/modules/data/data.js +12 -4
- package/src/modules/data/data.routes.js +1691 -1646
- package/src/providers.js +110 -1
- package/src/sso.js +194 -0
- package/test/data.integration.test.js +3 -0
- package/test/user.test.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "data-primals-engine",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"description": "data-primals-engine is a package responsible from handling large amount of data using MongoDB in a practical and performant way. It can also get workflow models working (for automation), and fully supports internationalisation. It also has integrated AI assistant.",
|
|
5
5
|
"main": "src/engine.js",
|
|
6
6
|
"type": "module",
|
|
@@ -31,12 +31,11 @@
|
|
|
31
31
|
"tar-fs": "3.0.9",
|
|
32
32
|
"on-headers": "1.1.0",
|
|
33
33
|
"brace-expansion": "2.0.2",
|
|
34
|
-
"prismjs": "1.30.0"
|
|
34
|
+
"prismjs": "1.30.0",
|
|
35
|
+
"xml2js": ">=0.5.0"
|
|
35
36
|
},
|
|
36
37
|
"overrides": {
|
|
37
|
-
"
|
|
38
|
-
"prismjs": "1.30.0"
|
|
39
|
-
}
|
|
38
|
+
"prismjs": "1.30.0"
|
|
40
39
|
},
|
|
41
40
|
"repository": {
|
|
42
41
|
"type": "git",
|
|
@@ -49,12 +48,6 @@
|
|
|
49
48
|
"./client": "./client/index.js",
|
|
50
49
|
"./*": "./src/*.js"
|
|
51
50
|
},
|
|
52
|
-
"peerDependencies": {
|
|
53
|
-
"express": "^5.1.0",
|
|
54
|
-
"react": "18.3.1",
|
|
55
|
-
"react-query": ">=3.0.0",
|
|
56
|
-
"react-i18next": "^15.6.1"
|
|
57
|
-
},
|
|
58
51
|
"dependencies": {
|
|
59
52
|
"@langchain/anthropic": "^0.3.26",
|
|
60
53
|
"@langchain/core": "^0.3.66",
|
|
@@ -86,6 +79,8 @@
|
|
|
86
79
|
"node-schedule": "^2.1.1",
|
|
87
80
|
"nodemailer": "^7.0.5",
|
|
88
81
|
"openai": "^5.10.2",
|
|
82
|
+
"passport": "^0.7.0",
|
|
83
|
+
"passport-saml-encrypted": "^0.1.13",
|
|
89
84
|
"process": "^0.11.10",
|
|
90
85
|
"prop-types": "^15.8.1",
|
|
91
86
|
"randomcolor": "^0.6.2",
|
|
@@ -102,6 +97,14 @@
|
|
|
102
97
|
"vitest": "^3.2.4",
|
|
103
98
|
"yaml": "^2.8.0"
|
|
104
99
|
},
|
|
100
|
+
"peerDependencies": {
|
|
101
|
+
"express": "^5.1.0",
|
|
102
|
+
"passport-azure-ad": "^4.3.5",
|
|
103
|
+
"passport-google-oauth20": "^2.0.0",
|
|
104
|
+
"react": "18.3.1",
|
|
105
|
+
"react-i18next": "^15.6.1",
|
|
106
|
+
"react-query": ">=3.0.0"
|
|
107
|
+
},
|
|
105
108
|
"devDependencies": {
|
|
106
109
|
"@eslint/js": "^9.32.0",
|
|
107
110
|
"concurrently": "^9.2.0",
|
package/src/gameObject.js
CHANGED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
|
|
2
|
+
import { Sso, SSOUserProvider } from '../../sso.js';
|
|
3
|
+
import {Logger} from "../../gameObject.js";
|
|
4
|
+
|
|
5
|
+
let logger;
|
|
6
|
+
|
|
7
|
+
export async function onInit(engine) {
|
|
8
|
+
logger = engine.getComponent(Logger);
|
|
9
|
+
|
|
10
|
+
// Le SSOUserProvider est nécessaire pour l'initialisation et pour la stratégie.
|
|
11
|
+
// On le crée donc en premier.
|
|
12
|
+
const ssoUserProvider = new SSOUserProvider(engine);
|
|
13
|
+
|
|
14
|
+
// 1. Récupérer le composant PassportAuth central.
|
|
15
|
+
let passportAuth = engine.getComponent(Sso);
|
|
16
|
+
if (!passportAuth) {
|
|
17
|
+
passportAuth = engine.addComponent(Sso);
|
|
18
|
+
// C'EST ICI QUE L'INITIALISATION DOIT AVOIR LIEU, UNE SEULE FOIS.
|
|
19
|
+
passportAuth.initialize({ ssoUserProvider });
|
|
20
|
+
passportAuth.addLogoutRoute();
|
|
21
|
+
logger.info("[auth-google] Sso component was not found, created and initialized a new one.");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 2. Vérifier que les variables d'environnement nécessaires sont présentes.
|
|
25
|
+
if (!process.env.GOOGLE_CLIENT_ID || !process.env.GOOGLE_CLIENT_SECRET) {
|
|
26
|
+
logger.warn("[auth-google] GOOGLE_CLIENT_ID or GOOGLE_CLIENT_SECRET are not set. Google SSO will be disabled.");
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 3. Créer l'instance de la stratégie Google.
|
|
31
|
+
const googleStrategy = new GoogleStrategy({
|
|
32
|
+
clientID: process.env.GOOGLE_CLIENT_ID,
|
|
33
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
|
34
|
+
callbackURL: "/api/auth/google/callback"
|
|
35
|
+
},
|
|
36
|
+
async (accessToken, refreshToken, profile, done) => {
|
|
37
|
+
try {
|
|
38
|
+
// On utilise le SSOUserProvider pour trouver ou créer l'utilisateur.
|
|
39
|
+
const user = await ssoUserProvider.findOrCreate(profile);
|
|
40
|
+
return done(null, user);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
return done(err);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// 4. Enregistrer la stratégie auprès du composant PassportAuth.
|
|
47
|
+
passportAuth.addStrategy('google', googleStrategy,
|
|
48
|
+
{ authPath: '/api/auth/google', callbackPath: '/api/auth/google/callback' },
|
|
49
|
+
{ scope: ['profile', 'email'] }
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Sso, SSOUserProvider } from '../../sso.js';
|
|
2
|
+
import { Logger } from "../../gameObject.js";
|
|
3
|
+
|
|
4
|
+
let logger;
|
|
5
|
+
|
|
6
|
+
export async function onInit(engine) {
|
|
7
|
+
logger = engine.getComponent(Logger);
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
// 1. Tenter d'importer dynamiquement la stratégie Azure AD.
|
|
11
|
+
// L'utilisateur devra exécuter `npm install passport-azure-ad`.
|
|
12
|
+
const { OIDCStrategy } = await import('passport-azure-ad');
|
|
13
|
+
|
|
14
|
+
const ssoUserProvider = new SSOUserProvider(engine);
|
|
15
|
+
|
|
16
|
+
// 2. Récupérer ou créer le composant SSO central.
|
|
17
|
+
let ssoComponent = engine.getComponent(Sso);
|
|
18
|
+
if (!ssoComponent) {
|
|
19
|
+
ssoComponent = engine.addComponent(Sso);
|
|
20
|
+
ssoComponent.initialize({ ssoUserProvider });
|
|
21
|
+
ssoComponent.addLogoutRoute();
|
|
22
|
+
logger.info("[auth-microsoft] Sso component was not found, created and initialized a new one.");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 3. Vérifier les variables d'environnement critiques pour Azure AD.
|
|
26
|
+
const { MICROSOFT_CLIENT_ID, MICROSOFT_CLIENT_SECRET, MICROSOFT_TENANT_ID } = process.env;
|
|
27
|
+
if (!MICROSOFT_CLIENT_ID || !MICROSOFT_CLIENT_SECRET || !MICROSOFT_TENANT_ID) {
|
|
28
|
+
logger.warn("[auth-microsoft] Microsoft environment variables are not fully set. Microsoft SSO will be disabled.");
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const callbackUrl = process.env.MICROSOFT_CALLBACK_URL || '/api/auth/microsoft/callback';
|
|
33
|
+
|
|
34
|
+
// 4. Créer l'instance de la stratégie OIDC pour Microsoft.
|
|
35
|
+
const microsoftStrategy = new OIDCStrategy({
|
|
36
|
+
identityMetadata: `https://login.microsoftonline.com/${MICROSOFT_TENANT_ID}/v2.0/.well-known/openid-configuration`,
|
|
37
|
+
clientID: MICROSOFT_CLIENT_ID,
|
|
38
|
+
responseType: 'code id_token',
|
|
39
|
+
responseMode: 'form_post',
|
|
40
|
+
redirectUrl: callbackUrl,
|
|
41
|
+
allowHttpForRedirectUrl: process.env.NODE_ENV !== 'production',
|
|
42
|
+
clientSecret: MICROSOFT_CLIENT_SECRET,
|
|
43
|
+
validateIssuer: false, // Important pour les tenants multiples
|
|
44
|
+
passReqToCallback: false,
|
|
45
|
+
scope: ['profile', 'email', 'openid']
|
|
46
|
+
},
|
|
47
|
+
async (iss, sub, profile, accessToken, refreshToken, done) => {
|
|
48
|
+
try {
|
|
49
|
+
// Le profil Azure AD est différent. On l'adapte à notre format standard.
|
|
50
|
+
const email = profile.upn || profile._json.email;
|
|
51
|
+
if (!email) {
|
|
52
|
+
return done(new Error("Microsoft profile is missing an email (upn)."));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const adaptedProfile = {
|
|
56
|
+
provider: 'azuread',
|
|
57
|
+
id: profile.oid, // Object ID de l'utilisateur dans Azure
|
|
58
|
+
displayName: profile.displayName,
|
|
59
|
+
emails: [{ value: email }]
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const user = await ssoUserProvider.findOrCreate(adaptedProfile);
|
|
63
|
+
return done(null, user);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
return done(err);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// 5. Enregistrer la stratégie auprès du composant SSO.
|
|
70
|
+
ssoComponent.addStrategy('azuread-openidconnect', microsoftStrategy, {
|
|
71
|
+
authPath: '/api/auth/microsoft',
|
|
72
|
+
callbackPath: callbackUrl
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
} catch (e) {
|
|
76
|
+
if (e.code === 'ERR_MODULE_NOT_FOUND') {
|
|
77
|
+
logger.info("[auth-microsoft] Module désactivé. Pour l'activer, exécutez 'npm install passport-azure-ad'");
|
|
78
|
+
} else {
|
|
79
|
+
logger.error("[auth-microsoft] Une erreur inattendue a empêché le chargement du module :", e);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Sso, SSOUserProvider } from '../../sso.js';
|
|
2
|
+
import { Logger } from "../../gameObject.js";
|
|
3
|
+
|
|
4
|
+
let logger;
|
|
5
|
+
|
|
6
|
+
export async function onInit(engine) {
|
|
7
|
+
logger = engine.getComponent(Logger);
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
// 1. Tenter d'importer dynamiquement la stratégie SAML.
|
|
11
|
+
// Cela ne fonctionnera que si l'utilisateur a exécuté `npm install passport-saml-encrypted`.
|
|
12
|
+
const { Strategy: SamlStrategy } = await import('passport-saml-encrypted');
|
|
13
|
+
|
|
14
|
+
const ssoUserProvider = new SSOUserProvider(engine);
|
|
15
|
+
|
|
16
|
+
// 2. Récupérer le composant SSO central.
|
|
17
|
+
let ssoComponent = engine.getComponent(Sso);
|
|
18
|
+
if (!ssoComponent) {
|
|
19
|
+
// S'il n'existe pas, on l'ajoute. C'est le premier module SSO à se charger.
|
|
20
|
+
ssoComponent = engine.addComponent(Sso);
|
|
21
|
+
ssoComponent.initialize({ ssoUserProvider });
|
|
22
|
+
ssoComponent.addLogoutRoute();
|
|
23
|
+
logger.info("[auth-saml] Sso component was not found, created and initialized a new one.");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 3. Vérifier les variables d'environnement critiques pour SAML.
|
|
27
|
+
const { SAML_ENTRY_POINT, SAML_ISSUER, SAML_CERT, SAML_CALLBACK_URL, SAML_DECRYPTION_KEY } = process.env;
|
|
28
|
+
if (!SAML_ENTRY_POINT || !SAML_ISSUER || !SAML_CERT || !SAML_DECRYPTION_KEY) {
|
|
29
|
+
logger.warn("[auth-saml] SAML environment variables (SAML_ENTRY_POINT, SAML_ISSUER, SAML_CERT, SAML_DECRYPTION_KEY) are not fully set. SAML SSO will be disabled.");
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const callbackUrl = SAML_CALLBACK_URL || '/api/auth/saml/callback';
|
|
34
|
+
|
|
35
|
+
// 4. Créer l'instance de la stratégie SAML.
|
|
36
|
+
// La configuration SAML est plus complexe que OAuth2.
|
|
37
|
+
const samlStrategy = new SamlStrategy(
|
|
38
|
+
{
|
|
39
|
+
path: callbackUrl, // Le chemin où l'IdP (Identity Provider) enverra la réponse POST.
|
|
40
|
+
entryPoint: SAML_ENTRY_POINT, // L'URL de connexion de l'IdP.
|
|
41
|
+
issuer: SAML_ISSUER, // L'identifiant de votre application (Service Provider).
|
|
42
|
+
cert: SAML_CERT, // Le certificat public de l'IdP pour vérifier la signature.
|
|
43
|
+
decryptionPvk: SAML_DECRYPTION_KEY, // Clé privée pour déchiffrer les assertions.
|
|
44
|
+
encryptedSAML: true, // Indique que nous attendons des assertions chiffrées.
|
|
45
|
+
acceptedClockSkewMs: -1 // Optionnel mais recommandé: pour les problèmes de synchronisation d'horloge.
|
|
46
|
+
},
|
|
47
|
+
async (profile, done) => {
|
|
48
|
+
try {
|
|
49
|
+
// Normaliser les clés du profil en minuscules pour une correspondance insensible à la casse.
|
|
50
|
+
const normalizedProfile = Object.keys(profile).reduce((acc, key) => {
|
|
51
|
+
acc[key.toLowerCase()] = profile[key];
|
|
52
|
+
return acc;
|
|
53
|
+
}, {});
|
|
54
|
+
|
|
55
|
+
// Le profil SAML est différent du profil Google. Nous devons l'adapter.
|
|
56
|
+
// `nameID` est souvent l'email. Les autres attributs dépendent de la configuration de l'IdP.
|
|
57
|
+
const email = normalizedProfile.email || normalizedProfile.nameid;
|
|
58
|
+
if (!email) {
|
|
59
|
+
return done(new Error("SAML profile is missing an email or nameID."));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const adaptedProfile = {
|
|
63
|
+
provider: 'saml',
|
|
64
|
+
id: normalizedProfile.nameid,
|
|
65
|
+
displayName: normalizedProfile.displayname || normalizedProfile.cn || `${normalizedProfile.firstname || ''} ${normalizedProfile.lastname || ''}`.trim() || email,
|
|
66
|
+
emails: [{ value: email }]
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const user = await ssoUserProvider.findOrCreate(adaptedProfile);
|
|
70
|
+
return done(null, user);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
return done(err);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// 5. Enregistrer la stratégie auprès du composant SSO.
|
|
78
|
+
ssoComponent.addStrategy('saml', samlStrategy, {
|
|
79
|
+
authPath: '/api/auth/saml',
|
|
80
|
+
callbackPath: callbackUrl
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
} catch (e) {
|
|
84
|
+
if (e.code === 'ERR_MODULE_NOT_FOUND') {
|
|
85
|
+
logger.info("[auth-saml] Module désactivé. Pour l'activer, exécutez 'npm install passport-saml-encrypted'");
|
|
86
|
+
} else {
|
|
87
|
+
logger.error("[auth-saml] Une erreur inattendue a empêché le chargement du module :", e);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
package/src/modules/data/data.js
CHANGED
|
@@ -4364,7 +4364,7 @@ async function manageBackupRotation(user, backupFrequency, s3Config = null) { //
|
|
|
4364
4364
|
* @param {string} [lang='en'] - Language code for localized data
|
|
4365
4365
|
* @returns {Promise<{success: boolean, summary: object, errors: Array, modifiedCount: number}>}
|
|
4366
4366
|
*/
|
|
4367
|
-
export async function installPack(packIdentifier, user = null, lang = 'en',
|
|
4367
|
+
export async function installPack(packIdentifier, user = null, lang = 'en', options = {}) {
|
|
4368
4368
|
let pack;
|
|
4369
4369
|
const packsCollection = getCollection('packs');
|
|
4370
4370
|
|
|
@@ -4377,7 +4377,7 @@ export async function installPack(packIdentifier, user = null, lang = 'en', isTe
|
|
|
4377
4377
|
p = packIdentifier;
|
|
4378
4378
|
}
|
|
4379
4379
|
// Existing behavior - fetch from database
|
|
4380
|
-
pack = await packsCollection.findOne({ $or:[{ _id: p}, { name: packIdentifier }]
|
|
4380
|
+
pack = await packsCollection.findOne({ $and: [{ _user: {$exists: false} }, {private: false}, {$or:[{ _id: p}, { name: packIdentifier }]}]} );
|
|
4381
4381
|
if (!pack) {
|
|
4382
4382
|
throw new Error(`Pack with ID ${packIdentifier} not found.`);
|
|
4383
4383
|
}
|
|
@@ -4465,7 +4465,7 @@ export async function installPack(packIdentifier, user = null, lang = 'en', isTe
|
|
|
4465
4465
|
const dataToInstall = { ...pack.data?.all, ...pack.data?.[lang] };
|
|
4466
4466
|
if (!dataToInstall || Object.keys(dataToInstall).length === 0) {
|
|
4467
4467
|
logger.warn(`Pack '${pack.name}' has no data to install.`);
|
|
4468
|
-
return { success:
|
|
4468
|
+
return { success: false, summary, errors, modifiedCount: 0 };
|
|
4469
4469
|
}
|
|
4470
4470
|
|
|
4471
4471
|
// Process link references (same as original)
|
|
@@ -4618,6 +4618,14 @@ export async function installPack(packIdentifier, user = null, lang = 'en', isTe
|
|
|
4618
4618
|
}
|
|
4619
4619
|
}
|
|
4620
4620
|
|
|
4621
|
+
if( options.installForUser && user?.username ){
|
|
4622
|
+
if( pack.name )
|
|
4623
|
+
await packsCollection.deleteOne({ name: pack.name, _user: user.username });
|
|
4624
|
+
logger.info(`--- Creating pack '${pack.name}' for user... ---`);
|
|
4625
|
+
const packToCreate = {...pack, _id: undefined, private: true, _user: user.username };
|
|
4626
|
+
await packsCollection.insertOne(packToCreate);
|
|
4627
|
+
}
|
|
4628
|
+
|
|
4621
4629
|
// Trigger event only if pack came from database (original behavior)
|
|
4622
4630
|
if (typeof packIdentifier === 'string') {
|
|
4623
4631
|
await Event.Trigger("OnPackInstalled", "event", "system", pack);
|
|
@@ -4636,7 +4644,7 @@ export async function installPack(packIdentifier, user = null, lang = 'en', isTe
|
|
|
4636
4644
|
export const installAllPacks = async () => {
|
|
4637
4645
|
const packs = await getAllPacks();
|
|
4638
4646
|
await packsCollection.deleteMany({ _user: { $exists : false }});
|
|
4639
|
-
await packsCollection.insertMany(packs);
|
|
4647
|
+
await packsCollection.insertMany(packs.map(p =>({...p, private: false })));
|
|
4640
4648
|
}
|
|
4641
4649
|
|
|
4642
4650
|
export async function handleDemoInitialization(req, res) {
|