@opengis/fastify-table 1.3.66 → 1.3.68
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/pg/funcs/getPG.js +1 -1
- package/server/plugins/pg/funcs/getPGAsync.js +1 -3
- package/server/plugins/policy/funcs/checkXSS.js +5 -5
- 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/server/routes/table/controllers/suggest.js +43 -9
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
|
+
}
|
|
@@ -12,7 +12,7 @@ import getDBParams from './getDBParams.js';
|
|
|
12
12
|
|
|
13
13
|
function getPG(param) {
|
|
14
14
|
const dbListParams = dblist.find(el => el.key === param?.key)
|
|
15
|
-
|| dblist.find(el => el.database === (param?.db || param?.database || param));
|
|
15
|
+
|| dblist.find(el => el.database === (param?.db || param?.database || param) && el.port === param?.port);
|
|
16
16
|
const {
|
|
17
17
|
user, password, host, port, db, database, name: origin,
|
|
18
18
|
} = dbListParams ?? (typeof param === 'string' ? getDBParams(param) : param || {});
|
|
@@ -12,7 +12,7 @@ import getDBParams from './getDBParams.js';
|
|
|
12
12
|
|
|
13
13
|
async function getPGAsync(param) {
|
|
14
14
|
const dbListParams = dblist.find(el => el.key === param?.key)
|
|
15
|
-
|| dblist.find(el => el.database === (param?.db || param?.database || param));
|
|
15
|
+
|| dblist.find(el => el.database === (param?.db || param?.database || param) && el.port === param?.port);
|
|
16
16
|
const {
|
|
17
17
|
user, password, host, port, db, database, name: origin,
|
|
18
18
|
} = dbListParams ?? (typeof param === 'string' ? getDBParams(param) : param || {});
|
|
@@ -29,8 +29,6 @@ async function getPGAsync(param) {
|
|
|
29
29
|
statement_timeout: config.pg?.statement_timeout || 10000,
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
-
if (!dbConfig.database) { return null; }
|
|
33
|
-
|
|
34
32
|
pgClients[name] = new pg.Pool(dbConfig);
|
|
35
33
|
|
|
36
34
|
await init(pgClients[name]);
|
|
@@ -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 };
|
|
@@ -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) });
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import { fileURLToPath } from 'url';
|
|
3
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
4
3
|
|
|
5
4
|
import {
|
|
6
|
-
config, getPG, getTemplate, getSelectMeta, getMeta, applyHook, getSelectVal,
|
|
5
|
+
config, getPG, getTemplate, getSelectMeta, getMeta, applyHook, getSelectVal, logger,
|
|
7
6
|
} from '../../../../utils.js';
|
|
8
7
|
|
|
9
8
|
const limit = 50;
|
|
@@ -13,8 +12,6 @@ const headers = {
|
|
|
13
12
|
'Cache-Control': 'no-cache',
|
|
14
13
|
};
|
|
15
14
|
|
|
16
|
-
const dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
-
|
|
18
15
|
function getTableColumnMeta(table, column, filtered) {
|
|
19
16
|
const original = {
|
|
20
17
|
true: `with c(id,text) as (select ${column}, ${column} from ${table} group by ${column}) select id, text from c`,
|
|
@@ -70,7 +67,9 @@ export default async function suggest(req) {
|
|
|
70
67
|
|
|
71
68
|
const meta = table && column
|
|
72
69
|
? getTableColumnMeta(table, column, query?.key || query?.val)
|
|
73
|
-
: await getSelectMeta({
|
|
70
|
+
: await getSelectMeta({
|
|
71
|
+
pg: pg1, name: selectName, nocache: query?.nocache, parent,
|
|
72
|
+
});
|
|
74
73
|
|
|
75
74
|
if (meta?.minLength && query.key && query.key.length < meta?.minLength) {
|
|
76
75
|
return { message: `min length: ${meta.minLength}` };
|
|
@@ -93,10 +92,10 @@ export default async function suggest(req) {
|
|
|
93
92
|
|
|
94
93
|
const { columns = [] } = await getMeta({ pg, table: loadTable?.table || tableName });
|
|
95
94
|
|
|
96
|
-
const
|
|
95
|
+
const column1 = columns.find(el => el.name === query.column);
|
|
97
96
|
const args = { table: tableName };
|
|
98
|
-
if (!
|
|
99
|
-
const sqlCls = `select array_agg(distinct value)::text[] from (select ${pg.pgType?.[
|
|
97
|
+
if (!column1) return [];
|
|
98
|
+
const sqlCls = `select array_agg(distinct value)::text[] from (select ${pg.pgType?.[column1.dataTypeID]?.includes('[]') ? `unnest(${column1.name})` : `${column1.name}`} as value from ${(loadTable?.table || tableName).replace(/'/g, "''")} where ${hookBody?.query || loadTable?.query || '1=1'})q`;
|
|
100
99
|
|
|
101
100
|
if (query.sql && (config.local || user?.user_type?.includes?.('admin'))) {
|
|
102
101
|
return sqlCls;
|
|
@@ -113,6 +112,20 @@ export default async function suggest(req) {
|
|
|
113
112
|
|
|
114
113
|
const data2 = data1.filter((el) => el.id && vals.includes(el.id.toString()));
|
|
115
114
|
const data = data2.slice(0, Math.min(query.limit || limit, limit));
|
|
115
|
+
|
|
116
|
+
if (config.debug) {
|
|
117
|
+
logger.file('suggest/debug', {
|
|
118
|
+
type: 1,
|
|
119
|
+
loadTable: loadTable?.table,
|
|
120
|
+
tableName,
|
|
121
|
+
column: column?.name,
|
|
122
|
+
data,
|
|
123
|
+
data1,
|
|
124
|
+
data2,
|
|
125
|
+
query,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
116
129
|
return {
|
|
117
130
|
time: Date.now() - time,
|
|
118
131
|
limit: Math.min(query.limit || limit, limit),
|
|
@@ -122,7 +135,7 @@ export default async function suggest(req) {
|
|
|
122
135
|
sql: (config.local || user?.user_type?.includes?.('admin')) ? sqlCls : undefined,
|
|
123
136
|
data,
|
|
124
137
|
};
|
|
125
|
-
}
|
|
138
|
+
}
|
|
126
139
|
}
|
|
127
140
|
|
|
128
141
|
if (arr) {
|
|
@@ -131,6 +144,16 @@ export default async function suggest(req) {
|
|
|
131
144
|
? arr?.filter((el) => !lower || (el[lang] || el.text)?.toLowerCase()?.indexOf(lower) !== -1)?.filter((el) => !query.val || el.id === query.val)
|
|
132
145
|
: arr;
|
|
133
146
|
const data = data1.slice(0, Math.min(query.limit || limit, limit));
|
|
147
|
+
|
|
148
|
+
if (config.debug) {
|
|
149
|
+
logger.file('suggest/debug', {
|
|
150
|
+
type: 2,
|
|
151
|
+
key: query.key,
|
|
152
|
+
data,
|
|
153
|
+
data1,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
134
157
|
return {
|
|
135
158
|
time: Date.now() - time,
|
|
136
159
|
limit: Math.min(query.limit || limit, limit),
|
|
@@ -183,6 +206,17 @@ export default async function suggest(req) {
|
|
|
183
206
|
data.forEach(el => Object.assign(el, { text: clsData?.[el.id || ''] || el.id }));
|
|
184
207
|
}
|
|
185
208
|
|
|
209
|
+
if (config.debug) {
|
|
210
|
+
logger.file('suggest/debug', {
|
|
211
|
+
type: 3,
|
|
212
|
+
sel: query.sel,
|
|
213
|
+
data,
|
|
214
|
+
dataNew,
|
|
215
|
+
sqlSuggest,
|
|
216
|
+
ids,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
186
220
|
const message = {
|
|
187
221
|
time: Date.now() - time,
|
|
188
222
|
limit: Math.min(query.limit || meta.limit || limit, limit),
|