@opengis/fastify-table 1.3.65 → 1.3.67
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/package.json +1 -1
- package/server/plugins/crud/funcs/validateData.js +19 -2
- package/server/plugins/metric/loggerSystem.js +1 -1
- package/server/plugins/policy/funcs/checkXSS.js +5 -5
- package/server/plugins/table/funcs/getTemplateSync.js +87 -0
- package/server/routes/crud/controllers/deleteCrud.js +4 -2
- package/server/routes/crud/controllers/insert.js +19 -10
- package/server/routes/crud/controllers/table.js +10 -4
- package/server/routes/crud/controllers/update.js +15 -9
- package/utils.js +2 -0
package/package.json
CHANGED
|
@@ -3,8 +3,25 @@ import config from '../../../../config.js';
|
|
|
3
3
|
const emailReg = /(?:[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-zA-Z0-9-]*[a-zA-Z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/g;
|
|
4
4
|
|
|
5
5
|
function checkField(key, val, options, idx, body) {
|
|
6
|
+
const type = options?.type?.toLowerCase?.();
|
|
7
|
+
const isrequired = options?.validators?.includes?.('required');
|
|
8
|
+
|
|
6
9
|
// validators: [required]
|
|
7
|
-
if (
|
|
10
|
+
if (type === 'switcher') {
|
|
11
|
+
// skip nulls at non-required switchers, restrict invalid values
|
|
12
|
+
if (val && typeof val !== 'boolean') {
|
|
13
|
+
return {
|
|
14
|
+
error: 'not a boolean', key, idx,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
// restrict nulls at required switchers
|
|
18
|
+
if (isrequired && typeof val !== 'boolean') {
|
|
19
|
+
return {
|
|
20
|
+
error: 'empty required', key, idx,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
else if (isrequired && !val) {
|
|
8
25
|
if (options?.conditions) {
|
|
9
26
|
const allowed = JSON.parse(options?.conditions?.[2] || null);
|
|
10
27
|
const check = options?.conditions?.[1] === 'in' && Array.isArray(allowed)
|
|
@@ -61,4 +78,4 @@ function checkBody({ body = {}, arr = [], idx }) {
|
|
|
61
78
|
export default function validateData({ body = {}, schema = {} }) {
|
|
62
79
|
const res = checkBody({ body, arr: Object.keys(schema).map(key => ({ ...schema[key], key })) });
|
|
63
80
|
return res;
|
|
64
|
-
}
|
|
81
|
+
}
|
|
@@ -122,7 +122,7 @@ export default async function loggerSystem(req) {
|
|
|
122
122
|
cpu: cpuInfo[0].model,
|
|
123
123
|
ram: formatMemoryUsage(totalMemory),
|
|
124
124
|
db: dbVerion,
|
|
125
|
-
redis: rclient
|
|
125
|
+
redis: rclient?.server_info?.redis_version,
|
|
126
126
|
node: process.version,
|
|
127
127
|
},
|
|
128
128
|
top: topProcess,
|
|
@@ -3,7 +3,7 @@ import xssInjection from '../xssInjection.js';
|
|
|
3
3
|
|
|
4
4
|
function checkXSS({ body, schema = {} }) {
|
|
5
5
|
const data = typeof body === 'string' ? body : JSON.stringify(body);
|
|
6
|
-
const stopWords = xssInjection.filter((el) => data
|
|
6
|
+
const stopWords = xssInjection.filter((el) => data?.toLowerCase?.()?.includes?.(el));
|
|
7
7
|
|
|
8
8
|
// check sql injection
|
|
9
9
|
const stopSpecialSymbols = data.match(/\p{S}OR\p{S}|\p{P}OR\p{P}| OR |\+OR\+/gi);
|
|
@@ -13,7 +13,7 @@ function checkXSS({ body, schema = {} }) {
|
|
|
13
13
|
const skipScreening = config.skipScreening || ['Summernote', 'Tiny', 'Ace', 'Texteditor'];
|
|
14
14
|
Object.keys(body)
|
|
15
15
|
.filter((key) => ['<', '>'].find((el) => body[key]?.includes?.(el))
|
|
16
|
-
|
|
16
|
+
&& !skipScreening.includes(schema?.[key]?.type))
|
|
17
17
|
?.forEach((key) => {
|
|
18
18
|
Object.assign(body, { [key]: body[key].replace(/</g, '<').replace(/>/g, '>') });
|
|
19
19
|
});
|
|
@@ -24,9 +24,9 @@ function checkXSS({ body, schema = {} }) {
|
|
|
24
24
|
|
|
25
25
|
const field = Object.keys(body)
|
|
26
26
|
?.find((key) => body[key]?.toLowerCase
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
&& !disabledCheckFields.includes(key)
|
|
28
|
+
&& (skipScreening.includes(schema?.[key]?.type) ? stopWords.find(el => !['href=', 'src='].includes(el)) : true)
|
|
29
|
+
&& body[key].toLowerCase().includes(stopWords[0]));
|
|
30
30
|
if (field) {
|
|
31
31
|
console.error(stopWords[0], field, body[field]);
|
|
32
32
|
return { error: `rule: ${stopWords[0]} | attr: ${field} | val: ${body[field]}`, body };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
3
|
+
import yaml from 'js-yaml';
|
|
4
|
+
|
|
5
|
+
import getTemplatePath from './getTemplatePath.js';
|
|
6
|
+
import loadTemplate from './loadTemplate.js';
|
|
7
|
+
import mdToHTML from '../../md/funcs/mdToHTML.js';
|
|
8
|
+
|
|
9
|
+
function readFileData(file) {
|
|
10
|
+
const data = readFileSync(file, 'utf-8');
|
|
11
|
+
const ext = file.substring(file.lastIndexOf('.') + 1);
|
|
12
|
+
|
|
13
|
+
if (ext === 'yml') {
|
|
14
|
+
return yaml.load(data);
|
|
15
|
+
}
|
|
16
|
+
if (ext === 'json') {
|
|
17
|
+
return JSON.parse(data);
|
|
18
|
+
}
|
|
19
|
+
if (ext === 'md') {
|
|
20
|
+
const match = data.match(/^---\r?\n([\s\S]*?)---\r?\n([\s\S]*)$/);
|
|
21
|
+
|
|
22
|
+
const parseLinesRecursively = (lines, index = 0, result = {}) => (index >= lines.length
|
|
23
|
+
? result
|
|
24
|
+
: (([key, ...rest]) => (key && rest.length
|
|
25
|
+
? parseLinesRecursively(lines, index + 1, { ...result, [key.trim()]: rest.join(':').trim() })
|
|
26
|
+
: parseLinesRecursively(lines, index + 1, result)))((lines[index] || '').split(':')));
|
|
27
|
+
|
|
28
|
+
const header = parseLinesRecursively(match[1]?.split?.(/\r?\n/) || []);
|
|
29
|
+
const body = match[2]?.replace(/\r\n/g, '')?.startsWith('```html') ? match[2]?.match?.(/```html\r?\n([\s\S]*?)```/)?.[1] : match[2];
|
|
30
|
+
|
|
31
|
+
return { ...header, html: mdToHTML(body) };
|
|
32
|
+
}
|
|
33
|
+
return data;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const isProduction = process.env.NODE_ENV === 'production';
|
|
37
|
+
|
|
38
|
+
function getTemplateData(template) {
|
|
39
|
+
// dir template: dashboard, card
|
|
40
|
+
if (template[0][3]) {
|
|
41
|
+
const files = readdirSync(template[0][1]);
|
|
42
|
+
const data = files.map(async el => readFileData(path.join(template[0][1], el)));
|
|
43
|
+
return files.map((el, i) => [el, data[i]]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// one file template: table, form
|
|
47
|
+
if (template.length === 1) {
|
|
48
|
+
const data = readFileData(template[0][1]);
|
|
49
|
+
return data;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// multi file template: select, etc
|
|
53
|
+
if (template.length > 1) {
|
|
54
|
+
const data = template.map(el => readFileData(el[1]));
|
|
55
|
+
if (Array.isArray(data[0])) {
|
|
56
|
+
return data[0];
|
|
57
|
+
}
|
|
58
|
+
const result = {};
|
|
59
|
+
template.forEach((el, i) => {
|
|
60
|
+
Object.assign(result, typeof data[i] === 'object' ? data[i] : { [el[2]]: data[i] });
|
|
61
|
+
});
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
export default function getTemplateSync(type, name) {
|
|
67
|
+
if (!type) return null;
|
|
68
|
+
if (!name) return null;
|
|
69
|
+
|
|
70
|
+
const key = `${type}:${name}`;
|
|
71
|
+
if (name === 'cache' && !isProduction) return loadTemplate; // all cache debug
|
|
72
|
+
if (loadTemplate[key] && isProduction && false) return loadTemplate[key]; // from cache
|
|
73
|
+
|
|
74
|
+
// type one or multi
|
|
75
|
+
const templateList = Array.isArray(type)
|
|
76
|
+
? type.map(el => getTemplatePath(el)).filter(list => list?.filter(el => el[0] === name).length)[0] || []
|
|
77
|
+
: getTemplatePath(type);
|
|
78
|
+
|
|
79
|
+
// find template
|
|
80
|
+
const template = templateList?.filter(el => el[0] === name);
|
|
81
|
+
if (name === 'list' && !isProduction) return templateList; // all template debug
|
|
82
|
+
|
|
83
|
+
if (!template.length) return null; // not found
|
|
84
|
+
|
|
85
|
+
loadTemplate[key] = getTemplateData(template);
|
|
86
|
+
return loadTemplate[key];
|
|
87
|
+
}
|
|
@@ -3,7 +3,9 @@ import {
|
|
|
3
3
|
} from '../../../../utils.js';
|
|
4
4
|
|
|
5
5
|
export default async function deleteCrud(req, reply) {
|
|
6
|
-
const {
|
|
6
|
+
const {
|
|
7
|
+
pg = pgClients.client, user, params = {}, headers = {},
|
|
8
|
+
} = req || {};
|
|
7
9
|
|
|
8
10
|
const hookData = await applyHook('preDelete', {
|
|
9
11
|
pg, table: params?.table, id: params?.id, user,
|
|
@@ -36,7 +38,7 @@ export default async function deleteCrud(req, reply) {
|
|
|
36
38
|
}).catch(err => {
|
|
37
39
|
if (err.message?.includes?.('foreign key' || 'unique')) {
|
|
38
40
|
const constraint = err.message.match(/constraint "([^"]+)"/g);
|
|
39
|
-
return reply.status(400).send(
|
|
41
|
+
return reply.status(400).send(`Видалення заборонено для збереження цілісності БД: ${constraint}`);
|
|
40
42
|
}
|
|
41
43
|
if (config.trace) console.error(err.toString());
|
|
42
44
|
return err.toString();
|
|
@@ -2,13 +2,17 @@ import {
|
|
|
2
2
|
applyHook, getAccess, getTemplate, checkXSS, dataInsert, getToken, config, pgClients, logger, validateData,
|
|
3
3
|
} from '../../../../utils.js';
|
|
4
4
|
|
|
5
|
-
export default async function insert(req) {
|
|
5
|
+
export default async function insert(req, reply) {
|
|
6
6
|
const {
|
|
7
7
|
pg = pgClients.client, user = {}, params = {}, body = {}, headers = {},
|
|
8
8
|
} = req || {};
|
|
9
|
-
if (!user)
|
|
9
|
+
if (!user) {
|
|
10
|
+
return reply.status(403).send('access restricted');
|
|
11
|
+
}
|
|
10
12
|
|
|
11
|
-
const hookData = await applyHook('preInsert', {
|
|
13
|
+
const hookData = await applyHook('preInsert', {
|
|
14
|
+
pg, table: params?.table, user, body,
|
|
15
|
+
});
|
|
12
16
|
|
|
13
17
|
if (hookData?.message && hookData?.status) {
|
|
14
18
|
return { message: hookData?.message, status: hookData?.status };
|
|
@@ -23,12 +27,12 @@ export default async function insert(req) {
|
|
|
23
27
|
|
|
24
28
|
const { actions = [] } = await getAccess({ table: add, user }, pg) || {};
|
|
25
29
|
|
|
26
|
-
if (!actions.includes('add') && !config
|
|
27
|
-
return
|
|
30
|
+
if (!actions.includes('add') && !config.local && !config.debug && !tokenData) {
|
|
31
|
+
return reply.status(403).send('access restricted');
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
if (!add) {
|
|
31
|
-
return
|
|
35
|
+
return reply.status(400).send('table is required');
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
const loadTemplate = await getTemplate('table', add);
|
|
@@ -43,8 +47,10 @@ export default async function insert(req) {
|
|
|
43
47
|
const xssCheck = checkXSS({ body, schema });
|
|
44
48
|
|
|
45
49
|
if (xssCheck.error && formData?.xssCheck !== false) {
|
|
46
|
-
logger.file('injection/xss', {
|
|
47
|
-
|
|
50
|
+
logger.file('injection/xss', {
|
|
51
|
+
table, form: form || loadTemplate?.form, body, uid: user?.uid, msg: xssCheck.error,
|
|
52
|
+
});
|
|
53
|
+
return reply.status(409).send('Дані містять заборонені символи. Приберіть їх та спробуйте ще раз');
|
|
48
54
|
}
|
|
49
55
|
|
|
50
56
|
const fieldCheck = validateData({ body, schema });
|
|
@@ -56,7 +62,7 @@ export default async function insert(req) {
|
|
|
56
62
|
uid: user?.uid,
|
|
57
63
|
...fieldCheck,
|
|
58
64
|
});
|
|
59
|
-
return
|
|
65
|
+
return reply.status(409).send('Дані не пройшли валідацію. Приберіть некоректні дані та спробуйте ще раз');
|
|
60
66
|
}
|
|
61
67
|
|
|
62
68
|
if (![add, table].includes('admin.users')) {
|
|
@@ -76,7 +82,10 @@ export default async function insert(req) {
|
|
|
76
82
|
tokenData,
|
|
77
83
|
referer,
|
|
78
84
|
});
|
|
79
|
-
|
|
85
|
+
|
|
86
|
+
if (!res) {
|
|
87
|
+
return reply.status(400).send('nothing added');
|
|
88
|
+
}
|
|
80
89
|
|
|
81
90
|
// admin.custom_column
|
|
82
91
|
await applyHook('afterInsert', {
|
|
@@ -33,17 +33,20 @@ export default async function tableAPI(req) {
|
|
|
33
33
|
|
|
34
34
|
if (tokenData && !id) return { message: {} };
|
|
35
35
|
if (!tableName && !id) {
|
|
36
|
-
return
|
|
36
|
+
return reply.status(400).send('not enough params');
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
const { actions = [], query: accessQuery } = await getAccess({ table: templateName, id, user }, pg) || {};
|
|
40
40
|
|
|
41
41
|
if (!actions.includes('edit') && !config?.local && !tokenData) {
|
|
42
|
-
return
|
|
42
|
+
return reply.status(403).send('access restricted');
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
const { pk, columns: dbColumns = [] } = await getMeta({ pg, table: tableName });
|
|
46
|
-
|
|
46
|
+
|
|
47
|
+
if (!pk) {
|
|
48
|
+
return reply.status(404).send(`table not found: ${table}`);
|
|
49
|
+
}
|
|
47
50
|
|
|
48
51
|
// const cols = columns.map((el) => el.name || el).join(',');
|
|
49
52
|
const formName = hookData?.form || tokenData?.form || form;
|
|
@@ -72,7 +75,10 @@ export default async function tableAPI(req) {
|
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
const data = await pg.query(q.replace(/{{uid}}/, user?.uid), [id]).then(el => el.rows[0]);
|
|
75
|
-
|
|
78
|
+
|
|
79
|
+
if (!data) {
|
|
80
|
+
return reply.status(404).send(`object not found: ${id}`);
|
|
81
|
+
}
|
|
76
82
|
|
|
77
83
|
Object.keys(schema).filter(key => schema[key]?.type === 'DataTable').forEach(key => {
|
|
78
84
|
if (data[key] && !Array.isArray(data[key])) { data[key] = null; }
|
|
@@ -4,12 +4,15 @@ import {
|
|
|
4
4
|
import config from '../../../../config.js';
|
|
5
5
|
import insert from './insert.js';
|
|
6
6
|
|
|
7
|
-
export default async function update(req) {
|
|
7
|
+
export default async function update(req, reply) {
|
|
8
8
|
const {
|
|
9
9
|
pg = pgClients.client, user, params = {}, body = {}, headers = {}, unittest,
|
|
10
10
|
} = req;
|
|
11
11
|
|
|
12
|
-
if (!user)
|
|
12
|
+
if (!user) {
|
|
13
|
+
return reply.status(403).send('access restricted');
|
|
14
|
+
}
|
|
15
|
+
|
|
13
16
|
const hookData = await applyHook('preUpdate', {
|
|
14
17
|
pg, table: params?.table, id: params?.id, user,
|
|
15
18
|
});
|
|
@@ -27,19 +30,20 @@ export default async function update(req) {
|
|
|
27
30
|
|
|
28
31
|
const { actions = [] } = await getAccess({ table: edit, id, user }, pg) || {};
|
|
29
32
|
|
|
30
|
-
if (!actions.includes('edit') && !config
|
|
31
|
-
return
|
|
33
|
+
if (!actions.includes('edit') && !config.local && !config.debug && !tokenData) {
|
|
34
|
+
return reply.status(403).send('access restricted');
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
if (!edit) {
|
|
35
|
-
return
|
|
38
|
+
return reply.status(400).send('table is required');
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
if (!id && tokenData?.table) {
|
|
39
42
|
return insert(req);
|
|
40
43
|
}
|
|
44
|
+
|
|
41
45
|
if (!id) {
|
|
42
|
-
return
|
|
46
|
+
return reply.status(400).send('id is required');
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
const loadTemplate = await getTemplate('table', edit);
|
|
@@ -59,7 +63,7 @@ export default async function update(req) {
|
|
|
59
63
|
|
|
60
64
|
if (xssCheck.error && formData?.xssCheck !== false) {
|
|
61
65
|
logger.file('injection/xss', { msg: xssCheck.error, table }, req);
|
|
62
|
-
return
|
|
66
|
+
return reply.status(409).send('Дані містять заборонені символи. Приберіть їх та спробуйте ще раз');
|
|
63
67
|
}
|
|
64
68
|
|
|
65
69
|
const fieldCheck = validateData({ body, schema });
|
|
@@ -71,7 +75,7 @@ export default async function update(req) {
|
|
|
71
75
|
uid: user?.uid,
|
|
72
76
|
...fieldCheck,
|
|
73
77
|
});
|
|
74
|
-
return
|
|
78
|
+
return reply.status(409).send('Дані не пройшли валідацію. Приберіть некоректні дані та спробуйте ще раз');
|
|
75
79
|
}
|
|
76
80
|
|
|
77
81
|
const res = await dataUpdate({
|
|
@@ -100,7 +104,9 @@ export default async function update(req) {
|
|
|
100
104
|
// insert new extra data
|
|
101
105
|
if (Array.isArray(body[key]) && body[key]?.length) {
|
|
102
106
|
const extraRows = await Promise.all(body[key]?.map?.(async (row) => {
|
|
103
|
-
const extraRes = await dataInsert({
|
|
107
|
+
const extraRes = await dataInsert({
|
|
108
|
+
pg, table: schema[key].table, data: { ...row, [schema[key].parent_id]: objId }, uid, tokenData, referer,
|
|
109
|
+
});
|
|
104
110
|
return extraRes?.rows?.[0];
|
|
105
111
|
}));
|
|
106
112
|
Object.assign(res.extra, { [key]: extraRows.filter((el) => el) });
|
package/utils.js
CHANGED
|
@@ -19,6 +19,7 @@ import redisClients from './server/plugins/redis/funcs/redisClients.js';
|
|
|
19
19
|
|
|
20
20
|
// template
|
|
21
21
|
import getTemplate from './server/plugins/table/funcs/getTemplate.js';
|
|
22
|
+
import getTemplateSync from './server/plugins/table/funcs/getTemplateSync.js';
|
|
22
23
|
import getTemplates from './server/plugins/table/funcs/getTemplates.js';
|
|
23
24
|
import getTemplatePath from './server/plugins/table/funcs/getTemplatePath.js';
|
|
24
25
|
import addTemplateDir from './server/plugins/table/funcs/addTemplateDir.js';
|
|
@@ -104,6 +105,7 @@ export {
|
|
|
104
105
|
|
|
105
106
|
// template
|
|
106
107
|
getTemplate,
|
|
108
|
+
getTemplateSync,
|
|
107
109
|
getTemplates,
|
|
108
110
|
getTemplatePath,
|
|
109
111
|
addTemplateDir,
|