@opengis/fastify-table 1.0.8 → 1.0.10
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/{changelog.md → Changelog.md} +21 -0
- package/config.js +10 -2
- package/crud/controllers/insert.js +20 -1
- package/crud/controllers/update.js +22 -3
- package/crud/controllers/utils/checkXSS.js +45 -0
- package/crud/controllers/utils/xssInjection.js +72 -0
- package/crud/funcs/dataInsert.js +1 -1
- package/crud/funcs/getIdByToken.js +29 -0
- package/crud/funcs/setTokenById.js +55 -0
- package/index.js +10 -0
- package/package.json +1 -1
- package/policy/funcs/checkPolicy.js +74 -0
- package/policy/funcs/sqlInjection.js +33 -0
- package/policy/index.js +14 -0
- package/redis/index.js +1 -1
- package/server/templates/form/test.dataset.form.json +412 -0
- package/table/controllers/data.js +2 -0
- package/table/controllers/filter.js +9 -1
- package/table/funcs/metaFormat/index.js +28 -0
- package/table/index.js +3 -0
- package/test/api/crud.test.js +7 -5
- package/test/api/crud.xss.test.js +70 -0
- package/test/funcs/crud.test.js +30 -4
- package/test/templates/cls/itree.recommend.json +26 -0
- package/test/templates/cls/itree.type_plant.json +65 -0
- package/test/templates/select/contact_id.sql +1 -0
- package/test/templates/table/green_space.table.json +3 -3
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# fastify-table
|
|
2
2
|
|
|
3
|
+
## 1.0.9 - 29.04.2024
|
|
4
|
+
|
|
5
|
+
- crud token support
|
|
6
|
+
- security - xss restriction
|
|
7
|
+
|
|
8
|
+
## 1.0.8 - 29.04.2024
|
|
9
|
+
|
|
10
|
+
- filter fix
|
|
11
|
+
|
|
12
|
+
## 1.0.7 - 26.04.2024
|
|
13
|
+
|
|
14
|
+
- code optimization
|
|
15
|
+
|
|
16
|
+
## 1.0.6 - 25.04.2024
|
|
17
|
+
|
|
18
|
+
- code optimization
|
|
19
|
+
|
|
20
|
+
## 1.0.5 - 24.04.2024
|
|
21
|
+
|
|
22
|
+
- code optimization
|
|
23
|
+
|
|
3
24
|
## 1.0.4 - 20.04.2024
|
|
4
25
|
|
|
5
26
|
- data api - order
|
package/config.js
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
|
|
3
|
+
const config = fs.existsSync('config.json')
|
|
4
|
+
? JSON.parse(fs.readFileSync('config.json'))
|
|
5
|
+
: {};
|
|
6
|
+
|
|
7
|
+
Object.assign(config, {
|
|
8
|
+
allTemplates: config?.allTemplates || {},
|
|
9
|
+
});
|
|
10
|
+
|
|
3
11
|
export default config;
|
|
@@ -1,9 +1,28 @@
|
|
|
1
1
|
import dataInsert from '../funcs/dataInsert.js';
|
|
2
|
+
import getIdByToken from '../funcs/getIdByToken.js';
|
|
3
|
+
import checkXSS from './utils/checkXSS.js';
|
|
4
|
+
import getTemplate from '../../table/controllers/utils/getTemplate.js';
|
|
2
5
|
|
|
3
6
|
export default async function insert(req) {
|
|
4
7
|
const { table } = req.params || {};
|
|
5
8
|
if (!table) return { status: 404, message: 'table is required' };
|
|
6
9
|
|
|
7
|
-
const
|
|
10
|
+
const { funcs, session, params } = req;
|
|
11
|
+
const tokenDataString = await getIdByToken({
|
|
12
|
+
funcs, session, token: params.table, mode: 'a', json: 0,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const { form, add } = JSON.parse(tokenDataString || '{}');
|
|
16
|
+
|
|
17
|
+
const formData = form ? await getTemplate('form', form) : {};
|
|
18
|
+
|
|
19
|
+
const xssCheck = checkXSS({ body: req.body, schema: formData?.schema });
|
|
20
|
+
|
|
21
|
+
if (xssCheck.error && formData?.xssCheck !== false) {
|
|
22
|
+
req.log.warn({ name: 'injection/xss', msg: xssCheck.error, table }, req);
|
|
23
|
+
return { message: 'Дані містять заборонені символи. Приберіть їх та спробуйте ще раз', status: 409 };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const res = await dataInsert({ table: add || table, data: req.body });
|
|
8
27
|
return res;
|
|
9
28
|
}
|
|
@@ -1,10 +1,29 @@
|
|
|
1
1
|
import dataUpdate from '../funcs/dataUpdate.js';
|
|
2
|
+
import getIdByToken from '../funcs/getIdByToken.js';
|
|
3
|
+
import checkXSS from './utils/checkXSS.js';
|
|
4
|
+
import getTemplate from '../../table/controllers/utils/getTemplate.js';
|
|
2
5
|
|
|
3
6
|
export default async function update(req) {
|
|
4
7
|
const { table, id } = req.params || {};
|
|
5
|
-
if (!table) return {
|
|
6
|
-
if (!id) return {
|
|
8
|
+
if (!req.params?.table) return { message: 'table is required', status: 404 };
|
|
9
|
+
if (!id) return { message: 'id is required', status: 404 };
|
|
7
10
|
|
|
8
|
-
const
|
|
11
|
+
const { funcs, session, params } = req;
|
|
12
|
+
const tokenDataString = await getIdByToken({
|
|
13
|
+
funcs, session, token: params.table, mode: 'w', json: 0,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const tokenData = JSON.parse(tokenDataString || '{}');
|
|
17
|
+
|
|
18
|
+
const formData = tokenData?.form ? await getTemplate('form', tokenData.form) : {};
|
|
19
|
+
|
|
20
|
+
const xssCheck = checkXSS({ body: req.body, schema: formData?.schema });
|
|
21
|
+
|
|
22
|
+
if (xssCheck.error && formData?.xssCheck !== false) {
|
|
23
|
+
req.log.warn({ name: 'injection/xss', msg: xssCheck.error, table }, req);
|
|
24
|
+
return { message: 'Дані містять заборонені символи. Приберіть їх та спробуйте ще раз', status: 409 };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const res = await dataUpdate({ table: tokenData?.table || table, id: tokenData?.id || id, data: req.body });
|
|
9
28
|
return res;
|
|
10
29
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/* import sqlInjection from '../../../policy/funcs/sqlInjection.js'; */
|
|
2
|
+
import xssInjection from './xssInjection.js';
|
|
3
|
+
|
|
4
|
+
/* const checkList = xssInjection.concat(sqlInjection); */
|
|
5
|
+
|
|
6
|
+
// RTE - rich text editor
|
|
7
|
+
|
|
8
|
+
function checkXSS({ body, schema = {} }) {
|
|
9
|
+
const data = typeof body === 'string' ? body : JSON.stringify(body);
|
|
10
|
+
const stopWords = xssInjection.filter((el) => data.toLowerCase().includes(el));
|
|
11
|
+
|
|
12
|
+
// check sql injection
|
|
13
|
+
const stopSpecialSymbols = data.match(/\p{S}OR\p{S}|\p{P}OR\p{P}| OR |\+OR\+/gi);
|
|
14
|
+
if (stopSpecialSymbols?.length) stopSpecialSymbols?.forEach((el) => stopWords.push(el));
|
|
15
|
+
|
|
16
|
+
// escape arrows on non-RTE
|
|
17
|
+
Object.keys(body)
|
|
18
|
+
.filter((key) => ['<', '>'].find((el) => body[key].includes(el))
|
|
19
|
+
&& !['Summernote', 'Tiny', 'Ace'].includes(schema[key]?.type))
|
|
20
|
+
?.forEach((key) => {
|
|
21
|
+
Object.assign(body, { [key]: body[key].replace(/</g, '<').replace(/>/g, '>') });
|
|
22
|
+
});
|
|
23
|
+
// try { } catch (err) { return { error: err.toString() }; }
|
|
24
|
+
|
|
25
|
+
if (!stopWords.length) return { body };
|
|
26
|
+
|
|
27
|
+
const disabledCheckFields = Object.keys(schema)?.filter((el) => schema[el]?.xssCheck === false); // exclude specific columns
|
|
28
|
+
|
|
29
|
+
// check RTE
|
|
30
|
+
/* const richTextFields = Object.keys(schema).filter((el) => ['Summernote', 'Tiny', 'Ace'].includes(schema[el]?.type));
|
|
31
|
+
richTextFields.filter((key) => !checkList.find((el) => body[key].includes(el)))?.forEach((key) => {
|
|
32
|
+
disabledCheckFields.push(key);
|
|
33
|
+
}); */
|
|
34
|
+
|
|
35
|
+
const field = Object.keys(body)
|
|
36
|
+
?.find((key) => body[key]
|
|
37
|
+
&& !disabledCheckFields.includes(key)
|
|
38
|
+
&& body[key].toLowerCase().includes(stopWords[0]));
|
|
39
|
+
if (field) {
|
|
40
|
+
return { error: `rule: ${stopWords[0]} | attr: ${field} | val: ${body[field]}`, body };
|
|
41
|
+
}
|
|
42
|
+
return { body };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default checkXSS;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const xssInjection = [
|
|
2
|
+
'onkeypress=',
|
|
3
|
+
'onkeyup=',
|
|
4
|
+
'ondblclick=',
|
|
5
|
+
'onerror=',
|
|
6
|
+
'onmouseover=',
|
|
7
|
+
'<meta',
|
|
8
|
+
'<script',
|
|
9
|
+
'vascript:',
|
|
10
|
+
'onkeydown=',
|
|
11
|
+
'onmousedown=',
|
|
12
|
+
'onmouseenter=',
|
|
13
|
+
'onmouseleave=',
|
|
14
|
+
'onmousemove=',
|
|
15
|
+
'onmouseout=',
|
|
16
|
+
'onmouseup=',
|
|
17
|
+
'onmousewheel=',
|
|
18
|
+
'onpaste=',
|
|
19
|
+
'onscroll=',
|
|
20
|
+
'onwheel=',
|
|
21
|
+
'javascript:',
|
|
22
|
+
'\\x',
|
|
23
|
+
'eval(',
|
|
24
|
+
'onmouseover=',
|
|
25
|
+
'action=',
|
|
26
|
+
'xlink:',
|
|
27
|
+
'allowscriptaccess',
|
|
28
|
+
'href=',
|
|
29
|
+
'behavior:',
|
|
30
|
+
'onreadystatechange=',
|
|
31
|
+
'onstart=',
|
|
32
|
+
'offline=',
|
|
33
|
+
'onabort=',
|
|
34
|
+
'onafterprint=',
|
|
35
|
+
'onbeforeonload=',
|
|
36
|
+
'onbeforeprint=',
|
|
37
|
+
'onblur=',
|
|
38
|
+
'oncanplay=',
|
|
39
|
+
'oncanplaythrough=',
|
|
40
|
+
'onchange=',
|
|
41
|
+
'onclick=',
|
|
42
|
+
'oncontextmenu=',
|
|
43
|
+
'ondblclick=',
|
|
44
|
+
'ondrag=',
|
|
45
|
+
'ondragend=',
|
|
46
|
+
'ondragenter=',
|
|
47
|
+
'ondragleave=',
|
|
48
|
+
'ondragover=',
|
|
49
|
+
'ondragstart=',
|
|
50
|
+
'ondrop=',
|
|
51
|
+
'ondurationchange=',
|
|
52
|
+
'onemptied=',
|
|
53
|
+
'onended=',
|
|
54
|
+
'onerror=',
|
|
55
|
+
'onfocus=',
|
|
56
|
+
'onformchange=',
|
|
57
|
+
'onforminput=',
|
|
58
|
+
'onhaschange=',
|
|
59
|
+
'oninput=',
|
|
60
|
+
'oninvalid=',
|
|
61
|
+
'onkeydown=',
|
|
62
|
+
'onkeypress=',
|
|
63
|
+
'onkeyup=',
|
|
64
|
+
'onload=',
|
|
65
|
+
'onloadeddata=',
|
|
66
|
+
'onloadedmetadata=',
|
|
67
|
+
'onloadstart=',
|
|
68
|
+
'alert(',
|
|
69
|
+
'script:',
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
export default xssInjection;
|
package/crud/funcs/dataInsert.js
CHANGED
|
@@ -18,7 +18,7 @@ export default async function dataInsert({ table, data }) {
|
|
|
18
18
|
values (${filterData?.map((key, i) => `$${i + 1}`).join(',')})
|
|
19
19
|
|
|
20
20
|
returning *`;
|
|
21
|
-
|
|
21
|
+
await pg.query('DROP TRIGGER if exists dataset_before_update_insert ON gis.dataset');
|
|
22
22
|
const res = await pg.one(insertQuery, [...filterData.map((el) => (typeof el[1] === 'object' ? JSON.stringify(el[1]) : el[1]))]) || {};
|
|
23
23
|
return res;
|
|
24
24
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import getRedis from '../../redis/funcs/getRedis.js';
|
|
2
|
+
|
|
3
|
+
function sprintf(str, ...args) {
|
|
4
|
+
return str.replace(/%s/g, () => args.shift());
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const keys = {
|
|
8
|
+
r: '%s:token:view:%s',
|
|
9
|
+
a: '%s:token:add:%s',
|
|
10
|
+
w: '%s:token:edit:%s',
|
|
11
|
+
e: '%s:token:exec:%s',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
async function getIdByToken({
|
|
15
|
+
funcs, session, token, mode = 'r', json,
|
|
16
|
+
}) {
|
|
17
|
+
if (mode === 'r') return token;
|
|
18
|
+
|
|
19
|
+
const { config } = funcs;
|
|
20
|
+
const { uid } = session?.passport?.user || (config.local ? { uid: '1' } : {});
|
|
21
|
+
|
|
22
|
+
const rclient2 = getRedis({ db: 2, funcs });
|
|
23
|
+
|
|
24
|
+
const key = sprintf(keys[mode], config?.pg?.database, uid);
|
|
25
|
+
const id = await rclient2.hget(key, token);
|
|
26
|
+
return json && id[0] === '{' ? JSON.parse(id) : id;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default getIdByToken;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'crypto';
|
|
2
|
+
|
|
3
|
+
import getRedis from '../../redis/funcs/getRedis.js';
|
|
4
|
+
|
|
5
|
+
const generateCodes = (ids, userToken) => {
|
|
6
|
+
const token = userToken || randomUUID();
|
|
7
|
+
const notNullIds = ids.filter((el) => el);
|
|
8
|
+
const obj = {};
|
|
9
|
+
const codes = notNullIds.reduce((acc, id) => {
|
|
10
|
+
const newToken = createHash('sha1').update(token + id).digest('base64url').replace(/-/g, '');
|
|
11
|
+
acc[newToken] = id; obj[id] = newToken;
|
|
12
|
+
return acc;
|
|
13
|
+
}, {});
|
|
14
|
+
return { codes, obj };
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function setTokenById({
|
|
18
|
+
funcs, ids: idsOrigin, mode = 'r', session, referer, array,
|
|
19
|
+
}) {
|
|
20
|
+
const { config } = funcs;
|
|
21
|
+
const { uid } = session?.passport?.user || (config.local ? { uid: '1' } : {});
|
|
22
|
+
|
|
23
|
+
const rclient2 = getRedis({ db: 2, funcs });
|
|
24
|
+
const rclient5 = getRedis({ db: 5, funcs });
|
|
25
|
+
|
|
26
|
+
if (!uid) return { user: 'empty' };
|
|
27
|
+
if (!Object.keys(idsOrigin).length) return { ids: 'empty' };
|
|
28
|
+
|
|
29
|
+
const ids = idsOrigin.map((el) => (typeof el === 'object' ? JSON.stringify(el) : el));
|
|
30
|
+
// update/delete
|
|
31
|
+
|
|
32
|
+
if (mode === 'r') return null;
|
|
33
|
+
|
|
34
|
+
// TODO generate salt
|
|
35
|
+
const { codes, obj } = generateCodes(ids, uid);
|
|
36
|
+
|
|
37
|
+
if (!Object.keys(codes).length) return { ids: 'empty' };
|
|
38
|
+
|
|
39
|
+
rclient2.hmset(`${config.pg.database}:token:${{
|
|
40
|
+
e: 'exec', r: 'view', w: 'edit', a: 'add',
|
|
41
|
+
}[mode]}:${uid}`, codes);
|
|
42
|
+
|
|
43
|
+
// log token for debug. add extra data - uid, mode, date
|
|
44
|
+
const dt = new Date().toISOString();
|
|
45
|
+
const codesLog = Object.keys(codes).reduce((acc, key) => {
|
|
46
|
+
acc[key] = `{"referer": "${referer}" ,"uid":"${uid}","mode":"${mode}","date":"${dt}",${codes[key].substr(1)}`;
|
|
47
|
+
return acc;
|
|
48
|
+
}, {});
|
|
49
|
+
rclient5.hmset(`${config.pg.database}:token:edit`, codesLog); // 'EX', 64800
|
|
50
|
+
|
|
51
|
+
// TODO дополнительно писать в hset token -> uid
|
|
52
|
+
return array ? Object.values(obj) : obj;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export default setTokenById;
|
package/index.js
CHANGED
|
@@ -7,12 +7,22 @@ import redisPlugin from './redis/index.js';
|
|
|
7
7
|
import pgPlugin from './pg/index.js';
|
|
8
8
|
import tablePlugin from './table/index.js';
|
|
9
9
|
import crudPlugin from './crud/index.js';
|
|
10
|
+
import policyPlugin from './policy/index.js';
|
|
10
11
|
|
|
11
12
|
async function plugin(fastify, opt) {
|
|
12
13
|
// console.log(opt);
|
|
13
14
|
config.pg = opt.pg;
|
|
14
15
|
config.redis = opt.redis;
|
|
15
16
|
|
|
17
|
+
// independent npm start / unit test
|
|
18
|
+
if (!fastify.config) {
|
|
19
|
+
fastify.decorate('config', config);
|
|
20
|
+
}
|
|
21
|
+
if (!fastify.funcs) {
|
|
22
|
+
fastify.decorateRequest('funcs', fastify);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
policyPlugin(fastify);
|
|
16
26
|
redisPlugin(fastify);
|
|
17
27
|
await pgPlugin(fastify, opt);
|
|
18
28
|
tablePlugin(fastify, opt);
|
package/package.json
CHANGED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import block from './sqlInjection.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Middleware func
|
|
5
|
+
*
|
|
6
|
+
* @type function
|
|
7
|
+
* @alias checkPolicy
|
|
8
|
+
* @summary Функція дозволяє налаштувати доступ до сайту або API для адмін. та публічної частини веб-ресурсу
|
|
9
|
+
* @param {String} path - назва апі
|
|
10
|
+
* @returns {object|null} Returns object
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export default function checkPolicy(req) {
|
|
14
|
+
const {
|
|
15
|
+
originalUrl: path, hostname, query, params, headers: hs, log, sid = 35, funcs = {},
|
|
16
|
+
} = req;
|
|
17
|
+
const user = req.user || req.session?.passport?.user;
|
|
18
|
+
|
|
19
|
+
const { config } = funcs;
|
|
20
|
+
const isUser = config.debug || !!user;
|
|
21
|
+
|
|
22
|
+
const isServer = process.argv[2];
|
|
23
|
+
const { policy = [] } = req.routeOptions?.config || {};
|
|
24
|
+
|
|
25
|
+
/*= == 1.File injection === */
|
|
26
|
+
if (JSON.stringify(params || {})?.includes('../') || JSON.stringify(query || {})?.includes('../') || path?.includes('../')) {
|
|
27
|
+
log.warn({
|
|
28
|
+
name: 'injection/file', params, query, message: 'access restricted: 1',
|
|
29
|
+
});
|
|
30
|
+
return { message: 'access restricted: 1', status: 403 };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/*= == 1.1 File === */
|
|
34
|
+
const allowExtPublic = ['.png', '.jpg', '.svg'];
|
|
35
|
+
const ext = path.toLowerCase().substr(-4);
|
|
36
|
+
if (path.includes('files/') && allowExtPublic.includes(ext)) return null;
|
|
37
|
+
|
|
38
|
+
/*= == 2.SQL Injection policy: no-sql === */
|
|
39
|
+
if (!policy.includes('no-sql')) {
|
|
40
|
+
const stopWords = block.filter((el) => path.includes(el));
|
|
41
|
+
if (stopWords?.length) {
|
|
42
|
+
log.warn({ name: 'injection/sql', stopWords, message: 'access restricted: 2' });
|
|
43
|
+
return { message: 'access restricted: 2', status: 403 };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/* Check is Not API */
|
|
47
|
+
const isApi = ['/files/', '/api/format/', '/api-user/', '/logger', '/file/'].filter((el) => path.includes(el)).length;
|
|
48
|
+
if (!isApi) return null;
|
|
49
|
+
|
|
50
|
+
/*= == 3. policy: referer === */
|
|
51
|
+
if (!hs?.referer?.includes?.(hostname) && policy.includes('referer') && !config.local && !config.debug) {
|
|
52
|
+
log.warn({ name: 'referer', message: 'access restricted: 3' });
|
|
53
|
+
return { message: 'access restricted: 3', status: 403 };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/*= == policy: public === */
|
|
57
|
+
if (policy.includes('public')) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/*= == 4. policy: site auth === */
|
|
62
|
+
if (!policy.includes('site') && sid === 1 && isUser && !config.local && !config.debug) {
|
|
63
|
+
log.warn({ name: 'site', message: 'access restricted: 4' });
|
|
64
|
+
return { message: 'access restricted: 4', status: 403 };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/*= == 5. base policy: block api === */
|
|
68
|
+
if (sid === 35 && !isUser && isServer && !config.local && !config.debug) {
|
|
69
|
+
log.warn({ name: 'api', message: 'access restricted: 5' });
|
|
70
|
+
return { message: 'access restricted: 5', status: 403 };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const sqlInjection = [
|
|
2
|
+
'()',
|
|
3
|
+
'^',
|
|
4
|
+
'*',
|
|
5
|
+
'like ',
|
|
6
|
+
'@variable',
|
|
7
|
+
'@@variable',
|
|
8
|
+
'group by ',
|
|
9
|
+
'union ',
|
|
10
|
+
'select ',
|
|
11
|
+
'having ',
|
|
12
|
+
'as injectx',
|
|
13
|
+
'where ',
|
|
14
|
+
'rlike ',
|
|
15
|
+
'if(',
|
|
16
|
+
'sleep(',
|
|
17
|
+
'waitfor delay',
|
|
18
|
+
'benchmark(',
|
|
19
|
+
'pg_sleep(',
|
|
20
|
+
"'\\\"",
|
|
21
|
+
'randomblob(',
|
|
22
|
+
'order by ',
|
|
23
|
+
'union all ',
|
|
24
|
+
'+or',
|
|
25
|
+
'or ',
|
|
26
|
+
'and ',
|
|
27
|
+
"'' ",
|
|
28
|
+
'""" ',
|
|
29
|
+
'<script',
|
|
30
|
+
'javascript:',
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
export default sqlInjection;
|
package/policy/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// import fp from 'fastify-plugin';
|
|
2
|
+
|
|
3
|
+
import checkPolicy from './funcs/checkPolicy.js';
|
|
4
|
+
|
|
5
|
+
async function plugin(fastify) {
|
|
6
|
+
fastify.addHook('onRequest', async (request, reply) => {
|
|
7
|
+
const hookData = checkPolicy(request);
|
|
8
|
+
if (hookData?.status && hookData?.message) {
|
|
9
|
+
return reply.status(hookData?.status).send(hookData.message);
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default plugin;
|
package/redis/index.js
CHANGED
|
@@ -8,7 +8,7 @@ function close(fastify) {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
async function plugin(fastify) {
|
|
11
|
-
const client = getRedis({ db: 0 });
|
|
11
|
+
const client = getRedis({ db: 0, funcs: fastify });
|
|
12
12
|
client.getJSON = client.get;
|
|
13
13
|
fastify.decorate('rclient', client);
|
|
14
14
|
fastify.decorate('getRedis', getRedis);
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
{
|
|
2
|
+
"label_style": "vertical",
|
|
3
|
+
"saveButton": false,
|
|
4
|
+
"schema": {
|
|
5
|
+
"dataset_name": {
|
|
6
|
+
"type": "Text",
|
|
7
|
+
"ua": "Назва набору українською",
|
|
8
|
+
"validators": [
|
|
9
|
+
"required"
|
|
10
|
+
],
|
|
11
|
+
"col": 12
|
|
12
|
+
},
|
|
13
|
+
"enabled": {
|
|
14
|
+
"type": "Switcher",
|
|
15
|
+
"ua": "On / Off",
|
|
16
|
+
"col": 3
|
|
17
|
+
},
|
|
18
|
+
"home_is": {
|
|
19
|
+
"ua": "Виводити на публічці",
|
|
20
|
+
"type": "Switcher",
|
|
21
|
+
"col": 2
|
|
22
|
+
},
|
|
23
|
+
"isadmin": {
|
|
24
|
+
"type": "Switcher",
|
|
25
|
+
"ua": "Admin only",
|
|
26
|
+
"col": 3
|
|
27
|
+
},
|
|
28
|
+
"is_map": {
|
|
29
|
+
"type": "Switcher",
|
|
30
|
+
"ua": "Наявність переходу на карту",
|
|
31
|
+
"col": 3
|
|
32
|
+
},
|
|
33
|
+
"is_register": {
|
|
34
|
+
"type": "Switcher",
|
|
35
|
+
"ua": "Наявність переходу до реєстру",
|
|
36
|
+
"col": 3
|
|
37
|
+
},
|
|
38
|
+
"table_name": {
|
|
39
|
+
"type": "Text",
|
|
40
|
+
"ua": "Назва таблиці в БД"
|
|
41
|
+
},
|
|
42
|
+
"pk": {
|
|
43
|
+
"type": "Text",
|
|
44
|
+
"ua": "Primary key",
|
|
45
|
+
"validators": [
|
|
46
|
+
"required"
|
|
47
|
+
]
|
|
48
|
+
},
|
|
49
|
+
"query": {
|
|
50
|
+
"type": "Text",
|
|
51
|
+
"ru": "Запит до таблиці",
|
|
52
|
+
"ua": "Запит до таблиці"
|
|
53
|
+
},
|
|
54
|
+
"order_by": {
|
|
55
|
+
"type": "Text",
|
|
56
|
+
"ua": "Умова на сортування"
|
|
57
|
+
},
|
|
58
|
+
"group_id": {
|
|
59
|
+
"type": "Autocomplete",
|
|
60
|
+
"data": "gis.category.select",
|
|
61
|
+
"ua": "Група"
|
|
62
|
+
},
|
|
63
|
+
"service_type": {
|
|
64
|
+
"type": "Select2",
|
|
65
|
+
"data": "dataset.service_type",
|
|
66
|
+
"col": 12,
|
|
67
|
+
"ua": "Тип сервісу"
|
|
68
|
+
},
|
|
69
|
+
"setting.popup": {
|
|
70
|
+
"type": "Text",
|
|
71
|
+
"ua": "Popup"
|
|
72
|
+
},
|
|
73
|
+
"setting.card": {
|
|
74
|
+
"type": "Text",
|
|
75
|
+
"ua": "Шаблон"
|
|
76
|
+
},
|
|
77
|
+
"geom": {
|
|
78
|
+
"type": "Geom",
|
|
79
|
+
"ua": "Баунд"
|
|
80
|
+
},
|
|
81
|
+
"export_columns": {
|
|
82
|
+
"type": "Tags",
|
|
83
|
+
"data1": "column.list.select",
|
|
84
|
+
"parent": "table_name",
|
|
85
|
+
"ua": "Колонки для экспорту"
|
|
86
|
+
},
|
|
87
|
+
"column_list": {
|
|
88
|
+
"type": "DataTable",
|
|
89
|
+
"font-size": "10",
|
|
90
|
+
"ua": "Колонки",
|
|
91
|
+
"import": true,
|
|
92
|
+
"height": 600,
|
|
93
|
+
"colModel": [
|
|
94
|
+
{
|
|
95
|
+
"name": "name",
|
|
96
|
+
"type": "Text",
|
|
97
|
+
"ua": "Назва колонки в БД"
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"name": "ua",
|
|
101
|
+
"type": "Text",
|
|
102
|
+
"ua": "Назва колонки українською",
|
|
103
|
+
"validators": [
|
|
104
|
+
"required"
|
|
105
|
+
]
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"name": "meta",
|
|
109
|
+
"type": "Autocomplete",
|
|
110
|
+
"data": "gis.meta_type",
|
|
111
|
+
"ua": "Meta",
|
|
112
|
+
"hide_column": true,
|
|
113
|
+
"column_hide": true
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"name": "format",
|
|
117
|
+
"type": "Autocomplete",
|
|
118
|
+
"data": "gis.column_list_type",
|
|
119
|
+
"default": "text",
|
|
120
|
+
"ua": "Тип колонки",
|
|
121
|
+
"validators": [
|
|
122
|
+
"required"
|
|
123
|
+
]
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"name": "data",
|
|
127
|
+
"ua": "Класифікатор",
|
|
128
|
+
"ru": "Класификатор",
|
|
129
|
+
"en": "Domen"
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"name": "values",
|
|
133
|
+
"type": "DataTable",
|
|
134
|
+
"hide_column": true,
|
|
135
|
+
"column_hide": true,
|
|
136
|
+
"ua": "Домени",
|
|
137
|
+
"help": "Використовується для створення випадаючих списків",
|
|
138
|
+
"colModel": [
|
|
139
|
+
{
|
|
140
|
+
"name": "ua",
|
|
141
|
+
"ua": "Назва домена"
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"name": "color",
|
|
145
|
+
"ua": "Колір"
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
"name": "icon",
|
|
149
|
+
"ua": "Іконка"
|
|
150
|
+
}
|
|
151
|
+
]
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"name": "hidden_table_public",
|
|
155
|
+
"type": "Switcher",
|
|
156
|
+
"ua": "Приховати поле в прев'ю",
|
|
157
|
+
"i": "Чи приховати поле при перегляді об'єктів набора"
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"name": "hidden_public",
|
|
161
|
+
"type": "Switcher",
|
|
162
|
+
"ua": "Приховати поле в картоці об'єкта"
|
|
163
|
+
}
|
|
164
|
+
]
|
|
165
|
+
},
|
|
166
|
+
"filter_list": {
|
|
167
|
+
"type": "DataTable",
|
|
168
|
+
"height": 600,
|
|
169
|
+
"import": true,
|
|
170
|
+
"title": false,
|
|
171
|
+
"colModel": [
|
|
172
|
+
{
|
|
173
|
+
"name": "name",
|
|
174
|
+
"type": "Text",
|
|
175
|
+
"ua": "Колонка"
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
"name": "type",
|
|
179
|
+
"edittype": "Autocomplete",
|
|
180
|
+
"ua": "Формат",
|
|
181
|
+
"data": "gis.filter_type"
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
"name": "data",
|
|
185
|
+
"ua": "Класифікатор",
|
|
186
|
+
"ru": "Класификатор",
|
|
187
|
+
"width": 30,
|
|
188
|
+
"en": "Domen"
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
"name": "disabled",
|
|
192
|
+
"width": 10,
|
|
193
|
+
"ru": "Відключити",
|
|
194
|
+
"ua": "Відключити",
|
|
195
|
+
"type": "Switcher",
|
|
196
|
+
"col": 2
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
"name": "ua",
|
|
200
|
+
"width": 30,
|
|
201
|
+
"ua": "Назва українською"
|
|
202
|
+
}
|
|
203
|
+
]
|
|
204
|
+
},
|
|
205
|
+
"style.geometry_type": {
|
|
206
|
+
"type": "Autocomplete",
|
|
207
|
+
"ua": "Тип геометрії",
|
|
208
|
+
"default": "point",
|
|
209
|
+
"validators": [
|
|
210
|
+
"required"
|
|
211
|
+
],
|
|
212
|
+
"options": [
|
|
213
|
+
{
|
|
214
|
+
"id": "point",
|
|
215
|
+
"text": "Точкові об'єкти"
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
"id": "line",
|
|
219
|
+
"text": "Лінійні об'єкти"
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
"id": "polygon",
|
|
223
|
+
"text": "Полігональні об'єкти"
|
|
224
|
+
}
|
|
225
|
+
],
|
|
226
|
+
"behavior": {
|
|
227
|
+
"point": {
|
|
228
|
+
"show": "style.as_icon,style.icon,style.pointFillColor,style.pointFillOpacity,style.pointStrokeColor,style.pointStrokeOpacity,style.pointStrokeWidth,style.radius,style.pointWidth,style.pointHeight",
|
|
229
|
+
"hide": "style.line_dotted,style.color,style.weight,style.fillColor,style.fillOpacity"
|
|
230
|
+
},
|
|
231
|
+
"line": {
|
|
232
|
+
"show": "style.line_dotted,style.color,style.weight",
|
|
233
|
+
"hide": "style.as_icon,style.icon,style.pointFillColor,style.pointFillOpacity,style.pointStrokeColor,style.pointStrokeOpacity,style.pointStrokeWidth,style.radius,style.pointWidth,style.pointHeight,style.fillColor,style.fillOpacity"
|
|
234
|
+
},
|
|
235
|
+
"polygon": {
|
|
236
|
+
"show": "style.fillColor,style.fillOpacity",
|
|
237
|
+
"hide": "style.as_icon,style.icon,style.pointFillColor,style.pointFillOpacity,style.pointStrokeColor,style.pointStrokeOpacity,style.pointStrokeWidth,style.radius,style.pointWidth,style.pointHeight,style.line_dotted,style.color,style.weight"
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
"style.as_icon": {
|
|
242
|
+
"type": "Autocomplete",
|
|
243
|
+
"ua": "Відображення іконкою",
|
|
244
|
+
"default": "yes",
|
|
245
|
+
"options": [
|
|
246
|
+
{
|
|
247
|
+
"id": "yes",
|
|
248
|
+
"text": "Так"
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
"id": "no",
|
|
252
|
+
"text": "Ні"
|
|
253
|
+
}
|
|
254
|
+
],
|
|
255
|
+
"behavior": {
|
|
256
|
+
"yes": {
|
|
257
|
+
"show": "style.icon",
|
|
258
|
+
"hide": "style.fillColor,style.fillOpacity,style.pointFillColor,style.pointFillOpacity,style.pointHeight,style.pointWidth,style.radius,style.pointStrokeWidth,style.pointStrokeOpacity,style.pointStrokeColor"
|
|
259
|
+
},
|
|
260
|
+
"no": {
|
|
261
|
+
"show": "style.fillColor,style.fillOpacity,style.pointFillColor,style.pointFillOpacity,style.pointHeight,style.pointWidth,style.radius,style.pointStrokeWidth,style.pointStrokeOpacity,style.pointStrokeColor",
|
|
262
|
+
"hide": "style.icon"
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
"style.icon": {
|
|
267
|
+
"type": "Text",
|
|
268
|
+
"ru": "Иконка",
|
|
269
|
+
"ua": "Іконка",
|
|
270
|
+
"help": "Приклад pin-l-bar+AA0000+2+2+4+20+20.png повна інструкція <a target='_blank' href='/api-user/marker_icon/'>/marker_icon/</a>"
|
|
271
|
+
},
|
|
272
|
+
"style.pointFillColor": {
|
|
273
|
+
"type": "colorpicker",
|
|
274
|
+
"ru": "Цвет точки",
|
|
275
|
+
"ua": "Колір точки",
|
|
276
|
+
"help": "Приклади: #aaa, red, #fa5050. По замовчуванню blue."
|
|
277
|
+
},
|
|
278
|
+
"style.pointFillOpacity": {
|
|
279
|
+
"type": "Number",
|
|
280
|
+
"ru": "Прозрачность точки",
|
|
281
|
+
"ua": "Прозорість точки",
|
|
282
|
+
"help": "Значення від 0.0 до 1.0. По замовчуванню 1.0."
|
|
283
|
+
},
|
|
284
|
+
"style.pointStrokeColor": {
|
|
285
|
+
"type": "colorpicker",
|
|
286
|
+
"ru": "Цвет контура",
|
|
287
|
+
"ua": "Колір контуру",
|
|
288
|
+
"help": "Приклади: #aaa, red, #fa5050. По замовчуванню black."
|
|
289
|
+
},
|
|
290
|
+
"style.pointStrokeOpacity": {
|
|
291
|
+
"type": "Number",
|
|
292
|
+
"ru": "Прозрачность контура",
|
|
293
|
+
"ua": "Прозорість контуру",
|
|
294
|
+
"help": "Значення від 0.0 до 1.0. По замовчуванню 1.0."
|
|
295
|
+
},
|
|
296
|
+
"style.pointStrokeWidth": {
|
|
297
|
+
"type": "Number",
|
|
298
|
+
"ru": "Ширина контура",
|
|
299
|
+
"ua": "Ширина контуру",
|
|
300
|
+
"help": "По замовчуванню 1."
|
|
301
|
+
},
|
|
302
|
+
"style.radius": {
|
|
303
|
+
"type": "Text",
|
|
304
|
+
"ru": "Radius",
|
|
305
|
+
"ua": "Radius",
|
|
306
|
+
"help": "По замовчуванню 10."
|
|
307
|
+
},
|
|
308
|
+
"style.pointWidth": {
|
|
309
|
+
"type": "Number",
|
|
310
|
+
"ru": "Ширина точки",
|
|
311
|
+
"ua": "Ширина точки",
|
|
312
|
+
"help": "По замовчуванню 10."
|
|
313
|
+
},
|
|
314
|
+
"style.pointHeight": {
|
|
315
|
+
"type": "Number",
|
|
316
|
+
"ru": "Высота точки",
|
|
317
|
+
"ua": "Висота точки",
|
|
318
|
+
"help": "По замовчуванню 10."
|
|
319
|
+
},
|
|
320
|
+
"style.fillColor": {
|
|
321
|
+
"type": "colorpicker",
|
|
322
|
+
"ru": "Цвет полигонов",
|
|
323
|
+
"ua": "Колір полігонів",
|
|
324
|
+
"help": "Приклади: #aaa, red, #fa5050."
|
|
325
|
+
},
|
|
326
|
+
"style.fillOpacity": {
|
|
327
|
+
"type": "Number",
|
|
328
|
+
"ru": "Прозрачность полигонов",
|
|
329
|
+
"ua": "Прозорість полігонів",
|
|
330
|
+
"help": "Значення від 0.0 до 1.0. По замовчуванню 1.0."
|
|
331
|
+
},
|
|
332
|
+
"style.color": {
|
|
333
|
+
"type": "colorpicker",
|
|
334
|
+
"ru": "Цвет линий",
|
|
335
|
+
"ua": "Колір ліній",
|
|
336
|
+
"help": "Приклади: #aaa, red, #fa5050."
|
|
337
|
+
},
|
|
338
|
+
"style.weight": {
|
|
339
|
+
"type": "Number",
|
|
340
|
+
"ru": "Ширина линий",
|
|
341
|
+
"ua": "Ширина ліній",
|
|
342
|
+
"help": "Значення в px. По замовчуванню 1.0."
|
|
343
|
+
},
|
|
344
|
+
"style.line_dotted": {
|
|
345
|
+
"type": "Text",
|
|
346
|
+
"ru": "Пунктирная линия",
|
|
347
|
+
"ua": "Пунктирна лінія",
|
|
348
|
+
"help": "Приклад: '10, 5', де значення задається з двох чисел через кому: перше - довжина пунктиру в px, а друге - відстань між пунктирами, в px. По замовчуванню відключено."
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
"fieldsets": [
|
|
352
|
+
{
|
|
353
|
+
"legend": "Основні налаштування",
|
|
354
|
+
"ua": "Основні налаштування",
|
|
355
|
+
"fields": [
|
|
356
|
+
"dataset_name",
|
|
357
|
+
"group_id",
|
|
358
|
+
"table_name",
|
|
359
|
+
"pk",
|
|
360
|
+
"query",
|
|
361
|
+
"service_type",
|
|
362
|
+
"enabled",
|
|
363
|
+
"isadmin",
|
|
364
|
+
"is_map",
|
|
365
|
+
"is_register",
|
|
366
|
+
"setting.popup",
|
|
367
|
+
"setting.card",
|
|
368
|
+
"geom"
|
|
369
|
+
]
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
"legend": "Колонки",
|
|
373
|
+
"ru": "Колонки",
|
|
374
|
+
"ua": "Колонки",
|
|
375
|
+
"fields": [
|
|
376
|
+
"column_list",
|
|
377
|
+
"export_columns"
|
|
378
|
+
]
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
"legend": "Фільтри",
|
|
382
|
+
"ru": "Фильтры",
|
|
383
|
+
"ua": "Фільтри",
|
|
384
|
+
"fields": [
|
|
385
|
+
"filter_list"
|
|
386
|
+
]
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
"legend": "Стилі",
|
|
390
|
+
"ru": "Стили",
|
|
391
|
+
"ua": "Стилі",
|
|
392
|
+
"fields": [
|
|
393
|
+
"style.geometry_type",
|
|
394
|
+
"style.as_icon",
|
|
395
|
+
"style.icon",
|
|
396
|
+
"style.pointFillColor",
|
|
397
|
+
"style.pointFillOpacity",
|
|
398
|
+
"style.pointStrokeColor",
|
|
399
|
+
"style.pointStrokeOpacity",
|
|
400
|
+
"style.pointStrokeWidth",
|
|
401
|
+
"style.radius",
|
|
402
|
+
"style.pointWidth",
|
|
403
|
+
"style.pointHeight",
|
|
404
|
+
"style.line_dotted",
|
|
405
|
+
"style.color",
|
|
406
|
+
"style.weight",
|
|
407
|
+
"style.fillColor",
|
|
408
|
+
"style.fillOpacity"
|
|
409
|
+
]
|
|
410
|
+
}
|
|
411
|
+
]
|
|
412
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import getTemplate from './utils/getTemplate.js';
|
|
2
2
|
import getFilterSQL from '../funcs/getFilterSQL/index.js';
|
|
3
3
|
import getMeta from '../../pg/funcs/getMeta.js';
|
|
4
|
+
import metaFormat from '../funcs/metaFormat/index.js';
|
|
4
5
|
|
|
5
6
|
const maxLimit = 100;
|
|
6
7
|
export default async function data(req) {
|
|
@@ -49,6 +50,7 @@ export default async function data(req) {
|
|
|
49
50
|
|
|
50
51
|
const total = keyQuery || params.id ? rows.length : await pg.queryCache(`select count(*) from ${table} t where ${where.join(' and ') || 'true'}`).then((el) => el?.rows[0]?.count);
|
|
51
52
|
|
|
53
|
+
await metaFormat({ rows, table: params.table });
|
|
52
54
|
return {
|
|
53
55
|
time: Date.now() - time, total, pk, form, rows, meta, columns, filters,
|
|
54
56
|
};
|
|
@@ -3,6 +3,7 @@ import getSelect from './utils/getSelect.js';
|
|
|
3
3
|
|
|
4
4
|
export default async function filter(req) {
|
|
5
5
|
const time = Date.now();
|
|
6
|
+
|
|
6
7
|
const {
|
|
7
8
|
params,
|
|
8
9
|
} = req;
|
|
@@ -12,7 +13,14 @@ export default async function filter(req) {
|
|
|
12
13
|
const filters = loadTable?.filters || loadTable?.filterList || [];
|
|
13
14
|
await Promise.all(filters.filter((el) => el.data).map(async (el) => {
|
|
14
15
|
const cls = await getSelect(el.data);
|
|
15
|
-
|
|
16
|
+
if (!cls?.arr || !loadTable.table) return;
|
|
17
|
+
const countArr = await req.pg.queryCache(`select ${el.id}::text as id,count(*) from ${loadTable.table} group by ${el.id}`);
|
|
18
|
+
|
|
19
|
+
const options = countArr.rows.map(cel => {
|
|
20
|
+
const data = cls?.arr.find(c => c.id === cel.id);
|
|
21
|
+
return { ...cel, ...data };
|
|
22
|
+
});
|
|
23
|
+
Object.assign(el, { options });
|
|
16
24
|
}));
|
|
17
25
|
return {
|
|
18
26
|
time: Date.now() - time,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import getTemplate from '../../controllers/utils/getTemplate.js';
|
|
2
|
+
import getSelect from '../../controllers/utils/getSelect.js';
|
|
3
|
+
import pg from '../../../pg/pgClients.js';
|
|
4
|
+
|
|
5
|
+
export default async function metaFormat({ rows, table }) {
|
|
6
|
+
const loadTable = await getTemplate('table', table);
|
|
7
|
+
const selectCols = loadTable.columns?.filter((e) => e.data);
|
|
8
|
+
|
|
9
|
+
// cls & select format
|
|
10
|
+
|
|
11
|
+
await Promise.all(selectCols?.map(async (attr) => {
|
|
12
|
+
const val = [...new Set(rows?.map((el) => el[attr.name]).flat())];
|
|
13
|
+
if (!val.filter((el) => el).length) return null;
|
|
14
|
+
const cls = await getSelect(attr.data);
|
|
15
|
+
if (!cls?.arr && !cls?.sql) return null;
|
|
16
|
+
|
|
17
|
+
const data = cls.arr || await pg.client.query(`with c(id,text) as (${cls.sql}) select * from c where id = any('{${val}}')`).then(el => el.rows);
|
|
18
|
+
|
|
19
|
+
const clsAr = data.reduce((p, el) => ({ ...p, [el.id.toString()]: el.text }), {});
|
|
20
|
+
rows.forEach(el => {
|
|
21
|
+
Object.assign(el, { [`${attr.name}_text`]: clsAr[el[attr.name]] || el[attr.name] });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return null;
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
return rows;
|
|
28
|
+
}
|
package/table/index.js
CHANGED
|
@@ -2,9 +2,12 @@ import suggest from './controllers/suggest.js';
|
|
|
2
2
|
import data from './controllers/data.js';
|
|
3
3
|
import filter from './controllers/filter.js';
|
|
4
4
|
import form from './controllers/form.js';
|
|
5
|
+
import metaFormat from './funcs/metaFormat/index.js';
|
|
5
6
|
|
|
6
7
|
async function plugin(fastify, config = {}) {
|
|
7
8
|
const prefix = config.prefix || '/api';
|
|
9
|
+
fastify.decorate('metaFormat', metaFormat);
|
|
10
|
+
|
|
8
11
|
fastify.get(`${prefix}/suggest/:data`, {}, suggest);
|
|
9
12
|
fastify.get(`${prefix}/data/:table/:id?`, {}, data); // vs.crm.data.api с node
|
|
10
13
|
fastify.get(`${prefix}/filter/:table`, {}, filter);
|
package/test/api/crud.test.js
CHANGED
|
@@ -2,15 +2,17 @@ import { test } from 'node:test';
|
|
|
2
2
|
import assert from 'node:assert';
|
|
3
3
|
|
|
4
4
|
import build from '../../helper.js';
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
import config from '../config.js';
|
|
6
7
|
|
|
7
8
|
test('api crud', async (t) => {
|
|
8
9
|
const app = await build(t);
|
|
10
|
+
const prefix = config.prefix || '/api';
|
|
9
11
|
|
|
10
12
|
await t.test('POST /insert', async () => {
|
|
11
13
|
const res = await app.inject({
|
|
12
14
|
method: 'POST',
|
|
13
|
-
url:
|
|
15
|
+
url: `${prefix}/crud/gis.dataset`,
|
|
14
16
|
body: { dataset_name: '111', dataset_id: '5400000' },
|
|
15
17
|
});
|
|
16
18
|
|
|
@@ -24,20 +26,20 @@ test('api crud', async (t) => {
|
|
|
24
26
|
await t.test('PUT /update', async () => {
|
|
25
27
|
const res = await app.inject({
|
|
26
28
|
method: 'PUT',
|
|
27
|
-
url:
|
|
29
|
+
url: `${prefix}/crud/gis.dataset/5400000`,
|
|
28
30
|
body: { editor_id: '11' },
|
|
29
31
|
});
|
|
30
32
|
|
|
31
33
|
const rep = JSON.parse(res?.body);
|
|
32
34
|
// rep.dataset_id
|
|
33
35
|
// console.log(rep);
|
|
34
|
-
assert.equal(rep.editor_id, '11')
|
|
36
|
+
assert.equal(rep.editor_id, '11');
|
|
35
37
|
});
|
|
36
38
|
|
|
37
39
|
await t.test('DELETE /delete', async () => {
|
|
38
40
|
const res = await app.inject({
|
|
39
41
|
method: 'DELETE',
|
|
40
|
-
url:
|
|
42
|
+
url: `${prefix}/crud/gis.dataset/5400000`,
|
|
41
43
|
});
|
|
42
44
|
|
|
43
45
|
const rep = JSON.parse(res?.body);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
|
|
4
|
+
import build from '../../helper.js';
|
|
5
|
+
|
|
6
|
+
import setTokenById from '../../crud/funcs/setTokenById.js';
|
|
7
|
+
import config from '../config.js';
|
|
8
|
+
|
|
9
|
+
test('api crud xss', async (t) => {
|
|
10
|
+
const app = await build(t);
|
|
11
|
+
const session = { passport: { user: { uid: '1' } } };
|
|
12
|
+
app.decorateRequest('session', session);
|
|
13
|
+
|
|
14
|
+
const prefix = config.prefix || '/api';
|
|
15
|
+
|
|
16
|
+
let addTokens;
|
|
17
|
+
let editTokens;
|
|
18
|
+
|
|
19
|
+
// before
|
|
20
|
+
t.test('setTokenById', async () => {
|
|
21
|
+
addTokens = setTokenById({
|
|
22
|
+
funcs: { config },
|
|
23
|
+
ids: [JSON.stringify({ add: 'gis.dataset', form: 'test.dataset.form' })],
|
|
24
|
+
mode: 'a',
|
|
25
|
+
session,
|
|
26
|
+
array: 1,
|
|
27
|
+
});
|
|
28
|
+
editTokens = setTokenById({
|
|
29
|
+
funcs: { config },
|
|
30
|
+
ids: [JSON.stringify({ id: '5400000', table: 'gis.dataset', form: 'test.dataset.form' })],
|
|
31
|
+
mode: 'w',
|
|
32
|
+
session,
|
|
33
|
+
array: 1,
|
|
34
|
+
});
|
|
35
|
+
assert.ok(addTokens.length === 1 && editTokens.length === 1, 'invalid token');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
await t.test('POST /insert', async () => {
|
|
39
|
+
const res = await app.inject({
|
|
40
|
+
method: 'POST',
|
|
41
|
+
url: `${prefix}/crud/${addTokens[0]}`,
|
|
42
|
+
body: { dataset_name: '<a onClick="alert("XSS Injection")">xss injection</a>', dataset_id: '5400000' },
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const rep = JSON.parse(res?.body);
|
|
46
|
+
|
|
47
|
+
assert.ok(rep.status, 409);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
await t.test('PUT /update', async () => {
|
|
51
|
+
const res = await app.inject({
|
|
52
|
+
method: 'PUT',
|
|
53
|
+
url: `${prefix}/crud/${editTokens[0]}/${editTokens[0]}`,
|
|
54
|
+
body: { editor_id: '11', dataset_name: '<a onClick="alert("XSS Injection")">xss injection</a>' },
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const rep = JSON.parse(res?.body);
|
|
58
|
+
|
|
59
|
+
assert.equal(rep.status, 409);
|
|
60
|
+
});
|
|
61
|
+
await t.test('DELETE /delete', async () => {
|
|
62
|
+
const res = await app.inject({
|
|
63
|
+
method: 'DELETE',
|
|
64
|
+
url: `${prefix}/crud/gis.dataset/5400000`,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const rep = JSON.parse(res?.body);
|
|
68
|
+
assert.ok(rep);
|
|
69
|
+
});
|
|
70
|
+
});
|
package/test/funcs/crud.test.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { test } from 'node:test';
|
|
2
2
|
import assert from 'node:assert';
|
|
3
|
-
import '../config.js';
|
|
4
3
|
|
|
5
4
|
import pgClients from '../../pg/pgClients.js';
|
|
6
5
|
import rclient from '../../redis/client.js';
|
|
@@ -13,15 +12,18 @@ import isFileExists from '../../crud/funcs/isFileExists.js';
|
|
|
13
12
|
import getOpt from '../../crud/funcs/getOpt.js';
|
|
14
13
|
import setOpt from '../../crud/funcs/setOpt.js';
|
|
15
14
|
|
|
16
|
-
|
|
15
|
+
import getIdByToken from '../../crud/funcs/getIdByToken.js';
|
|
16
|
+
import setTokenById from '../../crud/funcs/setTokenById.js';
|
|
17
|
+
import config from '../config.js';
|
|
17
18
|
|
|
19
|
+
test('funcs crud', async (t) => {
|
|
18
20
|
await t.test('getOpt/setOpt', async () => {
|
|
19
21
|
const opt = await setOpt({ table: 'gis.dataset' });
|
|
20
22
|
const data = await getOpt(opt);
|
|
21
23
|
// console.log(data);
|
|
22
24
|
assert.equal(data.table, 'gis.dataset');
|
|
23
25
|
});
|
|
24
|
-
|
|
26
|
+
|
|
25
27
|
const id = (Math.random() * 10000).toFixed();
|
|
26
28
|
await t.test('dataInsert', async () => {
|
|
27
29
|
const data = await dataInsert({ table: 'gis.dataset', data: { dataset_id: id, dataset_name: '222' } });
|
|
@@ -39,10 +41,34 @@ test('funcs crud', async (t) => {
|
|
|
39
41
|
});
|
|
40
42
|
|
|
41
43
|
await t.test('isFileExists', async () => {
|
|
42
|
-
const data = await isFileExists({filepath: '../../crud/funcs/isFileExists.js'});
|
|
44
|
+
const data = await isFileExists({ filepath: '../../crud/funcs/isFileExists.js' });
|
|
43
45
|
assert.equal(data, false);
|
|
44
46
|
});
|
|
45
47
|
|
|
48
|
+
let tokens;
|
|
49
|
+
const session = { passport: { user: { uid: '1' } } };
|
|
50
|
+
const tokenData = JSON.stringify({ add: 'gis.dataset', form: 'test.dataset.form' });
|
|
51
|
+
|
|
52
|
+
await t.test('setTokenById', async () => {
|
|
53
|
+
tokens = setTokenById({
|
|
54
|
+
funcs: { config },
|
|
55
|
+
ids: [tokenData],
|
|
56
|
+
mode: 'a',
|
|
57
|
+
session,
|
|
58
|
+
array: 1,
|
|
59
|
+
});
|
|
60
|
+
assert.equal(tokens.length, 1);
|
|
61
|
+
});
|
|
62
|
+
await t.test('getIdByToken', async () => {
|
|
63
|
+
const data = await getIdByToken({
|
|
64
|
+
funcs: { config },
|
|
65
|
+
session,
|
|
66
|
+
token: tokens[0],
|
|
67
|
+
mode: 'a',
|
|
68
|
+
});
|
|
69
|
+
assert.equal(data, tokenData);
|
|
70
|
+
});
|
|
71
|
+
|
|
46
72
|
// pgClients.client.query('delete from gis.dataset where dataset_id=$1', [id]);
|
|
47
73
|
t.after(() => {
|
|
48
74
|
pgClients.client?.end();
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "3",
|
|
4
|
+
"text": "Заміна",
|
|
5
|
+
"en": "Replacement",
|
|
6
|
+
"color": "#B8860B"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"id": "2",
|
|
10
|
+
"text": "Видалення",
|
|
11
|
+
"en": "Removal",
|
|
12
|
+
"color": "#8B0000"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"id": "1",
|
|
16
|
+
"text": "Обрізка",
|
|
17
|
+
"en": "Cutting",
|
|
18
|
+
"color": "#DEB887"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "4",
|
|
22
|
+
"text": "Відсутні",
|
|
23
|
+
"en": "None",
|
|
24
|
+
"color": "#2E8B57"
|
|
25
|
+
}
|
|
26
|
+
]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "4",
|
|
4
|
+
"text": "Газони",
|
|
5
|
+
"en": "Lawns",
|
|
6
|
+
"icon": "/assets/image/icon/60936458728884429/3f714a00-6dec-11ea-b853-074b5683525e.svg",
|
|
7
|
+
"color": "#87a96b",
|
|
8
|
+
"data": "lawns"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"id": "8",
|
|
12
|
+
"text": "Пам’ятка природи",
|
|
13
|
+
"en": "Landmark",
|
|
14
|
+
"color": "blue",
|
|
15
|
+
"data": "landmark"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"id": "7",
|
|
19
|
+
"text": "Пеньок",
|
|
20
|
+
"en": "Stump",
|
|
21
|
+
"icon": "/assets/image/icon/2322523888191276250/52ec1310-0d2b-11eb-ab6a-23ffd484ced7.svg",
|
|
22
|
+
"color": "#63594C",
|
|
23
|
+
"data": "stump"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "2",
|
|
27
|
+
"text": "Дерева",
|
|
28
|
+
"en": "Tree",
|
|
29
|
+
"icon": "/assets/image/icon/2202400955901674510/ba471bb0-5b13-11ea-9003-07912ed5d347.svg",
|
|
30
|
+
"color": "#66cd00",
|
|
31
|
+
"data": "tree"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"id": "6",
|
|
35
|
+
"text": "Лунка",
|
|
36
|
+
"en": "Digger",
|
|
37
|
+
"icon": "/assets/image/icon/2322523536775709909/50634550-0d2b-11eb-ab6a-23ffd484ced7.svg",
|
|
38
|
+
"color": "#cdb79e",
|
|
39
|
+
"data": "digger"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"id": "1",
|
|
43
|
+
"text": "Кущі",
|
|
44
|
+
"en": "Bush",
|
|
45
|
+
"icon": "/assets/image/icon/2202401113020302354/c62b1580-5b13-11ea-9003-07912ed5d347.svg",
|
|
46
|
+
"color": "#556b2f",
|
|
47
|
+
"data": "bush"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"id": "3",
|
|
51
|
+
"text": "Живопліт",
|
|
52
|
+
"en": "Hedge",
|
|
53
|
+
"icon": "/assets/image/icon/59524404264212563/dd365ec0-6e6d-11ea-bc2a-d3dca36653f0.svg",
|
|
54
|
+
"color": "#9fa91f",
|
|
55
|
+
"data": "hedge"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"id": "5",
|
|
59
|
+
"text": "Квітники",
|
|
60
|
+
"en": "Flowers",
|
|
61
|
+
"icon": "/assets/image/icon/2254649206885057992/e961fa00-5b13-11ea-9003-07912ed5d347.svg",
|
|
62
|
+
"color": "#ff7f24",
|
|
63
|
+
"data": "flowers"
|
|
64
|
+
}
|
|
65
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
select contact_id, coalesce(last_name,'')||' '||coalesce(first_name,'') from crm_acc.crm_contact order by last_name
|