@opengis/fastify-table 1.1.20 → 1.1.22
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 +8 -0
- package/index.js +12 -8
- package/module/core/select/core.user_mentioned.sql +2 -0
- package/notification/controllers/readNotifications.js +30 -0
- package/notification/controllers/userNotifications.js +52 -7
- package/notification/funcs/addNotification.js +16 -5
- package/notification/hook/onWidgetSet.js +63 -0
- package/notification/index.js +20 -3
- package/package.json +1 -1
- package/policy/funcs/checkPolicy.js +31 -22
- package/policy/index.js +1 -1
- package/server/migrations/crm.sql +20 -3
- package/server/migrations/users.sql +35 -1
- package/test/api/notification.test.js +56 -19
- package/test/api/user.test.js +84 -0
- package/user/controllers/user.cls.id.js +14 -0
- package/user/controllers/user.cls.js +71 -0
- package/user/controllers/user.cls.post.js +55 -0
- package/user/controllers/user.info.js +21 -0
- package/user/index.js +46 -0
- package/utils.js +10 -0
- package/widget/controllers/widget.set.js +6 -5
- package/hook/index.js +0 -6
package/Changelog.md
CHANGED
package/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import path from 'path';
|
|
2
|
-
import { existsSync, readdirSync, readFileSync } from 'fs';
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
3
4
|
|
|
4
5
|
import fp from 'fastify-plugin';
|
|
5
6
|
import config from './config.js';
|
|
@@ -14,11 +15,11 @@ import crudPlugin from './crud/index.js';
|
|
|
14
15
|
import policyPlugin from './policy/index.js';
|
|
15
16
|
import utilPlugin from './util/index.js';
|
|
16
17
|
import cronPlugin from './cron/index.js';
|
|
17
|
-
import
|
|
18
|
+
import userPlugin from './user/index.js';
|
|
18
19
|
|
|
19
20
|
import pgClients from './pg/pgClients.js';
|
|
20
21
|
|
|
21
|
-
import execMigrations from './
|
|
22
|
+
import { addTemplateDir, execMigrations } from './utils.js';
|
|
22
23
|
|
|
23
24
|
async function plugin(fastify, opt) {
|
|
24
25
|
// console.log(opt);
|
|
@@ -68,10 +69,8 @@ async function plugin(fastify, opt) {
|
|
|
68
69
|
await client.query(sql, [JSON.stringify(rows).replace(/'/g, "''")]);
|
|
69
70
|
}
|
|
70
71
|
}
|
|
71
|
-
// call from another repo / project
|
|
72
|
-
fastify.execMigrations = execMigrations;
|
|
73
72
|
// execute core migrations
|
|
74
|
-
await
|
|
73
|
+
await execMigrations();
|
|
75
74
|
});
|
|
76
75
|
if (!fastify.funcs) {
|
|
77
76
|
fastify.addHook('onRequest', async (req) => {
|
|
@@ -93,7 +92,12 @@ async function plugin(fastify, opt) {
|
|
|
93
92
|
widgetPlugin(fastify, opt);
|
|
94
93
|
utilPlugin(fastify, opt);
|
|
95
94
|
cronPlugin(fastify, opt);
|
|
96
|
-
|
|
95
|
+
userPlugin(fastify, opt);
|
|
96
|
+
|
|
97
|
+
// core templates && cls
|
|
98
|
+
const filename = fileURLToPath(import.meta.url);
|
|
99
|
+
const cwd = path.dirname(filename);
|
|
100
|
+
addTemplateDir(path.join(cwd, 'module/core'));
|
|
97
101
|
}
|
|
98
102
|
export default fp(plugin);
|
|
99
103
|
// export { rclient };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export default async function readNotifications({
|
|
2
|
+
pg, params = {}, query = {}, session = {},
|
|
3
|
+
}) {
|
|
4
|
+
const { uid } = session.passport?.user || {};
|
|
5
|
+
|
|
6
|
+
if (!uid) {
|
|
7
|
+
return { message: 'access restricted', status: 403 };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const { userId } = await pg.query('select uid as "userId" from admin.users where $1 in (uid,login) limit 1', [uid])
|
|
12
|
+
.then((res) => res.rows?.[0] || {});
|
|
13
|
+
|
|
14
|
+
if (!userId) {
|
|
15
|
+
return { message: 'access restricted: 2', status: 403 };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const q = `update crm.notifications set read=true where read is not true
|
|
19
|
+
and ${params?.id ? 'notification_id=$2' : '1=1'} and addressee_id=$1`;
|
|
20
|
+
|
|
21
|
+
if (query.sql) return q;
|
|
22
|
+
|
|
23
|
+
const { rowCount = 0 } = await pg.query(q, [userId, params?.id].filter((el) => el));
|
|
24
|
+
|
|
25
|
+
return { message: `${rowCount} unread notifications marked as read`, status: 200 };
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
return { error: err.toString(), status: 500 };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -1,16 +1,61 @@
|
|
|
1
|
+
// for example only
|
|
2
|
+
/*
|
|
3
|
+
const res = await funcs.dataInsert({
|
|
4
|
+
pg,
|
|
5
|
+
table: 'crm.notifications',
|
|
6
|
+
data: {
|
|
7
|
+
subject: 'notif title',
|
|
8
|
+
body: 'notif body',
|
|
9
|
+
link: 'http://localhost:3000/api/notification',
|
|
10
|
+
addressee_id: userId,
|
|
11
|
+
author_id: userId,
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { getSelectVal } from '../../utils.js';
|
|
17
|
+
|
|
18
|
+
const maxLimit = 100;
|
|
19
|
+
|
|
1
20
|
export default async function userNotifications({
|
|
2
|
-
pg,
|
|
21
|
+
pg, query = {}, session = {},
|
|
3
22
|
}) {
|
|
4
23
|
const time = Date.now();
|
|
24
|
+
|
|
25
|
+
const { uid } = session.passport?.user || {};
|
|
26
|
+
|
|
27
|
+
if (!uid) {
|
|
28
|
+
return { message: 'access restricted', status: 403 };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const limit = Math.min(maxLimit, +(query.limit || 5));
|
|
32
|
+
const offset = query.page && query.page > 0 ? (query.page - 1) * limit : 0;
|
|
33
|
+
|
|
5
34
|
try {
|
|
6
|
-
const {
|
|
7
|
-
|
|
35
|
+
const { userId } = await pg.query('select uid as "userId" from admin.users where $1 in (uid,login) limit 1', [uid])
|
|
36
|
+
.then((res) => res.rows?.[0] || {});
|
|
37
|
+
|
|
38
|
+
if (!userId) {
|
|
39
|
+
return { message: 'access restricted: 2', status: 403 };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const q = `select notification_id as id, subject, body, cdate,
|
|
43
|
+
author_id, read, link, entity_id, (select avatar from admin.users where uid=a.author_id limit 1) as avatar from crm.notifications a where addressee_id=$1 order by cdate desc limit ${limit} offset ${offset}`;
|
|
44
|
+
|
|
45
|
+
if (query.sql) return q;
|
|
46
|
+
|
|
47
|
+
const { rows = [] } = await pg.query(q, [userId]);
|
|
48
|
+
|
|
49
|
+
const values = rows.map((el) => el.author_id)
|
|
50
|
+
?.filter((el, idx, arr) => el && arr.indexOf(el) === idx);
|
|
8
51
|
|
|
9
|
-
|
|
52
|
+
if (values?.length) {
|
|
53
|
+
const vals = await getSelectVal({ name: 'core.user_mentioned', values });
|
|
54
|
+
rows.forEach((row) => {
|
|
55
|
+
Object.assign(row, { author: vals?.[row.author_id] });
|
|
56
|
+
});
|
|
57
|
+
}
|
|
10
58
|
|
|
11
|
-
// queryCache not supports $1 params
|
|
12
|
-
const { rows } = await queryFunc(`select notification_id as id, notification_type as type,
|
|
13
|
-
notification_status as status, title, body, link, uid from crm.notification where uid='${uid}'`);
|
|
14
59
|
return { time: Date.now() - time, total: rows?.length, rows };
|
|
15
60
|
}
|
|
16
61
|
catch (err) {
|
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
export default async function addNotification({
|
|
2
|
-
pg, session = {},
|
|
2
|
+
pg, funcs, session = {}, subject, body, link, uid, entity,
|
|
3
3
|
}) {
|
|
4
|
-
const uid
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
const { uid: author } = session.passport?.user || {};
|
|
5
|
+
const res = await funcs.dataInsert({
|
|
6
|
+
pg,
|
|
7
|
+
table: 'crm.notifications',
|
|
8
|
+
data: {
|
|
9
|
+
subject,
|
|
10
|
+
body,
|
|
11
|
+
link,
|
|
12
|
+
addressee_id: uid,
|
|
13
|
+
author_id: author,
|
|
14
|
+
entity_id: entity,
|
|
15
|
+
uid: author,
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
return res?.rows?.[0] || {};
|
|
8
19
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/* eslint-disable camelcase */
|
|
2
|
+
import {
|
|
3
|
+
getSelect, addNotification, sendNotification,
|
|
4
|
+
} from '../../utils.js';
|
|
5
|
+
|
|
6
|
+
function sequence(arr, data, fn) {
|
|
7
|
+
return arr.reduce((promise, row) => promise.then(() => fn({
|
|
8
|
+
...data, ...row,
|
|
9
|
+
})), Promise.resolve());
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default async function onWidgetSet({ req, type, data = {} }) {
|
|
13
|
+
const values = data.body?.match(/\B@[a-zA-z0-9а-яА-яіїІЇєЄ]+ [a-zA-z0-9а-яА-яіїІЇєЄ]+/g)
|
|
14
|
+
?.filter((el, idx, arr) => el?.replace && arr.indexOf(el) === idx)
|
|
15
|
+
?.map((el) => el.replace(/@/g, ''));
|
|
16
|
+
|
|
17
|
+
if (type !== 'comment' || !values?.length) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const {
|
|
22
|
+
pg, funcs, path: link, session = {}, log,
|
|
23
|
+
} = req;
|
|
24
|
+
const { config = {} } = funcs;
|
|
25
|
+
|
|
26
|
+
const { sql } = await getSelect('core.user_mentioned');
|
|
27
|
+
|
|
28
|
+
const { rows = [] } = await pg.query(`with data (id,name,email) as (${sql})
|
|
29
|
+
select id, name, email from data where name = any($1)`, [values]);
|
|
30
|
+
|
|
31
|
+
if (!rows?.length) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const message = `${data.body?.substring(0, 50)}...`;
|
|
36
|
+
const subject = 'You were mentioned';
|
|
37
|
+
|
|
38
|
+
await sequence(rows, {}, async ({ id, name, email }) => {
|
|
39
|
+
const res = await addNotification({
|
|
40
|
+
pg, funcs, session, subject, entity: data.entity_id, body: message, link, uid: id,
|
|
41
|
+
});
|
|
42
|
+
try {
|
|
43
|
+
const res1 = await sendNotification({
|
|
44
|
+
pg,
|
|
45
|
+
funcs,
|
|
46
|
+
log,
|
|
47
|
+
to: email,
|
|
48
|
+
// template,
|
|
49
|
+
message,
|
|
50
|
+
title: subject,
|
|
51
|
+
data,
|
|
52
|
+
nocache: 1,
|
|
53
|
+
});
|
|
54
|
+
if (config.local) console.info('comment notification', name, id, email, res1);
|
|
55
|
+
await pg.query('update crm.notifications set sent=true where notification_id=$1', [res?.notification_id]);
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
console.error('comment notification send error', err.toString());
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
console.log('comment notification add', rows);
|
|
62
|
+
return null;
|
|
63
|
+
}
|
package/notification/index.js
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
// api
|
|
2
|
-
import
|
|
2
|
+
import readNotifications from './controllers/readNotifications.js'; // mark as read
|
|
3
|
+
import userNotifications from './controllers/userNotifications.js'; // check all, backend pagination
|
|
3
4
|
import testEmail from './controllers/testEmail.js';
|
|
4
5
|
// funcs
|
|
5
6
|
import addNotification from './funcs/addNotification.js'; // add to db
|
|
6
|
-
import notification from './funcs/sendNotification.js'; // send
|
|
7
|
+
import notification from './funcs/sendNotification.js'; // send notification
|
|
8
|
+
|
|
9
|
+
import onWidgetSet from './hook/onWidgetSet.js'; // send notification on comment
|
|
10
|
+
import { addHook } from '../utils.js';
|
|
7
11
|
|
|
8
12
|
const tableSchema = {
|
|
13
|
+
params: {
|
|
14
|
+
id: { type: 'string' },
|
|
15
|
+
},
|
|
9
16
|
querystring: {
|
|
10
17
|
nocache: { type: 'string', pattern: '^(\\d+)$' },
|
|
11
18
|
},
|
|
@@ -17,11 +24,20 @@ async function plugin(fastify, config = {}) {
|
|
|
17
24
|
method: 'GET',
|
|
18
25
|
url: `${prefix}/notification`,
|
|
19
26
|
config: {
|
|
20
|
-
policy: ['user'],
|
|
27
|
+
policy: ['user'],
|
|
21
28
|
},
|
|
22
29
|
schema: tableSchema,
|
|
23
30
|
handler: userNotifications,
|
|
24
31
|
});
|
|
32
|
+
fastify.route({
|
|
33
|
+
method: 'GET',
|
|
34
|
+
url: `${prefix}/notification-read/:id?`,
|
|
35
|
+
config: {
|
|
36
|
+
policy: ['user'],
|
|
37
|
+
},
|
|
38
|
+
schema: tableSchema,
|
|
39
|
+
handler: readNotifications,
|
|
40
|
+
});
|
|
25
41
|
fastify.route({
|
|
26
42
|
method: 'GET',
|
|
27
43
|
url: `${prefix}/test-email`,
|
|
@@ -33,6 +49,7 @@ async function plugin(fastify, config = {}) {
|
|
|
33
49
|
|
|
34
50
|
fastify.decorate('addNotification', addNotification);
|
|
35
51
|
fastify.decorate('notification', notification);
|
|
52
|
+
addHook('onWidgetSet', onWidgetSet);
|
|
36
53
|
}
|
|
37
54
|
|
|
38
55
|
export default plugin;
|
package/package.json
CHANGED
|
@@ -24,60 +24,69 @@ export default function checkPolicy(req) {
|
|
|
24
24
|
|
|
25
25
|
/*= == 0.Check superadmin access === */
|
|
26
26
|
if (policy.includes('superadmin') && user?.user_type !== 'superadmin') {
|
|
27
|
-
log.warn({
|
|
28
|
-
|
|
27
|
+
log.warn('api/superadmin', {
|
|
28
|
+
path, params, query, body: JSON.stringify(req?.body || {}).substring(30), message: 'access restricted: 0',
|
|
29
29
|
});
|
|
30
30
|
return { message: 'access restricted: 0', status: 403 };
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/*= == 1.File injection === */
|
|
34
34
|
if (JSON.stringify(params || {})?.includes('../') || JSON.stringify(query || {})?.includes('../') || path?.includes('../')) {
|
|
35
|
-
log.warn({
|
|
36
|
-
|
|
35
|
+
log.warn('injection/file', {
|
|
36
|
+
path, params, query, message: 'access restricted: 1',
|
|
37
37
|
});
|
|
38
38
|
return { message: 'access restricted: 1', status: 403 };
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
/* === 1.1 File === */
|
|
42
42
|
const allowExtPublic = ['.png', '.jpg', '.svg'];
|
|
43
43
|
const ext = path.toLowerCase().substr(-4);
|
|
44
44
|
if (path.includes('files/') && allowExtPublic.includes(ext)) return null;
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
/* === 2.SQL Injection policy: no-sql === */
|
|
47
47
|
if (!policy.includes('no-sql')) {
|
|
48
48
|
// skip polyline param - data filter (geometry bounds)
|
|
49
49
|
const stopWords = block.filter((el) => path.replace(query.polyline, '').includes(el));
|
|
50
50
|
if (stopWords?.length) {
|
|
51
|
-
log.warn(
|
|
51
|
+
log.warn('injection/sql', { stopWords, message: 'access restricted: 2', path });
|
|
52
52
|
return { message: 'access restricted: 2', status: 403 };
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
-
/*
|
|
56
|
-
const isApi = ['/files/', '/api/format/', '/api-user/', '/logger', '/file/'].filter((el) => path.includes(el)).length;
|
|
57
|
-
if (!isApi)
|
|
58
|
-
|
|
59
|
-
/*= == 3. policy: referer === */
|
|
60
|
-
if (!hs?.referer?.includes?.(hostname) && policy.includes('referer') && !config.local && !config.debug) {
|
|
61
|
-
log.warn({ name: 'referer', message: 'access restricted: 3' });
|
|
62
|
-
return { message: 'access restricted: 3', status: 403 };
|
|
55
|
+
/* policy: skip if not API */
|
|
56
|
+
const isApi = ['/files/', '/api/format/', '/api', '/api-user/', '/logger', '/file/'].filter((el) => path.includes(el)).length;
|
|
57
|
+
if (!isApi) {
|
|
58
|
+
return null;
|
|
63
59
|
}
|
|
64
60
|
|
|
65
|
-
|
|
61
|
+
/* === policy: public === */
|
|
66
62
|
if (policy.includes('public')) {
|
|
67
63
|
return null;
|
|
68
64
|
}
|
|
69
65
|
|
|
70
|
-
|
|
71
|
-
if (!policy.includes('
|
|
72
|
-
log.warn(
|
|
66
|
+
/* === 3. policy: user === */
|
|
67
|
+
if (!user && policy.includes('user') && false) {
|
|
68
|
+
log.warn('policy/user', { message: 'access restricted: 3', path });
|
|
69
|
+
return { message: 'access restricted: 3', status: 403 };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* === 4. policy: referer === */
|
|
73
|
+
if (!hs?.referer?.includes?.(hostname) && policy.includes('referer') && !config.local && !config.debug) {
|
|
74
|
+
log.warn('policy/referer', { message: 'access restricted: 4', uid: user?.uid });
|
|
73
75
|
return { message: 'access restricted: 4', status: 403 };
|
|
74
76
|
}
|
|
75
77
|
|
|
76
|
-
|
|
77
|
-
if (sid ===
|
|
78
|
-
log.warn(
|
|
78
|
+
/* === 5. policy: site auth === */
|
|
79
|
+
if (!policy.includes('site') && sid === 1 && isUser && !config.local && !config.debug) {
|
|
80
|
+
log.warn('policy/site', { message: 'access restricted: 5', path, uid: user?.uid });
|
|
79
81
|
return { message: 'access restricted: 5', status: 403 };
|
|
80
82
|
}
|
|
81
83
|
|
|
84
|
+
/* === 6. base policy: block api, except login === */
|
|
85
|
+
if (sid === 35 && !isUser && isServer && !config.local && !config.debug
|
|
86
|
+
&& !path.startsWith(`${config.prefix || '/api'}/login`)) {
|
|
87
|
+
log.warn('policy/api', { message: 'access restricted: 6', path, uid: user?.uid });
|
|
88
|
+
return { message: 'access restricted: 6', status: 403 };
|
|
89
|
+
}
|
|
90
|
+
|
|
82
91
|
return null;
|
|
83
92
|
}
|
package/policy/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import checkPolicy from './funcs/checkPolicy.js';
|
|
4
4
|
|
|
5
5
|
async function plugin(fastify) {
|
|
6
|
-
fastify.addHook('
|
|
6
|
+
fastify.addHook('preParsing', async (request, reply) => {
|
|
7
7
|
const hookData = checkPolicy(request);
|
|
8
8
|
if (hookData?.status && hookData?.message) {
|
|
9
9
|
return reply.status(hookData?.status).send(hookData.message);
|
|
@@ -6,12 +6,20 @@ CREATE TABLE IF NOT EXISTS crm.notifications();
|
|
|
6
6
|
ALTER TABLE crm.notifications DROP CONSTRAINT IF EXISTS crm_notifications_pkey;
|
|
7
7
|
ALTER TABLE crm.notifications ADD COLUMN IF NOT EXISTS notification_id text NOT NULL DEFAULT next_id();
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
ALTER TABLE crm.notifications
|
|
11
|
-
ALTER TABLE crm.notifications
|
|
9
|
+
-- drop deprecated columns
|
|
10
|
+
ALTER TABLE crm.notifications DROP COLUMN IF EXISTS notification_user_id;
|
|
11
|
+
ALTER TABLE crm.notifications DROP COLUMN IF EXISTS notification_type;
|
|
12
|
+
ALTER TABLE crm.notifications DROP COLUMN IF EXISTS notification_status;
|
|
13
|
+
|
|
14
|
+
-- add actual columns
|
|
15
|
+
ALTER TABLE crm.notifications ADD COLUMN IF NOT EXISTS addressee_id text;
|
|
16
|
+
ALTER TABLE crm.notifications ADD COLUMN IF NOT EXISTS read boolean;
|
|
17
|
+
ALTER TABLE crm.notifications ADD COLUMN IF NOT EXISTS sent boolean;
|
|
12
18
|
ALTER TABLE crm.notifications ADD COLUMN IF NOT EXISTS subject text;
|
|
13
19
|
ALTER TABLE crm.notifications ADD COLUMN IF NOT EXISTS body text;
|
|
14
20
|
ALTER TABLE crm.notifications ADD COLUMN IF NOT EXISTS link text;
|
|
21
|
+
ALTER TABLE crm.notifications ADD COLUMN IF NOT EXISTS author_id text;
|
|
22
|
+
ALTER TABLE crm.notifications ADD COLUMN IF NOT EXISTS entity_id text;
|
|
15
23
|
|
|
16
24
|
ALTER TABLE crm.notifications ADD COLUMN IF NOT EXISTS uid text;
|
|
17
25
|
ALTER TABLE crm.notifications ADD COLUMN IF NOT EXISTS files json;
|
|
@@ -20,6 +28,15 @@ ALTER TABLE crm.notifications ADD COLUMN IF NOT EXISTS editor_id text;
|
|
|
20
28
|
ALTER TABLE crm.notifications ADD COLUMN IF NOT EXISTS editor_date timestamp without time zone;
|
|
21
29
|
ALTER TABLE crm.notifications ADD CONSTRAINT crm_notifications_pkey PRIMARY KEY (notification_id);
|
|
22
30
|
|
|
31
|
+
COMMENT ON COLUMN crm.notifications.addressee_id is 'ID користувача отримувача повідомлення';
|
|
32
|
+
COMMENT ON COLUMN crm.notifications.read is 'Чи було повідомлення прочитане';
|
|
33
|
+
COMMENT ON COLUMN crm.notifications.sent is 'Чи було повідомлення відправлене';
|
|
34
|
+
COMMENT ON COLUMN crm.notifications.subject is 'Тема повідомлення';
|
|
35
|
+
COMMENT ON COLUMN crm.notifications.body is 'Зміст повідомлення';
|
|
36
|
+
COMMENT ON COLUMN crm.notifications.link is 'Посилання на об''єкт';
|
|
37
|
+
COMMENT ON COLUMN crm.notifications.author_id is 'ID користувача автора повідомлення';
|
|
38
|
+
COMMENT ON COLUMN crm.notifications.entity_id is 'ID на об''єкту';
|
|
39
|
+
|
|
23
40
|
-- crm.files
|
|
24
41
|
-- DROP TABLE IF EXISTS crm.files;
|
|
25
42
|
CREATE TABLE IF NOT EXISTS crm.files();
|
|
@@ -131,4 +131,38 @@ COMMENT ON COLUMN admin.users_social_auth.social_auth_obj IS 'обьект со
|
|
|
131
131
|
COMMENT ON COLUMN admin.users_social_auth.social_auth_date IS 'время получение последнего обьекта соцсети';
|
|
132
132
|
COMMENT ON COLUMN admin.users_social_auth.social_auth_softpro_code IS 'код обьекта соцсети, создаваемый и используемый сервером авторизации';
|
|
133
133
|
COMMENT ON COLUMN admin.users_social_auth.enabled IS 'Выключатель';
|
|
134
|
-
COMMENT ON COLUMN admin.users_social_auth.social_auth_url IS 'URL для QR code';
|
|
134
|
+
COMMENT ON COLUMN admin.users_social_auth.social_auth_url IS 'URL для QR code';
|
|
135
|
+
|
|
136
|
+
CREATE TABLE IF NOT EXISTS admin.user_cls();
|
|
137
|
+
ALTER TABLE admin.user_cls DROP CONSTRAINT IF EXISTS admin_user_cls_pkey;
|
|
138
|
+
ALTER TABLE admin.user_cls DROP CONSTRAINT IF EXISTS admin_user_unique;
|
|
139
|
+
|
|
140
|
+
ALTER TABLE admin.user_cls ADD COLUMN IF NOT EXISTS user_clsid text;
|
|
141
|
+
ALTER TABLE admin.user_cls ADD COLUMN IF NOT EXISTS code text;
|
|
142
|
+
ALTER TABLE admin.user_cls ADD COLUMN IF NOT EXISTS parent text;
|
|
143
|
+
ALTER TABLE admin.user_cls ADD COLUMN IF NOT EXISTS name text;
|
|
144
|
+
ALTER TABLE admin.user_cls ADD COLUMN IF NOT EXISTS icon text;
|
|
145
|
+
ALTER TABLE admin.user_cls ADD COLUMN IF NOT EXISTS data text;
|
|
146
|
+
ALTER TABLE admin.user_cls ADD COLUMN IF NOT EXISTS type text;
|
|
147
|
+
ALTER TABLE admin.user_cls ADD COLUMN IF NOT EXISTS files json;
|
|
148
|
+
ALTER TABLE admin.user_cls ADD COLUMN IF NOT EXISTS cdate timestamp without time zone;
|
|
149
|
+
ALTER TABLE admin.user_cls ADD COLUMN IF NOT EXISTS editor_id text;
|
|
150
|
+
ALTER TABLE admin.user_cls ADD COLUMN IF NOT EXISTS editor_date timestamp without time zone;
|
|
151
|
+
ALTER TABLE admin.user_cls ADD COLUMN IF NOT EXISTS uid text;
|
|
152
|
+
ALTER TABLE admin.user_cls ADD COLUMN IF NOT EXISTS color text;
|
|
153
|
+
|
|
154
|
+
ALTER TABLE admin.user_cls ALTER COLUMN user_clsid SET NOT NULL;
|
|
155
|
+
ALTER TABLE admin.user_cls ALTER COLUMN user_clsid SET DEFAULT next_id();
|
|
156
|
+
ALTER TABLE admin.user_cls ALTER COLUMN cdate SET DEFAULT (now())::timestamp without time zone;
|
|
157
|
+
ALTER TABLE admin.user_cls ALTER COLUMN uid SET NOT NULL;
|
|
158
|
+
ALTER TABLE admin.user_cls ALTER COLUMN uid SET DEFAULT '1'::text;
|
|
159
|
+
|
|
160
|
+
ALTER TABLE admin.user_cls ADD CONSTRAINT admin_user_cls_pkey PRIMARY KEY(user_clsid);
|
|
161
|
+
ALTER TABLE admin.user_cls ADD CONSTRAINT admin_user_unique UNIQUE(code, parent);
|
|
162
|
+
|
|
163
|
+
COMMENT ON TABLE admin.user_cls IS 'Користувацькі класифікатори';
|
|
164
|
+
COMMENT ON COLUMN admin.user_cls.user_clsid IS 'ID';
|
|
165
|
+
COMMENT ON COLUMN admin.user_cls.code IS 'Код';
|
|
166
|
+
COMMENT ON COLUMN admin.user_cls.name IS 'Назва';
|
|
167
|
+
COMMENT ON COLUMN admin.user_cls.icon IS 'Іконка';
|
|
168
|
+
COMMENT ON COLUMN admin.user_cls.color IS 'Колір';
|
|
@@ -4,34 +4,71 @@ import assert from 'node:assert';
|
|
|
4
4
|
import build from '../../helper.js';
|
|
5
5
|
import config from '../config.js';
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
import userNotifications from '../../notification/controllers/userNotifications.js';
|
|
7
|
+
import { getSelect, initPG, pgClients } from '../../utils.js';
|
|
10
8
|
|
|
11
|
-
|
|
9
|
+
const session = { passport: { user: { uid: config.testUser?.uid || '1' } } };
|
|
10
|
+
const { uid } = session.passport.user;
|
|
12
11
|
|
|
13
12
|
test('api && funcs notification', async (t) => {
|
|
14
13
|
const app = await build(t);
|
|
15
|
-
const pg = pgClients
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
const { client: pg } = pgClients;
|
|
15
|
+
await initPG(pg);
|
|
16
|
+
|
|
17
|
+
app.addHook('onRequest', async (req) => {
|
|
18
|
+
req.session = session;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const notificationIds = await pgClients.client.query(`insert into crm.notifications(addressee_id,entity_id,author_id,subject,body,link)
|
|
22
|
+
values($1,$1,$1,$1,$1,$1), ($1,$1,$1,$1,$1,$1) returning notification_id as "notificationId"`, [uid])
|
|
23
|
+
.then((res) => res.rows?.map((el) => el.notificationId) || {});
|
|
24
|
+
|
|
25
|
+
if (!notificationIds?.length) {
|
|
26
|
+
assert.ok(0, 'insert notification error');
|
|
27
|
+
}
|
|
28
|
+
await t.test('GET /notification', async () => {
|
|
19
29
|
const res = await app.inject({
|
|
20
30
|
method: 'GET',
|
|
21
|
-
url:
|
|
31
|
+
url: '/api/notification',
|
|
22
32
|
});
|
|
23
|
-
|
|
33
|
+
const rep = res.json();
|
|
34
|
+
assert.ok(rep.total > 0);
|
|
24
35
|
});
|
|
25
|
-
await t.test('GET /notification', async () => {
|
|
36
|
+
await t.test('GET /notification-read/:id', async () => {
|
|
26
37
|
const res = await app.inject({
|
|
27
38
|
method: 'GET',
|
|
28
|
-
url:
|
|
39
|
+
url: `/api/notification-read/${notificationIds[0]}`,
|
|
40
|
+
});
|
|
41
|
+
const rep = res.json();
|
|
42
|
+
assert.ok(!rep.message?.startsWith(0), res.body);
|
|
43
|
+
assert.ok(rep.message?.includes('unread notifications marked'), res.body);
|
|
44
|
+
});
|
|
45
|
+
await t.test('GET /notification-read', async () => {
|
|
46
|
+
const res = await app.inject({
|
|
47
|
+
method: 'GET',
|
|
48
|
+
url: '/api/notification-read',
|
|
29
49
|
});
|
|
30
|
-
const rep =
|
|
31
|
-
assert.ok(rep.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
})
|
|
50
|
+
const rep = res.json();
|
|
51
|
+
assert.ok(!rep.message?.startsWith(0), res.body);
|
|
52
|
+
assert.ok(rep.message?.includes('unread notifications marked'), res.body);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const { sql } = await getSelect('core.user_mentioned');
|
|
56
|
+
const { name } = await pg.query(`with data (id,name,email) as (${sql})
|
|
57
|
+
select name from data where id = $1`, [uid]).then((res) => res.rows?.[0] || {});
|
|
58
|
+
if (name) {
|
|
59
|
+
await t.test('GET Email Notification via HOOK at POST /widget/comment/:id', async () => {
|
|
60
|
+
const res = await app.inject({
|
|
61
|
+
method: 'POST',
|
|
62
|
+
url: `/api/widget/comment/${notificationIds[0]}`,
|
|
63
|
+
body: { body: `@${name}`, entity_id: uid },
|
|
64
|
+
});
|
|
65
|
+
assert.equal(res.statusCode, 200);
|
|
66
|
+
assert.equal(res.json().rowCount, 1);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
await t.test('clean up', async () => {
|
|
71
|
+
const { rowCount = 0 } = await pg.query('delete from crm.notifications where entity_id=$1', [uid]);
|
|
72
|
+
console.log('clean up', rowCount);
|
|
73
|
+
});
|
|
37
74
|
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
|
|
4
|
+
import pgClients from '../../pg/pgClients.js';
|
|
5
|
+
import init from '../../pg/funcs/init.js';
|
|
6
|
+
|
|
7
|
+
import build from '../../helper.js';
|
|
8
|
+
import config from '../config.js';
|
|
9
|
+
|
|
10
|
+
const prefix = config.prefix || '/api';
|
|
11
|
+
|
|
12
|
+
const body = {
|
|
13
|
+
name: 'test.user.cls',
|
|
14
|
+
type: 'json',
|
|
15
|
+
children: [
|
|
16
|
+
{
|
|
17
|
+
id: 'get',
|
|
18
|
+
text: 'Отримання',
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
test('applyHook to API data/table', async (t) => {
|
|
24
|
+
const app = await build(t);
|
|
25
|
+
await init(pgClients.client);
|
|
26
|
+
|
|
27
|
+
app.addHook('onRequest', async (req) => {
|
|
28
|
+
req.session = { passport: { user: { uid: '1' } } };
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
await t.test('GET /user-info', async () => {
|
|
32
|
+
const res = await app.inject({
|
|
33
|
+
method: 'GET',
|
|
34
|
+
url: `${prefix}/user-info`,
|
|
35
|
+
});
|
|
36
|
+
assert.equal(res.statusCode, 200);
|
|
37
|
+
assert.equal(res.json().uid, '1');
|
|
38
|
+
assert.ok(res.json().user_name, 'not enough info: user_name');
|
|
39
|
+
assert.ok(res.json().notifications || res.json().notifications === 0, 'not enough info: notifications');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
await t.test('GET /user-cls', async () => {
|
|
43
|
+
const res = await app.inject({
|
|
44
|
+
method: 'GET',
|
|
45
|
+
url: `${prefix}/user-cls`,
|
|
46
|
+
});
|
|
47
|
+
const { message = {} } = res.json() || {};
|
|
48
|
+
assert.ok(message.rows?.length, 'empty cls list');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
await t.test('POST /user-cls', async () => {
|
|
52
|
+
const res = await app.inject({
|
|
53
|
+
method: 'POST',
|
|
54
|
+
url: `${prefix}/user-cls`,
|
|
55
|
+
body,
|
|
56
|
+
});
|
|
57
|
+
assert.equal(res.statusCode, 200);
|
|
58
|
+
assert.ok(res.json().children?.length, 'insert user cls error');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
await t.test('GET /user-cls/:name', async () => {
|
|
62
|
+
const res = await app.inject({
|
|
63
|
+
method: 'GET',
|
|
64
|
+
url: `${prefix}/user-cls/${body.name}`,
|
|
65
|
+
body,
|
|
66
|
+
});
|
|
67
|
+
assert.equal(res.statusCode, 200);
|
|
68
|
+
assert.equal(res.json().status || 200, 200);
|
|
69
|
+
assert.equal(res.json().message?.children?.length, 1);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
await t.test('clean up', async () => {
|
|
73
|
+
const { rowCount = 0 } = await pgClients.client.query(`with recursive rows as (
|
|
74
|
+
select user_clsid, name, code, icon, color, parent
|
|
75
|
+
from admin.user_cls a
|
|
76
|
+
where name=$1
|
|
77
|
+
union all
|
|
78
|
+
select a.user_clsid, a.name, a.code, a.icon, a.color, a.parent
|
|
79
|
+
from admin.user_cls a
|
|
80
|
+
join rows b on a.parent=b.name
|
|
81
|
+
) delete from admin.user_cls where user_clsid in (select user_clsid from rows )`, [body.name]);
|
|
82
|
+
console.log('clean up', rowCount);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import userCls from './user.cls.js';
|
|
2
|
+
|
|
3
|
+
export default async function userClsId(req) {
|
|
4
|
+
const { id } = req.params || {};
|
|
5
|
+
const res = await userCls(req);
|
|
6
|
+
if (req.query?.sql || res?.error) {
|
|
7
|
+
return res;
|
|
8
|
+
}
|
|
9
|
+
const { rows = [] } = res?.message || {};
|
|
10
|
+
if (!rows?.length) {
|
|
11
|
+
return { message: `cls not found: ${id}`, status: 404 };
|
|
12
|
+
}
|
|
13
|
+
return { message: rows?.[0], status: 200 };
|
|
14
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { readFile } from 'fs/promises';
|
|
2
|
+
import { getSelect, getTemplatePath } from '../../utils.js';
|
|
3
|
+
|
|
4
|
+
export default async function userCls(req) {
|
|
5
|
+
const {
|
|
6
|
+
pg, params = {}, query = {}, session = {},
|
|
7
|
+
} = req;
|
|
8
|
+
const { uid } = session.passport?.user || {};
|
|
9
|
+
|
|
10
|
+
if (!uid) {
|
|
11
|
+
return { message: 'access restricted', status: 403 };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const q = `select user_clsid as id, name, type,
|
|
15
|
+
case when type='json' then (
|
|
16
|
+
${params?.id
|
|
17
|
+
? `(with recursive rows as (
|
|
18
|
+
select user_clsid, name, code, icon, color, parent
|
|
19
|
+
from admin.user_cls a
|
|
20
|
+
where name=u.name
|
|
21
|
+
union all
|
|
22
|
+
select a.user_clsid, a.name, a.code, a.icon, a.color, a.parent
|
|
23
|
+
from admin.user_cls a
|
|
24
|
+
join rows b on a.parent=b.name
|
|
25
|
+
) select json_agg(row_to_json(q)) from rows q where name<>u.name
|
|
26
|
+
)`
|
|
27
|
+
: 'select count(*)::int from admin.user_cls where parent=u.name'} ) else null end as children,
|
|
28
|
+
case when type='sql' then data else null end as sql from admin.user_cls u
|
|
29
|
+
where (case when type='json' then parent is null else true end)
|
|
30
|
+
and uid=(select uid from admin.users where $1 in (login,uid) limit 1)
|
|
31
|
+
and ${params?.id ? 'u.name=$2' : '1=1'}`;
|
|
32
|
+
|
|
33
|
+
if (query?.sql) return q;
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const { rows = [] } = await pg.query(q, [uid, params.id].filter((el) => el));
|
|
37
|
+
|
|
38
|
+
rows.forEach((row) => {
|
|
39
|
+
if (row.type === 'sql') delete row.children;
|
|
40
|
+
if (row.type === 'json') { delete row.sql; Object.assign(row, { children: row.children || (params?.id ? [] : 0) }); }
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const clsList = getTemplatePath('cls');
|
|
44
|
+
const selectList = getTemplatePath('select');
|
|
45
|
+
|
|
46
|
+
const userClsNames = rows.map((el) => el.name);
|
|
47
|
+
const selectNames = selectList.map((el) => el[0]);
|
|
48
|
+
|
|
49
|
+
const arr = (clsList || []).concat(selectList || []).map((el) => ({ name: el[0], path: el[1], type: el[2] }))
|
|
50
|
+
?.filter((el) => (params?.id ? el.name === params?.id : true))
|
|
51
|
+
?.filter((el) => ['sql', 'json'].includes(el?.type) && !userClsNames.includes(el.name))
|
|
52
|
+
?.filter((el) => (el?.type === 'json' ? !selectNames?.includes(el?.name) : true));
|
|
53
|
+
|
|
54
|
+
const res = await Promise.all(arr?.map(async (el) => {
|
|
55
|
+
const type = { json: 'cls', sql: 'select' }[el.type] || el.type;
|
|
56
|
+
// const clsData = await getSelect(type, el.name);
|
|
57
|
+
const str = await readFile(el.path, 'utf-8');
|
|
58
|
+
const clsData = type === 'cls' ? JSON.parse(str) : str;
|
|
59
|
+
if (type === 'cls') {
|
|
60
|
+
const children = params?.id ? clsData : clsData?.length || 0;
|
|
61
|
+
return { name: el.name, type: el.type, children };
|
|
62
|
+
}
|
|
63
|
+
return { name: el.name, type: el.type, sql: clsData?.sql || clsData };
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
return { message: { rows: rows.concat(res) }, status: 200 };
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
return { error: err.toString(), status: 200 };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export default async function userClsPost({
|
|
2
|
+
pg, body = {}, session = {},
|
|
3
|
+
}) {
|
|
4
|
+
const { uid } = session.passport?.user || {};
|
|
5
|
+
const {
|
|
6
|
+
name, type = 'json', children = [], sql,
|
|
7
|
+
} = body;
|
|
8
|
+
|
|
9
|
+
if (!uid) {
|
|
10
|
+
return { message: 'access restricted', status: 403 };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (!name) {
|
|
14
|
+
return { message: 'not enough params: name', status: 400 };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (type === 'json' && (!Array.isArray(children) || !children.length || !children?.[0]?.id)) {
|
|
18
|
+
return { message: 'invalid params: children (array of objects)', status: 400 };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (type === 'sql' && (!sql || typeof sql !== 'string')) {
|
|
22
|
+
return { message: 'invalid params: sql (string)', status: 400 };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const { rowCount = 0 } = await pg.query(`with recursive rows as (
|
|
27
|
+
select user_clsid, name, code, icon, color, parent
|
|
28
|
+
from admin.user_cls a
|
|
29
|
+
where name=$1
|
|
30
|
+
union all
|
|
31
|
+
select a.user_clsid, a.name, a.code, a.icon, a.color, a.parent
|
|
32
|
+
from admin.user_cls a
|
|
33
|
+
join rows b on a.parent=b.name
|
|
34
|
+
) delete from admin.user_cls where user_clsid in (select user_clsid from rows )`, [name]);
|
|
35
|
+
console.log('delete old user cls', name, rowCount);
|
|
36
|
+
|
|
37
|
+
const { id, data } = await pg.query('insert into admin.user_cls(name,type,data,uid) values($1,$2,$3,$4) returning user_clsid as id, data', [name, type, sql, uid])
|
|
38
|
+
.then((res) => res.rows?.[0] || {});
|
|
39
|
+
if (type === 'json') {
|
|
40
|
+
if (!id) { return { error: 'insert user cls error', status: 500 }; }
|
|
41
|
+
const q1 = `insert into admin.user_cls(code,name,color,icon,parent,uid)
|
|
42
|
+
|
|
43
|
+
select value->>'id',value->>'text',value->>'color',value->>'icon', '${name.replace(/'/g, "''")}', '${uid}'
|
|
44
|
+
from json_array_elements('${JSON.stringify(children).replace(/'/g, "''")}'::json)
|
|
45
|
+
|
|
46
|
+
returning user_clsid as id, code, name, parent`;
|
|
47
|
+
const { rows = [] } = await pg.query(q1);
|
|
48
|
+
return { id, children: rows };
|
|
49
|
+
}
|
|
50
|
+
return { id, data };
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
return { error: err.toString(), status: 500 };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export default async function userInfo({
|
|
2
|
+
pg, session = {},
|
|
3
|
+
}) {
|
|
4
|
+
const { uid } = session.passport?.user || {};
|
|
5
|
+
|
|
6
|
+
if (!uid) {
|
|
7
|
+
return { message: 'access restricted', status: 403 };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const data = await pg.query(`select user_name, sur_name, father_name, user_rnokpp, user_type, email, login from admin.users
|
|
11
|
+
where uid=$1`, [uid]).then((res) => res.rows?.[0] || {});
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const { notifications = 0 } = await pg.query(`select count(*)::int as notifications from crm.notifications
|
|
15
|
+
where addressee_id=$1 and read is not true`, [uid]).then((res) => res.rows?.[0] || {});
|
|
16
|
+
return { uid, ...data, notifications };
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
return { error: err.toString(), status: 500 };
|
|
20
|
+
}
|
|
21
|
+
}
|
package/user/index.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import userCls from './controllers/user.cls.js';
|
|
2
|
+
import userClsId from './controllers/user.cls.id.js';
|
|
3
|
+
import userInfo from './controllers/user.info.js';
|
|
4
|
+
import userClsPost from './controllers/user.cls.post.js';
|
|
5
|
+
|
|
6
|
+
async function plugin(fastify, opts) {
|
|
7
|
+
const { prefix = '/api' } = opts || {};
|
|
8
|
+
fastify.route({
|
|
9
|
+
method: 'GET',
|
|
10
|
+
url: `${prefix}/user-cls`,
|
|
11
|
+
config: {
|
|
12
|
+
policy: ['user'],
|
|
13
|
+
},
|
|
14
|
+
schema: {},
|
|
15
|
+
handler: userCls,
|
|
16
|
+
});
|
|
17
|
+
fastify.route({
|
|
18
|
+
method: 'GET',
|
|
19
|
+
url: `${prefix}/user-cls/:id`,
|
|
20
|
+
config: {
|
|
21
|
+
policy: ['user'],
|
|
22
|
+
},
|
|
23
|
+
schema: {},
|
|
24
|
+
handler: userClsId,
|
|
25
|
+
});
|
|
26
|
+
fastify.route({
|
|
27
|
+
method: 'POST',
|
|
28
|
+
url: `${prefix}/user-cls`,
|
|
29
|
+
config: {
|
|
30
|
+
policy: ['user'],
|
|
31
|
+
},
|
|
32
|
+
schema: {},
|
|
33
|
+
handler: userClsPost,
|
|
34
|
+
});
|
|
35
|
+
fastify.route({
|
|
36
|
+
method: 'GET',
|
|
37
|
+
url: `${prefix}/user-info`,
|
|
38
|
+
config: {
|
|
39
|
+
policy: ['user'],
|
|
40
|
+
},
|
|
41
|
+
schema: {},
|
|
42
|
+
handler: userInfo,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default plugin;
|
package/utils.js
CHANGED
|
@@ -24,8 +24,13 @@ import gisIRColumn from './table/controllers/utils/gisIRColumn.js';
|
|
|
24
24
|
import getMeta from './pg/funcs/getMeta.js';
|
|
25
25
|
import getAccess from './crud/funcs/getAccess.js';
|
|
26
26
|
import getSelectVal from './table/funcs/metaFormat/getSelectVal.js';
|
|
27
|
+
import getSelect from './table/controllers/utils/getSelect.js';
|
|
28
|
+
import getSelectMeta from './table/controllers/utils/getSelectMeta.js';
|
|
27
29
|
import applyHook from './hook/funcs/applyHook.js';
|
|
28
30
|
import addHook from './hook/funcs/addHook.js';
|
|
31
|
+
import execMigrations from './migration/exec.migrations.js';
|
|
32
|
+
import addNotification from './notification/funcs/addNotification.js';
|
|
33
|
+
import sendNotification from './notification/funcs/sendNotification.js';
|
|
29
34
|
|
|
30
35
|
export default null;
|
|
31
36
|
export {
|
|
@@ -49,6 +54,11 @@ export {
|
|
|
49
54
|
getMeta,
|
|
50
55
|
getAccess,
|
|
51
56
|
getSelectVal,
|
|
57
|
+
getSelectMeta,
|
|
58
|
+
getSelect,
|
|
52
59
|
applyHook,
|
|
53
60
|
addHook,
|
|
61
|
+
execMigrations,
|
|
62
|
+
addNotification,
|
|
63
|
+
sendNotification,
|
|
54
64
|
};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import {
|
|
4
|
+
getMeta, dataInsert, dataUpdate, applyHook,
|
|
5
|
+
} from '../../utils.js';
|
|
6
6
|
|
|
7
7
|
const tableList = {
|
|
8
8
|
comment: 'crm.communications',
|
|
@@ -21,8 +21,8 @@ export default async function widgetSet(req) {
|
|
|
21
21
|
} = req;
|
|
22
22
|
const { user = {} } = session.passport || {};
|
|
23
23
|
const { type, id, objectid } = params;
|
|
24
|
-
if (!['comment', 'checklist', 'file', 'gallery'].includes(type)) return {
|
|
25
|
-
if (!objectid) return {
|
|
24
|
+
if (!['comment', 'checklist', 'file', 'gallery'].includes(type)) return { message: 'param type not valid', status: 400 };
|
|
25
|
+
if (!objectid) return { message: 'id required', status: 400 };
|
|
26
26
|
|
|
27
27
|
const table = tableList[type];
|
|
28
28
|
|
|
@@ -57,6 +57,7 @@ export default async function widgetSet(req) {
|
|
|
57
57
|
|
|
58
58
|
const data = { ...body, uid: user?.uid, entity_id: objectid };
|
|
59
59
|
|
|
60
|
+
await applyHook('onWidgetSet', { req, type, data });
|
|
60
61
|
const result = id
|
|
61
62
|
? await dataUpdate({
|
|
62
63
|
table, data, id, uid: user?.uid,
|