data-primals-engine 1.0.0

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.
@@ -0,0 +1,192 @@
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 "../../../data-primals-engine/src/i18n.js";
6
+ import {getUserStorageLimit} from "../user.js";
7
+ import {getCollection} from "./mongodb.js";
8
+ import {getFileExtension, isGUID, uuidv4} from "../core.js";
9
+ import path from "node:path";
10
+ import process from "node:process";
11
+ import fs from "node:fs";
12
+ import { checkServerCapacity} from "./data.js";
13
+ import crypto from "node:crypto";
14
+ import * as tar from "tar";
15
+ import {promisify} from "node:util";
16
+ import {calculateTotalUserStorageUsage, hasPermission} from "./user.js";
17
+
18
+ const pbkdf2Async = promisify(crypto.pbkdf2);
19
+
20
+ const fsPromises = fs.promises;
21
+
22
+ // Encryption settings
23
+ const algorithm = 'aes-256-cbc'; // Algorithm to use
24
+ const iterations = 100000; // Number of iterations for PBKDF2
25
+ const keyLength = 32; // Key length for AES-256
26
+ const ivLength = 16; // IV length for AES
27
+
28
+ // Function to derive a key and IV from a passphrase and salt
29
+ async function deriveKeyAndIV(password, salt) {
30
+ const key = await pbkdf2Async(password, salt, iterations, keyLength, 'sha256');
31
+ // L'IV n'est plus dérivé ici.
32
+ return { key };
33
+ }
34
+
35
+
36
+ export const unzip = async (file) => {
37
+ await tar.extract({ file: file, gzip: true, sync: true });
38
+ }
39
+
40
+ export const zip = async (filename) => {
41
+ await tar.create({ gzip: true, sync: true, file: filename+'.gz' }, [filename])
42
+ }
43
+ export const addFile = async (file, user) => {
44
+ if (!file) throw new Error("Le fichier est requis");
45
+
46
+ if (file.size > maxPrivateFileSize) {
47
+ throw new Error(`La taille du fichier dépasse la limite autorisée (${maxPrivateFileSize / megabytes} Mo).`);
48
+ }
49
+
50
+ if (user.username !== 'demo' && isLocalUser(user) && !await hasPermission(["API_ADMIN", "API_UPLOAD_FILE"], user)) {
51
+ throw new Error(i18n.t("api.permission.uploadFile"));
52
+ }
53
+
54
+ const incomingDataSize = file.size;
55
+
56
+ const userStorageLimit = getUserStorageLimit(user);
57
+ const currentStorageUsage = await calculateTotalUserStorageUsage(user);
58
+
59
+ if (currentStorageUsage + incomingDataSize > userStorageLimit) {
60
+ throw new Error(i18n.t("api.data.storageLimitExceeded", { limit: Math.round(userStorageLimit / megabytes) }));
61
+ }
62
+
63
+ const serverCapacity = await checkServerCapacity(incomingDataSize);
64
+ if (!serverCapacity.isSufficient) {
65
+ throw new Error(i18n.t("api.data.serverStorageFull", "Le serveur a atteint sa capacité de stockage maximale. Veuillez réessayer plus tard."));
66
+ }
67
+
68
+ const collection = getCollection("files");
69
+
70
+ // Générer un GUID pour le fichier
71
+ const guid = uuidv4();
72
+ const filename = guid + path.extname(file.originalname || file.name); // Préserver l'extension
73
+ const filepath = path.join(process.cwd(), 'uploads', 'private', filename);
74
+
75
+ try {
76
+ // Enregistrer le fichier sur le serveur
77
+ await fsPromises.mkdir(path.join(process.cwd(), 'uploads', 'private'), {recursive: true});
78
+ await fsPromises.writeFile(filepath, file.buffer || fs.readFileSync(file.path)); // Utiliser buffer si disponible
79
+ if( file.path && fs.existsSync(file.path) ){
80
+ fs.unlinkSync(file.path);
81
+ }
82
+
83
+ // Enregistrer les métadonnées du fichier
84
+ const fileMetadata = {
85
+ guid,
86
+ filename: file.originalname || file.name, // Conserver le nom original
87
+ mimeType: file.type,
88
+ size: file.size,
89
+ mainUser: user._user,
90
+ user: user.username,
91
+ timestamp: new Date()
92
+ };
93
+
94
+ // Insérer le fichier dans une collection dédiée (par exemple, "privateFiles")
95
+ const result = await collection.insertOne({...fileMetadata, _model: "privateFile"});
96
+ if (!result.insertedId) throw new Error("Échec de l'indexation du fichier.");
97
+
98
+ return guid;
99
+
100
+ } catch (error) {
101
+ // Nettoyage en cas d'erreur
102
+ if (fs.existsSync(filepath)) {
103
+ fs.unlinkSync(filepath);
104
+ }
105
+ throw new Error(`Erreur lors de l'ajout du fichier: ${error.message}`);
106
+ }
107
+ };
108
+
109
+ export const removeFile = async (guid, user) => {
110
+ if (!guid) return false;
111
+ if (!isGUID(guid)) throw new Error("Le GUID du fichier n'est pas valide.");
112
+
113
+ const collection = getCollection("files");
114
+
115
+ // Trouver le fichier et vérifier l'autorisation (propriétaire ou admin)
116
+ const file = await collection.findOne({ guid });
117
+ if (!file) throw new Error("Fichier non trouvé (" + guid + ")");
118
+
119
+ if (user.username !== 'demo' && isLocalUser(user) && !await hasPermission(["API_ADMIN", "API_EDIT_DATA", "API_EDIT_DATA_privateFile", `API_EDIT_DATA_privateFile_${guid}`], user) ) {
120
+ if (file._user !== (user._user || user.username)) {
121
+ throw new Error("Vous n'êtes pas autorisé à supprimer ce fichier.");
122
+ }
123
+ }
124
+
125
+ try {
126
+ // Supprimer le fichier de la base de données
127
+ const deleteResult = await collection.deleteOne({ _model: "privateFile", guid });
128
+ if (deleteResult.deletedCount !== 1) throw new Error("Échec de la suppression de l'indexation du fichier.");
129
+
130
+ // Supprimer le fichier du serveur
131
+ const filepath = path.join(process.cwd(), 'uploads', 'private', guid)+'.'+getFileExtension(file.filename);
132
+ if (fs.existsSync(filepath)) {
133
+ fs.unlinkSync(filepath);
134
+ }
135
+
136
+ return { success: true, message: "Fichier supprimé avec succès." };
137
+ } catch (error) {
138
+ throw new Error(`Erreur lors de la suppression du fichier: ${error.message}`);
139
+ }
140
+ };
141
+
142
+
143
+
144
+
145
+ // Function to encrypt the file content
146
+ export async function encryptFile(filePath, password) {
147
+ try {
148
+ const salt = crypto.randomBytes(16);
149
+ const iv = crypto.randomBytes(ivLength);
150
+
151
+ // On ne dérive que la clé
152
+ const { key } = await deriveKeyAndIV(password, salt);
153
+
154
+ const fileData = await fs.promises.readFile(filePath);
155
+ const cipher = crypto.createCipheriv(algorithm, key, iv);
156
+ const encryptedData = Buffer.concat([cipher.update(fileData), cipher.final()]);
157
+
158
+ // On it : [salt][iv][données chiffrées]
159
+ await fs.promises.writeFile(filePath, Buffer.concat([salt, iv, encryptedData]));
160
+
161
+ console.log('File encrypted successfully.', filePath);
162
+ } catch (error) {
163
+ console.error('Error during encryption:', error.message);
164
+ }
165
+ }
166
+ // Function to decrypt the file content
167
+ export async function decryptFile(filePath, password) {
168
+ try {
169
+ const fileData = await fs.promises.readFile(filePath);
170
+
171
+ // Extraire le sel, l'IV et les données
172
+ const salt = fileData.slice(0, 16);
173
+ // NOUVEAU: Extraire l'IV qui suit le sel
174
+ const iv = fileData.slice(16, 16 + ivLength);
175
+ const encryptedData = fileData.slice(16 + ivLength);
176
+
177
+ // On dérive la même clé en utilisant le sel extrait
178
+ const { key } = await deriveKeyAndIV(password, salt);
179
+
180
+ // On utilise l'IV extrait pour le déchiffrement
181
+ const decipher = crypto.createDecipheriv(algorithm, key, iv);
182
+ const decryptedData = Buffer.concat([decipher.update(encryptedData), decipher.final()]);
183
+
184
+ await fs.promises.writeFile(filePath, decryptedData);
185
+
186
+ console.log('File decrypted successfully.');
187
+ } catch (error) {
188
+ console.error('Error during decryption:', error.message);
189
+ // Relancer l'erreur peut être utile pour que l'appelant sache que ça a échoué
190
+ throw new Error(`Decryption failed: ${error.message}`);
191
+ }
192
+ }
@@ -0,0 +1,67 @@
1
+
2
+ import process from "process";
3
+ import {MongoClient as InternalMongoClient} from "mongodb";
4
+ import {Logger} from "../gameObject.js";
5
+ import {MongoDatabase} from "../engine.js";
6
+ import * as tls from "node:tls";
7
+ import fs from "node:fs";
8
+
9
+ export let modelsCollection, datasCollection, filesCollection, packsCollection;
10
+
11
+ let engine, logger;
12
+ export async function onInit(defaultEngine) {
13
+ engine = defaultEngine;
14
+ logger = engine.getComponent(Logger);
15
+
16
+ const isProduction = process.env.NODE_ENV === 'production'
17
+
18
+ let ca, cert, key;
19
+ try {
20
+ ca = fs.readFileSync('certs/mongodb-cert.crt');
21
+ cert = fs.readFileSync('certs/mongodb.pem');
22
+ key = fs.readFileSync(`certs/mongodb-cert.key`);
23
+ } catch (e) {
24
+
25
+ }
26
+ // Create a SecureContext object
27
+ // Connection URL
28
+ const dbUrl = process.env.MONGO_DB_URL || 'mongodb://localhost:27017';
29
+ const MongoClient = new InternalMongoClient(dbUrl, {
30
+ tls: false, maxPoolSize: 20
31
+ });
32
+ await MongoClient.connect();
33
+
34
+ modelsCollection = MongoDatabase.collection("models");
35
+ datasCollection = MongoDatabase.collection("datas");
36
+ filesCollection = MongoDatabase.collection("files");
37
+ packsCollection = MongoDatabase.collection("packs");
38
+
39
+ logger.info("MongoDB collections loaded.");
40
+
41
+ };
42
+
43
+
44
+ export const isObjectId = (id) => {
45
+ return (typeof(id) === 'string' && id.match(/^[0-9a-fA-F]{24}$/));
46
+ };
47
+
48
+
49
+ export const getCollection = (str) => {
50
+ return MongoDatabase.collection(str);
51
+ }
52
+
53
+
54
+
55
+ // New function to determine the collection name for a user
56
+ export const getUserCollectionName = (user) => {
57
+ return user?.userPlan === 'premium' ? `datas_${user.username}` : 'datas';
58
+ };
59
+
60
+
61
+ // Modify existing functions to use the correct collection
62
+ export const getCollectionForUser = (user) => {
63
+ const collectionName = getUserCollectionName(user);
64
+ return getCollection(collectionName);
65
+ };
66
+
67
+
@@ -0,0 +1,18 @@
1
+
2
+ import swaggerUi from 'swagger-ui-express';
3
+
4
+ import YAML from 'yaml';
5
+
6
+ import {Logger} from "../gameObject.js";
7
+ import fs from "node:fs";
8
+ let engine, logger;
9
+
10
+ export async function onInit(defaultEngine) {
11
+ engine = defaultEngine;
12
+ logger = engine.getComponent(Logger);
13
+
14
+ const swaggerDocument = YAML.parse(fs.readFileSync(process.cwd() + '/swagger-en.yml', 'utf8'));
15
+
16
+ engine.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument));
17
+
18
+ };
@@ -0,0 +1,226 @@
1
+ import i18n from "../../../data-primals-engine/src/i18n.js";
2
+ import {MongoClient, MongoDatabase} from "../engine.js";
3
+ import {getCollection, getCollectionForUser, getUserCollectionName} from "./mongodb.js";
4
+ import {dbName, plans} from "../constants.js";
5
+ import {isLocalUser} from "../data.js";
6
+ import {ObjectId} from "mongodb";
7
+ import {getAPILang} from "./data.js";
8
+ import {Logger} from "../gameObject.js";
9
+ import rateLimit from "express-rate-limit";
10
+
11
+ export const userInitiator = async (req, res, next) => {
12
+
13
+ const lang = getAPILang(req.query.lang || req.headers['Accept-Language']);
14
+
15
+ req.lang = lang;
16
+ if(req.me)
17
+ req.me.lang = lang;
18
+ res.setHeader('Content-Language', lang);
19
+
20
+ // set current lang for user
21
+ i18n.changeLanguage(lang);
22
+
23
+ if (req.me.userPlan === 'premium') {
24
+ const collections = await MongoDatabase.listCollections().toArray();
25
+ const collectionNames = collections.map(c => c.name);
26
+ const coll = getUserCollectionName(req.me);
27
+ if (collectionNames.includes(coll)) {
28
+ const collection = await MongoDatabase.createCollection(coll);
29
+ const indexes = await collection.indexes();
30
+ if (!indexes.find(i => i.name === 'genericPartialIndex')) {
31
+ await collection.createIndex({"$**": 1}, {
32
+ name: 'genericPartialIndex',
33
+ partialFilterExpression: {
34
+ _model: 1,
35
+ _user: 1
36
+ }
37
+ });
38
+ }
39
+ if (!await collection.indexExists("_hash")) {
40
+ await collection.createIndex({_hash: 1});
41
+ }
42
+ if (!indexes.find(i => i.name === 'modelUserIndex')) {
43
+ await collection.createIndex({_model: 1, _user: 1}, {name: 'modelUserIndex'});
44
+ }
45
+ }
46
+ }
47
+ next();
48
+ }
49
+
50
+
51
+ export const middlewareAuthenticator = async (req, res, next) => {
52
+ const engine = req.app.get('engine');
53
+ if (!engine || !engine.userProvider) {
54
+ // Sécurité pour s'assurer que le moteur est bien configuré
55
+ return res.status(500).json({ error: "UserProvider not configured in engine." });
56
+ }
57
+
58
+ try {
59
+ // 1. On demande au provider (votre PrimalsUserProvider) d'identifier l'utilisateur
60
+ await engine.userProvider.initiateUser(req);
61
+
62
+ // 2. On vérifie simplement si le provider a attaché un utilisateur
63
+ if (req.me) {
64
+ // L'utilisateur est authentifié, on continue
65
+ return next();
66
+ } else {
67
+ // Le provider n'a trouvé aucun utilisateur valide
68
+ return res.status(401).json({ error: "Authentication required" });
69
+ }
70
+ } catch (e) {
71
+ // Le provider peut lever une erreur (ex: token invalide, compte non vérifié)
72
+ return res.status(401).json({ error: e.message || "Authentication failed" });
73
+ }
74
+ };
75
+
76
+ export const generateLimiter = rateLimit({
77
+ windowMs: 7000,
78
+ limit: 1,
79
+ standardHeaders: true,
80
+ legacyHeaders: false,
81
+ skip: (req) => {
82
+ return !!req.body?.confirmedAction;
83
+ }
84
+ });
85
+
86
+ const freeSearchLimiter = rateLimit({
87
+ windowMs: 1000 * 3600,
88
+ limit: plans.free.requestLimitPerHour,
89
+ standardHeaders: true,
90
+ legacyHeaders: false,
91
+ });
92
+ const premiumSearchLimiter = rateLimit({
93
+ windowMs: 1000 * 3600,
94
+ limit: plans.premium.requestLimitPerHour,
95
+ standardHeaders: true,
96
+ legacyHeaders: false,
97
+ });
98
+
99
+ export const myFreePremiumAnonymousLimiter = async function (req, res, next) {
100
+ const user = req.me;
101
+ if (user?.userPlan === "premium") {
102
+ return premiumSearchLimiter(req, res, next);
103
+ }
104
+ return freeSearchLimiter(req, res, next);
105
+ };
106
+
107
+ let logger,engine;
108
+ export async function onInit(defaultEngine) {
109
+ engine = defaultEngine;
110
+ logger = engine.getComponent(Logger);
111
+
112
+
113
+ }
114
+
115
+ export async function hasPermission(permissionNames, user) {
116
+ if( !isLocalUser(user)){
117
+ return user.roles?.some(f => permissionNames.includes(f));
118
+ }
119
+ try {
120
+ // Si on a une string on le transforme en tableau.
121
+ const permissionNamesArray = Array.isArray(permissionNames) ? permissionNames : [permissionNames];
122
+ const collection = getCollectionForUser(user);
123
+
124
+ const job = [
125
+ {
126
+ $lookup: {
127
+ from: 'datas',
128
+ let: { rolesIds: (user.roles ||[]).map(m => new ObjectId(m)) },
129
+ pipeline: [
130
+ {
131
+ $match: {
132
+ $expr: {
133
+ $and: [
134
+ { $in: ['$_id', '$$rolesIds'] },
135
+ ],
136
+ },
137
+ },
138
+ },
139
+ {
140
+ $lookup: {
141
+ from: 'datas',
142
+ let: { rolePermissions: {
143
+ "$map": {
144
+ "input": "$permissions",
145
+ "in": { "$toObjectId": "$$this" }
146
+ }
147
+ } },
148
+ pipeline: [
149
+ {
150
+ $match: {
151
+ $expr: {
152
+ $and: [
153
+ { $in: ['$_id', '$$rolePermissions'] },
154
+ ],
155
+ },
156
+ },
157
+ },
158
+ { $limit: 1 },
159
+ ],
160
+ as: 'permissions',
161
+ },
162
+ },
163
+ { $unwind: { path: '$permissions', preserveNullAndEmptyArrays: true } },
164
+ ],
165
+ as: 'roles',
166
+ },
167
+ },
168
+ { $unwind: { path: '$roles', preserveNullAndEmptyArrays: true } },
169
+ { $match: { 'roles.permissions.name': { $in: permissionNamesArray } } }, // Match if permissions.name in array
170
+ { $limit: 1 },
171
+ { $project: { _id: 0, hasPermission: { $cond: [{ $in: ['$roles.permissions.name', permissionNamesArray] }, true, false] } } } //check the value
172
+ ];
173
+ const result = await collection.aggregate(job).toArray();
174
+ return result.length === 1 && result[0].hasPermission;
175
+ } catch (e) {
176
+ logger.error(e);
177
+ return false;
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Calcule l'utilisation totale de l'espace de stockage pour un utilisateur en octets.
183
+ * Cela inclut la taille des documents dans sa collection de données et la taille de ses fichiers uploadés.
184
+ * @param {object} user - L'objet utilisateur.
185
+ * @returns {Promise<number>} - L'utilisation totale en octets.
186
+ */
187
+ export async function calculateTotalUserStorageUsage(user) {
188
+ const userId = user._user || user.username;
189
+ const datasCollection = getCollectionForUser(user);
190
+ const filesCollection = getCollection("files");
191
+
192
+ // Pipeline pour calculer la taille des documents de données
193
+ const dataSizePipeline = [
194
+ { $match: { _user: userId } },
195
+ {
196
+ $group: {
197
+ _id: null, // Grouper tous les documents ensemble
198
+ totalSize: { $sum: { $bsonSize: "$$ROOT" } } // Sommer la taille BSON de chaque document
199
+ }
200
+ }
201
+ ];
202
+
203
+ // Pipeline pour calculer la taille des fichiers
204
+ const fileSizePipeline = [
205
+ // Le champ est 'user' dans la collection 'files' selon votre fonction addFile
206
+ { $match: { user: userId, _model: "privateFile" } },
207
+ {
208
+ $group: {
209
+ _id: null,
210
+ totalSize: { $sum: "$size" }
211
+ }
212
+ }
213
+ ];
214
+
215
+ // Exécuter les deux calculs en parallèle pour plus d'efficacité
216
+ const [dataResult, fileResult] = await Promise.all([
217
+ datasCollection.aggregate(dataSizePipeline).toArray(),
218
+ filesCollection.aggregate(fileSizePipeline).toArray()
219
+ ]);
220
+
221
+ const dataSize = dataResult.length > 0 ? dataResult[0].totalSize : 0;
222
+ const filesSize = fileResult.length > 0 ? fileResult[0].totalSize : 0;
223
+
224
+ logger.debug(`[Storage] User ${userId}: Data size = ${dataSize} bytes, Files size = ${filesSize} bytes. Total = ${dataSize + filesSize} bytes.`);
225
+ return dataSize + filesSize;
226
+ }