@galadrim/galadmin 0.1.235 → 0.2.6
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/.github/workflows/main.yml +33 -2
- package/README.md +65 -0
- package/dist/api.d.ts +16 -0
- package/dist/api.js +276 -215
- package/dist/api.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utils/dbManager.d.ts +6 -0
- package/dist/utils/dbManager.js +150 -10
- package/dist/utils/dbManager.js.map +1 -1
- package/dist/utils/imageDimensions.d.ts +5 -0
- package/dist/utils/imageDimensions.js +98 -0
- package/dist/utils/imageDimensions.js.map +1 -0
- package/jest.config.js +43 -0
- package/jest.integration.config.js +16 -0
- package/package.json +30 -10
- package/src/api.ts +403 -303
- package/src/front/ckeditor5-build/node_modules/.bin/semver +12 -0
- package/src/front/ckeditor5-build/node_modules/.bin/semver.cmd +17 -0
- package/src/front/ckeditor5-build/node_modules/.bin/semver.ps1 +28 -0
- package/src/front/ckeditor5-build/node_modules/semver/bin/semver.js +0 -0
- package/src/front/src/App.test.js +28 -0
- package/src/front/src/components/OutsideClickHandler.test.js +53 -0
- package/src/front/src/components/RedirectToFirstView.test.js +38 -0
- package/src/front/src/utils/checkRequiredColumns.test.js +46 -0
- package/src/front/src/utils/evalExpression.test.js +29 -0
- package/src/index.ts +1 -1
- package/src/utils/dbManager.ts +248 -102
- package/src/utils/imageDimensions.ts +112 -0
- package/tests/__mocks__/@aws-sdk/client-s3.js +6 -0
- package/tests/__mocks__/@aws-sdk/s3-request-presigner.js +3 -0
- package/tests/__mocks__/@google-cloud/storage.js +7 -0
- package/tests/__mocks__/argon2.js +3 -0
- package/tests/__mocks__/axios.js +7 -0
- package/tests/__mocks__/connect-pg-simple.js +24 -0
- package/tests/__mocks__/express-mysql-session.js +24 -0
- package/tests/__mocks__/mysql2/promise.js +3 -0
- package/tests/__mocks__/pg.js +3 -0
- package/tests/__mocks__/rollup.js +5 -0
- package/tests/__mocks__/ssh2-sftp-client.js +10 -0
- package/tests/api-helpers.test.ts +376 -0
- package/tests/dbManager.init.test.ts +129 -0
- package/tests/dbManager.test.ts +265 -0
- package/tests/edge-cases.test.ts +167 -0
- package/tests/helpers/app.ts +69 -0
- package/tests/helpers/fakeDb.ts +66 -0
- package/tests/helpers/fakePool.ts +76 -0
- package/tests/helpers/fixtures.ts +68 -0
- package/tests/imageDimensions.test.ts +111 -0
- package/tests/index.sessionStore.test.ts +19 -0
- package/tests/index.test.ts +363 -0
- package/tests/integration/__mocks__/axios.js +3 -0
- package/tests/integration/dbHarness.ts +381 -0
- package/tests/integration/dbManager.integration.test.ts +69 -0
- package/tests/integration/globalSetup.ts +43 -0
- package/tests/integration/globalTeardown.ts +42 -0
- package/tests/integration/routes.authorization.integration.test.ts +319 -0
- package/tests/integration/routes.config.integration.test.ts +189 -0
- package/tests/integration/routes.crud.integration.test.ts +805 -0
- package/tests/integration/routes.custom.integration.test.ts +376 -0
- package/tests/integration/routes.foreign.integration.test.ts +306 -0
- package/tests/integration/routes.importExport.integration.test.ts +404 -0
- package/tests/integration/routes.integration.test.ts +366 -0
- package/tests/integration/routes.postgresql.integration.test.ts +88 -0
- package/tests/integration/routes.records.integration.test.ts +634 -0
- package/tests/integration/routes.upload.integration.test.ts +425 -0
- package/tests/setup/silence-console.ts +15 -0
package/dist/api.js
CHANGED
|
@@ -11,6 +11,15 @@ var __rest = (this && this.__rest) || function (s, e) {
|
|
|
11
11
|
return t;
|
|
12
12
|
};
|
|
13
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.looksLikeSqlBreakout = exports.writableTables = exports.areSafeColumnNames = exports.MUTATING_VIEW_ROUTES = exports.viewFromRequest = exports.canWriteView = exports.roleOf = exports.ROOT_ROLE = void 0;
|
|
15
|
+
exports.generateUniqueIdForObject = generateUniqueIdForObject;
|
|
16
|
+
exports.sqlAnd = sqlAnd;
|
|
17
|
+
exports.getFormattedValue = getFormattedValue;
|
|
18
|
+
exports.evalExpression = evalExpression;
|
|
19
|
+
exports.formatValueForInsertion = formatValueForInsertion;
|
|
20
|
+
exports.collectAllowedQueries = collectAllowedQueries;
|
|
21
|
+
exports.enrichConfig = enrichConfig;
|
|
22
|
+
exports.enrichColumn = enrichColumn;
|
|
14
23
|
const express = require("express");
|
|
15
24
|
const multiparty = require("multiparty");
|
|
16
25
|
const fs = require("fs");
|
|
@@ -20,7 +29,7 @@ const mime = require("mime");
|
|
|
20
29
|
const client_s3_1 = require("@aws-sdk/client-s3");
|
|
21
30
|
const s3_request_presigner_1 = require("@aws-sdk/s3-request-presigner");
|
|
22
31
|
const storage_1 = require("@google-cloud/storage");
|
|
23
|
-
const
|
|
32
|
+
const imageDimensions_1 = require("./utils/imageDimensions");
|
|
24
33
|
const serializeJs = require("serialize-javascript");
|
|
25
34
|
const SftpClient = require("ssh2-sftp-client");
|
|
26
35
|
const rollup = require("rollup");
|
|
@@ -28,19 +37,175 @@ const plugin_babel_1 = require("@rollup/plugin-babel");
|
|
|
28
37
|
const plugin_node_resolve_1 = require("@rollup/plugin-node-resolve");
|
|
29
38
|
const axios_1 = require("axios");
|
|
30
39
|
const uuid_1 = require("uuid");
|
|
40
|
+
const dbManager_1 = require("./utils/dbManager");
|
|
31
41
|
function generateUniqueIdForObject(obj) {
|
|
32
42
|
const str = JSON.stringify(obj);
|
|
33
43
|
const hash = crypto.createHash("sha256").update(str).digest("hex");
|
|
34
44
|
return hash;
|
|
35
45
|
}
|
|
46
|
+
exports.ROOT_ROLE = "root";
|
|
47
|
+
const roleOf = (req) => req.session && req.session.user && req.session.user.role;
|
|
48
|
+
exports.roleOf = roleOf;
|
|
49
|
+
const canWriteView = (view, role) => role === exports.ROOT_ROLE || !view || !view.roles || view.roles.includes(role);
|
|
50
|
+
exports.canWriteView = canWriteView;
|
|
51
|
+
const viewFromRequest = (req) => {
|
|
52
|
+
const idx = req.body && req.body.viewIndex !== undefined
|
|
53
|
+
? req.body.viewIndex
|
|
54
|
+
: req.query && req.query.viewIndex !== undefined
|
|
55
|
+
? req.query.viewIndex
|
|
56
|
+
: undefined;
|
|
57
|
+
if (idx === undefined || !req.config || !req.config.views)
|
|
58
|
+
return undefined;
|
|
59
|
+
return req.config.views[idx];
|
|
60
|
+
};
|
|
61
|
+
exports.viewFromRequest = viewFromRequest;
|
|
62
|
+
exports.MUTATING_VIEW_ROUTES = new Set([
|
|
63
|
+
"/updateRecord",
|
|
64
|
+
"/createRecord",
|
|
65
|
+
"/deleteRecord",
|
|
66
|
+
"/deleteRecords",
|
|
67
|
+
"/deleteRecordFromPanel",
|
|
68
|
+
"/customAction",
|
|
69
|
+
"/uploadFile",
|
|
70
|
+
]);
|
|
71
|
+
const areSafeColumnNames = (data) => !data || Object.keys(data).every((k) => /^[A-Za-z0-9_]+$/.test(k));
|
|
72
|
+
exports.areSafeColumnNames = areSafeColumnNames;
|
|
73
|
+
const writableTables = (req) => {
|
|
74
|
+
var _a, _b;
|
|
75
|
+
const role = (0, exports.roleOf)(req);
|
|
76
|
+
const tables = new Set();
|
|
77
|
+
for (const view of ((_a = req.config) === null || _a === void 0 ? void 0 : _a.views) || []) {
|
|
78
|
+
if (!(0, exports.canWriteView)(view, role))
|
|
79
|
+
continue;
|
|
80
|
+
if (view.tableName)
|
|
81
|
+
tables.add(view.tableName);
|
|
82
|
+
for (const panel of view.recordViewPanels || []) {
|
|
83
|
+
if (panel.tableName)
|
|
84
|
+
tables.add(panel.tableName);
|
|
85
|
+
if ((_b = panel.createConfig) === null || _b === void 0 ? void 0 : _b.tableName)
|
|
86
|
+
tables.add(panel.createConfig.tableName);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return tables;
|
|
90
|
+
};
|
|
91
|
+
exports.writableTables = writableTables;
|
|
92
|
+
const looksLikeSqlBreakout = (frag) => typeof frag === "string" &&
|
|
93
|
+
/(;|--|\/\*|\bunion\b|\binto\s+(out|dump)file\b|\bsleep\s*\(|\bbenchmark\s*\(|\bload_file\s*\()/i.test(frag);
|
|
94
|
+
exports.looksLikeSqlBreakout = looksLikeSqlBreakout;
|
|
95
|
+
function sqlAnd(conditions) {
|
|
96
|
+
const existingConditions = [];
|
|
97
|
+
for (const condition of conditions) {
|
|
98
|
+
if (condition && condition.length > 0) {
|
|
99
|
+
existingConditions.push(condition);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (existingConditions.length <= 1) {
|
|
103
|
+
return existingConditions[0] || null;
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
let result = "";
|
|
107
|
+
for (let i = 0; i < existingConditions.length; i++) {
|
|
108
|
+
if (i > 0) {
|
|
109
|
+
result += " AND ";
|
|
110
|
+
}
|
|
111
|
+
result += "(" + existingConditions[i] + ")";
|
|
112
|
+
}
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function getFormattedValue(column, record) {
|
|
117
|
+
let value;
|
|
118
|
+
try {
|
|
119
|
+
record.options = column.options;
|
|
120
|
+
if (column.formatValueForExport) {
|
|
121
|
+
value = await column.formatValueForExport(record);
|
|
122
|
+
}
|
|
123
|
+
else if (column.formattedValue) {
|
|
124
|
+
value = evalExpression(column.formattedValue, record);
|
|
125
|
+
}
|
|
126
|
+
else if (column.formatValue) {
|
|
127
|
+
value = await column.formatValue(record);
|
|
128
|
+
}
|
|
129
|
+
else if (column.dataType == "tinyint") {
|
|
130
|
+
value = record[column.name] == 1 ? "Oui" : "Non";
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
value = record[column.name];
|
|
134
|
+
}
|
|
135
|
+
return value === null || typeof value == "undefined" ? "" : value;
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
return value || "";
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function evalExpression(expression, data) {
|
|
142
|
+
let varString = "";
|
|
143
|
+
let statement = "";
|
|
144
|
+
try {
|
|
145
|
+
for (let key in data) {
|
|
146
|
+
statement =
|
|
147
|
+
"var " +
|
|
148
|
+
key +
|
|
149
|
+
" = " +
|
|
150
|
+
(typeof data[key] === "string"
|
|
151
|
+
? "'" +
|
|
152
|
+
data[key]
|
|
153
|
+
.replace(/'/g, "\\'")
|
|
154
|
+
.replace(/\n/g, "")
|
|
155
|
+
.replace(/\r/g, "") +
|
|
156
|
+
"'"
|
|
157
|
+
: typeof data[key] === "object"
|
|
158
|
+
? JSON.stringify(data[key])
|
|
159
|
+
: data[key]) +
|
|
160
|
+
";";
|
|
161
|
+
varString += statement;
|
|
162
|
+
}
|
|
163
|
+
const result = eval(varString + expression);
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
catch (e) {
|
|
167
|
+
console.log("Problème d'évaluation de l'expression", statement);
|
|
168
|
+
console.error(e);
|
|
169
|
+
return "Error";
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function formatValueForInsertion(value, dbType) {
|
|
173
|
+
if (value !== 0 && !value) {
|
|
174
|
+
return "NULL";
|
|
175
|
+
}
|
|
176
|
+
if (value instanceof Date) {
|
|
177
|
+
return value.toISOString();
|
|
178
|
+
}
|
|
179
|
+
if (typeof value === "string") {
|
|
180
|
+
if (dbType === "POSTGRESQL") {
|
|
181
|
+
return "'" + value.replace(/'/g, "''") + "'";
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
return "'" + value.replace(/'/g, "\\'") + "'";
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (typeof value === "object") {
|
|
188
|
+
return "'" + JSON.stringify(value).replace(/'/g, "\\'") + "'";
|
|
189
|
+
}
|
|
190
|
+
if (typeof value === "boolean") {
|
|
191
|
+
return value ? "TRUE" : "FALSE";
|
|
192
|
+
}
|
|
193
|
+
if (typeof value === "number") {
|
|
194
|
+
return value;
|
|
195
|
+
}
|
|
196
|
+
return value;
|
|
197
|
+
}
|
|
36
198
|
exports.default = (db, config) => {
|
|
37
199
|
const app = express();
|
|
38
200
|
app.use((req, res, next) => {
|
|
39
201
|
req.config = Object.assign(Object.assign({}, config), { views: config.views
|
|
40
|
-
.filter(x => !x.roles
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
202
|
+
.filter((x) => !x.roles ||
|
|
203
|
+
(req.session.user &&
|
|
204
|
+
[...(x.roles || []), ...(x.readRoles || [])].includes(req.session.user.role)))
|
|
205
|
+
.map((view) => {
|
|
206
|
+
const readOnly = req.session.user &&
|
|
207
|
+
view.roles &&
|
|
208
|
+
!view.roles.includes(req.session.user.role);
|
|
44
209
|
if (readOnly) {
|
|
45
210
|
return Object.assign(Object.assign({}, view), { canEdit: false, canCreate: false, canDelete: false, clickToEdit: false });
|
|
46
211
|
}
|
|
@@ -48,9 +213,11 @@ exports.default = (db, config) => {
|
|
|
48
213
|
return view;
|
|
49
214
|
}
|
|
50
215
|
})
|
|
51
|
-
.map(view => {
|
|
216
|
+
.map((view) => {
|
|
52
217
|
if (view.type === "table") {
|
|
53
|
-
return Object.assign(Object.assign({}, view), { columns: view.columns.filter(column => !column.roles ||
|
|
218
|
+
return Object.assign(Object.assign({}, view), { columns: view.columns.filter((column) => !column.roles ||
|
|
219
|
+
(req.session.user &&
|
|
220
|
+
column.roles.includes(req.session.user.role))) });
|
|
54
221
|
}
|
|
55
222
|
else {
|
|
56
223
|
return view;
|
|
@@ -64,60 +231,16 @@ exports.default = (db, config) => {
|
|
|
64
231
|
}
|
|
65
232
|
next();
|
|
66
233
|
});
|
|
67
|
-
const ROOT_ROLE = "root";
|
|
68
|
-
const roleOf = (req) => req.session && req.session.user && req.session.user.role;
|
|
69
|
-
const canWriteView = (view, role) => role === ROOT_ROLE || !view || !view.roles || view.roles.includes(role);
|
|
70
|
-
const viewFromRequest = (req) => {
|
|
71
|
-
const idx = req.body && req.body.viewIndex !== undefined
|
|
72
|
-
? req.body.viewIndex
|
|
73
|
-
: req.query && req.query.viewIndex !== undefined
|
|
74
|
-
? req.query.viewIndex
|
|
75
|
-
: undefined;
|
|
76
|
-
if (idx === undefined || !req.config || !req.config.views)
|
|
77
|
-
return undefined;
|
|
78
|
-
return req.config.views[idx];
|
|
79
|
-
};
|
|
80
|
-
const MUTATING_VIEW_ROUTES = new Set([
|
|
81
|
-
"/updateRecord",
|
|
82
|
-
"/createRecord",
|
|
83
|
-
"/deleteRecord",
|
|
84
|
-
"/deleteRecords",
|
|
85
|
-
"/deleteRecordFromPanel",
|
|
86
|
-
"/customAction",
|
|
87
|
-
"/uploadFile",
|
|
88
|
-
]);
|
|
89
|
-
const areSafeColumnNames = (data) => !data ||
|
|
90
|
-
Object.keys(data).every((k) => /^[A-Za-z0-9_]+$/.test(k));
|
|
91
|
-
const writableTables = (req) => {
|
|
92
|
-
var _a, _b;
|
|
93
|
-
const role = roleOf(req);
|
|
94
|
-
const tables = new Set();
|
|
95
|
-
for (const view of ((_a = req.config) === null || _a === void 0 ? void 0 : _a.views) || []) {
|
|
96
|
-
if (!canWriteView(view, role))
|
|
97
|
-
continue;
|
|
98
|
-
if (view.tableName)
|
|
99
|
-
tables.add(view.tableName);
|
|
100
|
-
for (const panel of view.recordViewPanels || []) {
|
|
101
|
-
if (panel.tableName)
|
|
102
|
-
tables.add(panel.tableName);
|
|
103
|
-
if ((_b = panel.createConfig) === null || _b === void 0 ? void 0 : _b.tableName)
|
|
104
|
-
tables.add(panel.createConfig.tableName);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
return tables;
|
|
108
|
-
};
|
|
109
|
-
const looksLikeSqlBreakout = (frag) => typeof frag === "string" &&
|
|
110
|
-
/(;|--|\/\*|\bunion\b|\binto\s+(out|dump)file\b|\bsleep\s*\(|\bbenchmark\s*\(|\bload_file\s*\()/i.test(frag);
|
|
111
234
|
app.use((req, res, next) => {
|
|
112
|
-
const role = roleOf(req);
|
|
235
|
+
const role = (0, exports.roleOf)(req);
|
|
113
236
|
if (req.body && Array.isArray(req.body.filters)) {
|
|
114
|
-
if (req.body.filters.some(looksLikeSqlBreakout)) {
|
|
237
|
+
if (req.body.filters.some(exports.looksLikeSqlBreakout)) {
|
|
115
238
|
return res.status(400).send({ error: "Invalid filter" });
|
|
116
239
|
}
|
|
117
240
|
}
|
|
118
|
-
if (MUTATING_VIEW_ROUTES.has(req.path)) {
|
|
119
|
-
const view = viewFromRequest(req);
|
|
120
|
-
if (!view || !canWriteView(view, role)) {
|
|
241
|
+
if (exports.MUTATING_VIEW_ROUTES.has(req.path)) {
|
|
242
|
+
const view = (0, exports.viewFromRequest)(req);
|
|
243
|
+
if (!view || !(0, exports.canWriteView)(view, role)) {
|
|
121
244
|
return res.sendStatus(403);
|
|
122
245
|
}
|
|
123
246
|
}
|
|
@@ -150,10 +273,13 @@ exports.default = (db, config) => {
|
|
|
150
273
|
else if (columns[i].name) {
|
|
151
274
|
queryString += columns[i].name
|
|
152
275
|
.split(".")
|
|
153
|
-
.map((x) => (db.dbType
|
|
276
|
+
.map((x) => (0, dbManager_1.quoteIdentifier)(x, db.dbType))
|
|
154
277
|
.join(".");
|
|
155
278
|
}
|
|
156
|
-
|
|
279
|
+
const searchEscaped = db.dbType === "POSTGRESQL"
|
|
280
|
+
? params.query.replace(/'/g, "''")
|
|
281
|
+
: params.query.replace(/'/g, "\\'");
|
|
282
|
+
queryString += " LIKE '%" + searchEscaped + "%'";
|
|
157
283
|
isNewCondition = false;
|
|
158
284
|
}
|
|
159
285
|
}
|
|
@@ -161,13 +287,13 @@ exports.default = (db, config) => {
|
|
|
161
287
|
}
|
|
162
288
|
const limit = params.allRecords ? 999999 : parseInt(params.limit);
|
|
163
289
|
const query = params.customQuery ||
|
|
164
|
-
`SELECT
|
|
165
|
-
${params.selectExpression || "*"}
|
|
166
|
-
FROM ${params.table}
|
|
167
|
-
${params.join || ""}
|
|
168
|
-
${queryString}
|
|
169
|
-
${params.groupBy ? "GROUP BY " + params.groupBy : ""}
|
|
170
|
-
${params.having ? " HAVING " + params.having : ""}
|
|
290
|
+
`SELECT
|
|
291
|
+
${params.selectExpression || "*"}
|
|
292
|
+
FROM ${params.table}
|
|
293
|
+
${params.join || ""}
|
|
294
|
+
${queryString}
|
|
295
|
+
${params.groupBy ? "GROUP BY " + params.groupBy : ""}
|
|
296
|
+
${params.having ? " HAVING " + params.having : ""}
|
|
171
297
|
${params.orderBy || params.primaryId ? " ORDER BY " + (params.orderBy || `${params.table}.${params.primaryId}`) : ""}`;
|
|
172
298
|
const { err, rows } = await db.query(`${query} LIMIT ${limit} OFFSET ${Math.max((params.page - 1) * limit, 0)}`);
|
|
173
299
|
if (err) {
|
|
@@ -238,6 +364,7 @@ exports.default = (db, config) => {
|
|
|
238
364
|
}
|
|
239
365
|
catch (err) {
|
|
240
366
|
console.error(err);
|
|
367
|
+
res.sendStatus(500);
|
|
241
368
|
}
|
|
242
369
|
});
|
|
243
370
|
app.post("/adjacentRecordId", async function (req, res, next) {
|
|
@@ -250,27 +377,6 @@ exports.default = (db, config) => {
|
|
|
250
377
|
recordId: recordId,
|
|
251
378
|
});
|
|
252
379
|
});
|
|
253
|
-
const sqlAnd = (conditions) => {
|
|
254
|
-
const existingConditions = [];
|
|
255
|
-
for (const condition of conditions) {
|
|
256
|
-
if (condition && condition.length > 0) {
|
|
257
|
-
existingConditions.push(condition);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
if (existingConditions.length <= 1) {
|
|
261
|
-
return existingConditions[0] || null;
|
|
262
|
-
}
|
|
263
|
-
else {
|
|
264
|
-
let result = "";
|
|
265
|
-
for (let i = 0; i < existingConditions.length; i++) {
|
|
266
|
-
if (i > 0) {
|
|
267
|
-
result += " AND ";
|
|
268
|
-
}
|
|
269
|
-
result += "(" + existingConditions[i] + ")";
|
|
270
|
-
}
|
|
271
|
-
return result;
|
|
272
|
-
}
|
|
273
|
-
};
|
|
274
380
|
app.post("/foreignSelectRecords", async (req, res, next) => {
|
|
275
381
|
var _a, _b, _c, _d, _e;
|
|
276
382
|
const view = req.config.views[req.body.viewIndex];
|
|
@@ -375,88 +481,6 @@ exports.default = (db, config) => {
|
|
|
375
481
|
});
|
|
376
482
|
});
|
|
377
483
|
const formattedValueToFunction = {};
|
|
378
|
-
async function getFormattedValue(column, record) {
|
|
379
|
-
let value;
|
|
380
|
-
try {
|
|
381
|
-
record.options = column.options;
|
|
382
|
-
if (column.formatValueForExport) {
|
|
383
|
-
value = await column.formatValueForExport(record);
|
|
384
|
-
}
|
|
385
|
-
else if (column.formattedValue) {
|
|
386
|
-
value = evalExpression(column.formattedValue, record);
|
|
387
|
-
}
|
|
388
|
-
else if (column.formatValue) {
|
|
389
|
-
value = await column.formatValue(record);
|
|
390
|
-
}
|
|
391
|
-
else if (column.dataType == "tinyint") {
|
|
392
|
-
value = record[column.name] == 1 ? "Oui" : "Non";
|
|
393
|
-
}
|
|
394
|
-
else {
|
|
395
|
-
value = record[column.name];
|
|
396
|
-
}
|
|
397
|
-
return value === null || typeof value == "undefined" ? "" : value;
|
|
398
|
-
}
|
|
399
|
-
catch (err) {
|
|
400
|
-
return value || "";
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
function evalExpression(expression, data) {
|
|
404
|
-
let varString = "";
|
|
405
|
-
let statement = "";
|
|
406
|
-
try {
|
|
407
|
-
for (let key in data) {
|
|
408
|
-
statement =
|
|
409
|
-
"var " +
|
|
410
|
-
key +
|
|
411
|
-
" = " +
|
|
412
|
-
(typeof data[key] === "string"
|
|
413
|
-
? "'" +
|
|
414
|
-
data[key]
|
|
415
|
-
.replace(/'/g, "\\'")
|
|
416
|
-
.replace(/\n/g, "")
|
|
417
|
-
.replace(/\r/g, "") +
|
|
418
|
-
"'"
|
|
419
|
-
: typeof data[key] === "object"
|
|
420
|
-
? JSON.stringify(data[key])
|
|
421
|
-
: data[key]) +
|
|
422
|
-
";";
|
|
423
|
-
varString += statement;
|
|
424
|
-
}
|
|
425
|
-
const result = eval(varString + expression);
|
|
426
|
-
return result;
|
|
427
|
-
}
|
|
428
|
-
catch (e) {
|
|
429
|
-
console.log("Problème d'évaluation de l'expression", statement);
|
|
430
|
-
console.error(e);
|
|
431
|
-
return "Error";
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
function formatValueForInsertion(value, dbType) {
|
|
435
|
-
if (value !== 0 && !value) {
|
|
436
|
-
return "NULL";
|
|
437
|
-
}
|
|
438
|
-
if (value instanceof Date) {
|
|
439
|
-
return value.toISOString();
|
|
440
|
-
}
|
|
441
|
-
if (typeof value === "string") {
|
|
442
|
-
if (dbType === "POSTGRESQL") {
|
|
443
|
-
return "'" + value.replace(/'/g, "''") + "'";
|
|
444
|
-
}
|
|
445
|
-
else {
|
|
446
|
-
return "'" + value.replace(/'/g, "\\'") + "'";
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
if (typeof value === "object") {
|
|
450
|
-
return "'" + JSON.stringify(value).replace(/'/g, "\\'") + "'";
|
|
451
|
-
}
|
|
452
|
-
if (typeof value === "boolean") {
|
|
453
|
-
return value ? "TRUE" : "FALSE";
|
|
454
|
-
}
|
|
455
|
-
if (typeof value === "number") {
|
|
456
|
-
return value;
|
|
457
|
-
}
|
|
458
|
-
return value;
|
|
459
|
-
}
|
|
460
484
|
app.post("/recordsExport", async function (req, res) {
|
|
461
485
|
const view = req.config.views[req.body.viewIndex];
|
|
462
486
|
const orderBy = req.body.orderBy || view.orderBy;
|
|
@@ -468,7 +492,14 @@ exports.default = (db, config) => {
|
|
|
468
492
|
table: view.tableName,
|
|
469
493
|
page: 1,
|
|
470
494
|
allRecords: true,
|
|
471
|
-
filters: sqlAnd([
|
|
495
|
+
filters: sqlAnd([
|
|
496
|
+
view.filters,
|
|
497
|
+
subview === null || subview === void 0 ? void 0 : subview.filters,
|
|
498
|
+
req.body.filters,
|
|
499
|
+
view.filtersFromUser
|
|
500
|
+
? view.filtersFromUser(req.session.user.userId, req.session.user.role)
|
|
501
|
+
: null,
|
|
502
|
+
]),
|
|
472
503
|
having: subview === null || subview === void 0 ? void 0 : subview.having,
|
|
473
504
|
customQuery: view.customQuery,
|
|
474
505
|
join: view.join,
|
|
@@ -520,6 +551,9 @@ exports.default = (db, config) => {
|
|
|
520
551
|
return res.status(500).send(err);
|
|
521
552
|
}
|
|
522
553
|
const view = req.config.views[fields.viewIndex[0]];
|
|
554
|
+
if (!view || !(0, exports.canWriteView)(view, (0, exports.roleOf)(req))) {
|
|
555
|
+
return res.sendStatus(403);
|
|
556
|
+
}
|
|
523
557
|
if (typeof view.parseImportFile !== "function") {
|
|
524
558
|
return res
|
|
525
559
|
.status(500)
|
|
@@ -542,7 +576,7 @@ exports.default = (db, config) => {
|
|
|
542
576
|
}
|
|
543
577
|
let queryString;
|
|
544
578
|
if (db.dbType === "POSTGRESQL") {
|
|
545
|
-
queryString = `INSERT INTO ?? (${columns.map((x) =>
|
|
579
|
+
queryString = `INSERT INTO ?? (${columns.map((x) => (0, dbManager_1.quoteIdentifier)(x, db.dbType)).join(", ")}) VALUES`;
|
|
546
580
|
for (let record of records) {
|
|
547
581
|
const valuesList = columns
|
|
548
582
|
.map((x) => formatValueForInsertion(record[x], db.dbType))
|
|
@@ -553,13 +587,13 @@ exports.default = (db, config) => {
|
|
|
553
587
|
queryString.slice(0, -1) +
|
|
554
588
|
` ON CONFLICT (${view.importFileUniqueField}) DO UPDATE SET `;
|
|
555
589
|
for (let column of columns) {
|
|
556
|
-
queryString +=
|
|
590
|
+
queryString += `${(0, dbManager_1.quoteIdentifier)(column, db.dbType)} = EXCLUDED.${(0, dbManager_1.quoteIdentifier)(column, db.dbType)},`;
|
|
557
591
|
}
|
|
558
592
|
}
|
|
559
593
|
else {
|
|
560
594
|
queryString =
|
|
561
595
|
"INSERT INTO ?? (" +
|
|
562
|
-
columns.map((x) =>
|
|
596
|
+
columns.map((x) => (0, dbManager_1.quoteIdentifier)(x, db.dbType)).join(", ") +
|
|
563
597
|
") VALUES";
|
|
564
598
|
for (let record of records) {
|
|
565
599
|
queryString +=
|
|
@@ -571,7 +605,7 @@ exports.default = (db, config) => {
|
|
|
571
605
|
}
|
|
572
606
|
queryString = queryString.slice(0, -1) + " ON DUPLICATE KEY UPDATE";
|
|
573
607
|
for (let column of columns) {
|
|
574
|
-
queryString +=
|
|
608
|
+
queryString += ` ${(0, dbManager_1.quoteIdentifier)(column, db.dbType)} = VALUES(${(0, dbManager_1.quoteIdentifier)(column, db.dbType)}),`;
|
|
575
609
|
}
|
|
576
610
|
}
|
|
577
611
|
queryString = queryString.slice(0, -1);
|
|
@@ -603,7 +637,15 @@ exports.default = (db, config) => {
|
|
|
603
637
|
return res.status(500).send(err);
|
|
604
638
|
}
|
|
605
639
|
const view = req.config.views[fields.viewIndex[0]];
|
|
606
|
-
const panel = view
|
|
640
|
+
const panel = view
|
|
641
|
+
? view.recordViewPanels[fields.panelIndex[0]]
|
|
642
|
+
: undefined;
|
|
643
|
+
if (!view ||
|
|
644
|
+
!(0, exports.canWriteView)(view, (0, exports.roleOf)(req)) ||
|
|
645
|
+
!panel ||
|
|
646
|
+
!(0, exports.canWriteView)(panel, (0, exports.roleOf)(req))) {
|
|
647
|
+
return res.sendStatus(403);
|
|
648
|
+
}
|
|
607
649
|
if (typeof panel.parseImportFile !== "function") {
|
|
608
650
|
return res
|
|
609
651
|
.status(500)
|
|
@@ -629,10 +671,10 @@ exports.default = (db, config) => {
|
|
|
629
671
|
}
|
|
630
672
|
}
|
|
631
673
|
}
|
|
632
|
-
let queryString = "INSERT IGNORE INTO ?? (
|
|
633
|
-
panel.createConfig.recordIdKey +
|
|
634
|
-
"
|
|
635
|
-
columns.map((x) =>
|
|
674
|
+
let queryString = "INSERT IGNORE INTO ?? (" +
|
|
675
|
+
(0, dbManager_1.quoteIdentifier)(panel.createConfig.recordIdKey, db.dbType) +
|
|
676
|
+
", " +
|
|
677
|
+
columns.map((x) => (0, dbManager_1.quoteIdentifier)(x, db.dbType)).join(", ") +
|
|
636
678
|
") VALUES";
|
|
637
679
|
for (let record of records) {
|
|
638
680
|
queryString +=
|
|
@@ -646,7 +688,7 @@ exports.default = (db, config) => {
|
|
|
646
688
|
}
|
|
647
689
|
queryString = queryString.slice(0, -1) + " ON DUPLICATE KEY UPDATE";
|
|
648
690
|
for (let column of columns) {
|
|
649
|
-
queryString +=
|
|
691
|
+
queryString += ` ${(0, dbManager_1.quoteIdentifier)(column, db.dbType)} = VALUES(${(0, dbManager_1.quoteIdentifier)(column, db.dbType)}),`;
|
|
650
692
|
}
|
|
651
693
|
const { err, rows: r } = await db.query(queryString.slice(0, -1), [
|
|
652
694
|
panel.tableName,
|
|
@@ -701,7 +743,7 @@ exports.default = (db, config) => {
|
|
|
701
743
|
});
|
|
702
744
|
app.post("/updateRecord", async function (req, res, next) {
|
|
703
745
|
const view = req.config.views[req.body.viewIndex];
|
|
704
|
-
if (!areSafeColumnNames(req.body.data)) {
|
|
746
|
+
if (!(0, exports.areSafeColumnNames)(req.body.data)) {
|
|
705
747
|
return res.status(400).send({ error: "Invalid column name" });
|
|
706
748
|
}
|
|
707
749
|
for (let key in req.body.data) {
|
|
@@ -715,7 +757,7 @@ exports.default = (db, config) => {
|
|
|
715
757
|
return res.sendStatus(200);
|
|
716
758
|
}
|
|
717
759
|
const queryExpression = Object.keys(req.body.data)
|
|
718
|
-
.map((x) => `${x} = ?`)
|
|
760
|
+
.map((x) => `${(0, dbManager_1.quoteIdentifier)(x, db.dbType)} = ?`)
|
|
719
761
|
.join(", ");
|
|
720
762
|
const queryParameters = Object.keys(req.body.data).map((x) => req.body.data[x]);
|
|
721
763
|
const { err } = await db.query(`UPDATE ?? SET ${queryExpression} WHERE ?? = ?`, [
|
|
@@ -853,7 +895,8 @@ exports.default = (db, config) => {
|
|
|
853
895
|
});
|
|
854
896
|
app.post("/deleteRecords", async function (req, res, next) {
|
|
855
897
|
const view = req.config.views[req.body.viewIndex];
|
|
856
|
-
const
|
|
898
|
+
const placeholders = req.body.primaryValues.map(() => "?").join(", ");
|
|
899
|
+
const { err } = await db.query(`DELETE FROM ?? WHERE ?? IN(${placeholders})`, [view.tableName, view.primaryId, ...req.body.primaryValues]);
|
|
857
900
|
if (err) {
|
|
858
901
|
return res.status(500).send(err);
|
|
859
902
|
}
|
|
@@ -889,7 +932,7 @@ exports.default = (db, config) => {
|
|
|
889
932
|
});
|
|
890
933
|
app.post("/duplicateRecord", async function (req, res, next) {
|
|
891
934
|
var _a;
|
|
892
|
-
const allowedTables = writableTables(req);
|
|
935
|
+
const allowedTables = (0, exports.writableTables)(req);
|
|
893
936
|
const requestedTables = [
|
|
894
937
|
req.body.table,
|
|
895
938
|
...(((_a = req.body.duplicateConfig) === null || _a === void 0 ? void 0 : _a.foreignTables) || []).map((t) => t.table),
|
|
@@ -941,9 +984,8 @@ exports.default = (db, config) => {
|
|
|
941
984
|
app.post("/createRecord", async function (req, res, next) {
|
|
942
985
|
var _a, _b, _c;
|
|
943
986
|
const view = req.config.views[req.body.viewIndex];
|
|
944
|
-
const
|
|
945
|
-
|
|
946
|
-
: null;
|
|
987
|
+
const hasPanel = req.body.panelIndex !== undefined && req.body.panelIndex !== null;
|
|
988
|
+
const panel = hasPanel ? view.recordViewPanels[req.body.panelIndex] : null;
|
|
947
989
|
try {
|
|
948
990
|
for (let key in req.body.data) {
|
|
949
991
|
if (req.body.data[key] == "NULL" ||
|
|
@@ -966,10 +1008,12 @@ exports.default = (db, config) => {
|
|
|
966
1008
|
delete createPayload[key];
|
|
967
1009
|
}
|
|
968
1010
|
}
|
|
969
|
-
if (!areSafeColumnNames(createPayload)) {
|
|
1011
|
+
if (!(0, exports.areSafeColumnNames)(createPayload)) {
|
|
970
1012
|
return res.status(400).send({ error: "Invalid column name" });
|
|
971
1013
|
}
|
|
972
|
-
const queryExpression = Object.keys(createPayload)
|
|
1014
|
+
const queryExpression = Object.keys(createPayload)
|
|
1015
|
+
.map((x) => (0, dbManager_1.quoteIdentifier)(x, db.dbType))
|
|
1016
|
+
.join(", ");
|
|
973
1017
|
const queryPlaceholders = Object.keys(createPayload)
|
|
974
1018
|
.map((x) => "?")
|
|
975
1019
|
.join(", ");
|
|
@@ -994,10 +1038,15 @@ exports.default = (db, config) => {
|
|
|
994
1038
|
catch (e) { }
|
|
995
1039
|
}
|
|
996
1040
|
if (panel ? panel.createCallbackFunction : view.createCallbackFunction) {
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1041
|
+
try {
|
|
1042
|
+
await (panel ? panel.createCallbackFunction : view.createCallbackFunction)({
|
|
1043
|
+
id: r.insertId,
|
|
1044
|
+
data: req.body.data,
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
catch (callbackErr) {
|
|
1048
|
+
console.error(callbackErr);
|
|
1049
|
+
}
|
|
1001
1050
|
}
|
|
1002
1051
|
res.send({
|
|
1003
1052
|
id: r.insertId,
|
|
@@ -1011,25 +1060,6 @@ exports.default = (db, config) => {
|
|
|
1011
1060
|
});
|
|
1012
1061
|
}
|
|
1013
1062
|
});
|
|
1014
|
-
function collectAllowedQueries(node, acc = new Set()) {
|
|
1015
|
-
if (!node || typeof node !== "object")
|
|
1016
|
-
return acc;
|
|
1017
|
-
if (Array.isArray(node)) {
|
|
1018
|
-
for (const item of node)
|
|
1019
|
-
collectAllowedQueries(item, acc);
|
|
1020
|
-
return acc;
|
|
1021
|
-
}
|
|
1022
|
-
for (const key of Object.keys(node)) {
|
|
1023
|
-
const value = node[key];
|
|
1024
|
-
if (typeof value === "string" && (key === "query" || /Query$/.test(key))) {
|
|
1025
|
-
acc.add(value.trim());
|
|
1026
|
-
}
|
|
1027
|
-
else if (value && typeof value === "object") {
|
|
1028
|
-
collectAllowedQueries(value, acc);
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
return acc;
|
|
1032
|
-
}
|
|
1033
1063
|
const CONFIG_USES_CUSTOM_QUERY = collectAllowedQueries(config.views).size > 0;
|
|
1034
1064
|
if (!CONFIG_USES_CUSTOM_QUERY) {
|
|
1035
1065
|
app.get("/customQuery", (req, res) => res.sendStatus(404));
|
|
@@ -1042,7 +1072,9 @@ exports.default = (db, config) => {
|
|
|
1042
1072
|
return res.status(403).send({ error: "Query not allowed" });
|
|
1043
1073
|
}
|
|
1044
1074
|
if (!/^\s*\(?\s*(select|with)\b/i.test(template) || /;/.test(template)) {
|
|
1045
|
-
return res
|
|
1075
|
+
return res
|
|
1076
|
+
.status(400)
|
|
1077
|
+
.send({ error: "Only read-only queries are allowed" });
|
|
1046
1078
|
}
|
|
1047
1079
|
const placeholderCount = (template.match(/\?/g) || []).length;
|
|
1048
1080
|
const params = new Array(placeholderCount).fill(req.query.primaryValue);
|
|
@@ -1066,7 +1098,7 @@ exports.default = (db, config) => {
|
|
|
1066
1098
|
function uploadFile(column, fileParamName, req, res) {
|
|
1067
1099
|
if (column.s3Config || column.gcpConfig) {
|
|
1068
1100
|
const form = new multiparty.Form();
|
|
1069
|
-
form.parse(req, function (err, fields, files) {
|
|
1101
|
+
form.parse(req, async function (err, fields, files) {
|
|
1070
1102
|
if (err) {
|
|
1071
1103
|
return res.status(500).send(err);
|
|
1072
1104
|
}
|
|
@@ -1075,8 +1107,14 @@ exports.default = (db, config) => {
|
|
|
1075
1107
|
const extension = originalFilename.match(/(?:\.([^.]+))?$/)[1];
|
|
1076
1108
|
let aspectRatio = null;
|
|
1077
1109
|
if (["png", "jpeg", "jpg", "gif"].includes(extension)) {
|
|
1078
|
-
|
|
1079
|
-
|
|
1110
|
+
try {
|
|
1111
|
+
const dimensions = await (0, imageDimensions_1.getImageDimensions)(files[fileParamName][0].path);
|
|
1112
|
+
if (dimensions) {
|
|
1113
|
+
aspectRatio = dimensions.height / dimensions.width;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
catch (_a) {
|
|
1117
|
+
}
|
|
1080
1118
|
}
|
|
1081
1119
|
const originalFilenameWithoutExtension = originalFilename.replace("." + extension, "");
|
|
1082
1120
|
if (column.gcpConfig) {
|
|
@@ -1227,6 +1265,10 @@ exports.default = (db, config) => {
|
|
|
1227
1265
|
}
|
|
1228
1266
|
}
|
|
1229
1267
|
app.post("/uploadFileFromBetterRichTextInput", async function (req, res, next) {
|
|
1268
|
+
const role = (0, exports.roleOf)(req);
|
|
1269
|
+
if (!req.config.views.some((view) => (0, exports.canWriteView)(view, role))) {
|
|
1270
|
+
return res.sendStatus(403);
|
|
1271
|
+
}
|
|
1230
1272
|
const fileUploadProperties = JSON.parse(req.headers["file-upload-properties"]);
|
|
1231
1273
|
uploadFile(fileUploadProperties, "upload", req, res);
|
|
1232
1274
|
});
|
|
@@ -1269,6 +1311,25 @@ exports.default = (db, config) => {
|
|
|
1269
1311
|
});
|
|
1270
1312
|
return app;
|
|
1271
1313
|
};
|
|
1314
|
+
function collectAllowedQueries(node, acc = new Set()) {
|
|
1315
|
+
if (!node || typeof node !== "object")
|
|
1316
|
+
return acc;
|
|
1317
|
+
if (Array.isArray(node)) {
|
|
1318
|
+
for (const item of node)
|
|
1319
|
+
collectAllowedQueries(item, acc);
|
|
1320
|
+
return acc;
|
|
1321
|
+
}
|
|
1322
|
+
for (const key of Object.keys(node)) {
|
|
1323
|
+
const value = node[key];
|
|
1324
|
+
if (typeof value === "string" && (key === "query" || /Query$/.test(key))) {
|
|
1325
|
+
acc.add(value.trim());
|
|
1326
|
+
}
|
|
1327
|
+
else if (value && typeof value === "object") {
|
|
1328
|
+
collectAllowedQueries(value, acc);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
return acc;
|
|
1332
|
+
}
|
|
1272
1333
|
async function enrichConfig(config, db) {
|
|
1273
1334
|
var _a, _b;
|
|
1274
1335
|
const { rows: info, err } = await db.query("SELECT * FROM information_schema.columns WHERE table_schema = ?", [((_a = config.mysql) === null || _a === void 0 ? void 0 : _a.database) || ((_b = config.postgresql) === null || _b === void 0 ? void 0 : _b.database)]);
|