adminizer 4.2.0 → 4.3.0-build.73
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/assets/add-BNb5A_qF.js +1 -0
- package/assets/{add-form-BjackXJe.js → add-form-CGi_qv3J.js} +1 -1
- package/assets/add-group-RRjHTu93.js +1 -0
- package/assets/{add-group-form-YI6KS37x.js → add-group-form-DSd4UrnU.js} +1 -1
- package/assets/add-user-DSuvcKp6.js +1 -0
- package/assets/{add-user-form-bv5BNX5A.js → add-user-form-BAssKDpI.js} +1 -1
- package/assets/{app-CVnUv8s_.css → app-GsYdx39W.css} +1 -1
- package/assets/{app-layout-DecN527i.js → app-layout-ContoN1K.js} +1 -1
- package/assets/{app-sidebar-4JFP8UdU.js → app-sidebar-D7l5BJPV.js} +1 -1
- package/assets/app-sidebar-header-CK8t32cm.js +1 -0
- package/assets/app.js +13 -13
- package/assets/{breadcrumbs-Bo2z-LNC.js → breadcrumbs-DnNOEnT4.js} +1 -1
- package/assets/{catalog-DVyrGTNR.js → catalog-BavcTVb4.js} +1 -1
- package/assets/{dashboard-Dy3pk9t7.js → dashboard-xr-OyVyG.js} +1 -1
- package/assets/{date-picker-B2z-aeuv.js → date-picker-DBHnomTY.js} +1 -1
- package/assets/{del-modal-l3LxJWCN.js → del-modal-BeF4r3gk.js} +1 -1
- package/assets/{field-renderer-BHo944Cn.js → field-renderer-Sd68OLyc.js} +3 -3
- package/assets/form-C9P9m_9q.js +1 -0
- package/assets/{form-hP8sjbND.js → form-DomTBI89.js} +1 -1
- package/assets/home-BWHeZm-1.js +2 -0
- package/assets/{icon-C_X2cQ6W.js → icon-ChQBPA5l.js} +1 -1
- package/assets/{init-user-DgCD4ZN1.js → init-user-DqMDju_E.js} +1 -1
- package/assets/input-error-g1h6ps7t.js +1 -0
- package/assets/list-EEgAn-nx.js +1 -0
- package/assets/{list-table-_OiQmr2A.js → list-table-Br0K3rXg.js} +1 -1
- package/assets/{login-jzvVXgAV.js → login-BR9cqsPn.js} +1 -1
- package/assets/manifest.json +97 -64
- package/assets/material-icon-BIcI8APG.js +1 -0
- package/assets/media-manager-CTstvyHm.js +14 -0
- package/assets/{module-Ceq6vuwa.js → module-CpZBs0fN.js} +1 -1
- package/assets/{multi-select-BbKSrMw2.js → multi-select-BFWbEjH-.js} +1 -1
- package/assets/{nav-footer-B8gMV6Wp.js → nav-footer-bO8D-Yir.js} +1 -1
- package/assets/{nav-main-BUp9qA8R.js → nav-main-DVgvko8g.js} +1 -1
- package/assets/{nav-user-DeF3SDRj.js → nav-user-vIwYShwE.js} +1 -1
- package/assets/notification-BTXEslqa.js +1 -0
- package/assets/{pagination-render-B9U_hVbB.js → pagination-render-rZ5_6g0n.js} +1 -1
- package/assets/{register-C6acWsLe.js → register-Kfu8zmmu.js} +1 -1
- package/assets/tabs-CwgzWt48.js +1 -0
- package/assets/{theme-switcher-D8T9NgWG.js → theme-switcher-fzVA8btX.js} +1 -1
- package/assets/{user-info-CAgkqARx.js → user-info-Db5Y8KeN.js} +1 -1
- package/assets/{user-menu-content-BxL0QMC9.js → user-menu-content-Bc8YNZPf.js} +1 -1
- package/assets/{welcome-Dk7GoS5R.js → welcome-MKPFhPtG.js} +1 -1
- package/controllers/add.js +2 -0
- package/controllers/edit.js +14 -0
- package/controllers/login.js +56 -21
- package/controllers/media-manager/mediaManagerApi.js +1 -1
- package/controllers/notifications/NotificationController.d.ts +11 -0
- package/controllers/notifications/NotificationController.js +203 -0
- package/controllers/remove.js +2 -0
- package/files/favicon.png +0 -0
- package/helpers/diffHelpers.d.ts +2 -0
- package/helpers/diffHelpers.js +24 -0
- package/index.d.ts +2 -0
- package/index.js +2 -0
- package/interfaces/adminpanelConfig.d.ts +6 -0
- package/interfaces/types.d.ts +22 -0
- package/lib/Adminizer.d.ts +18 -2
- package/lib/Adminizer.js +61 -1
- package/lib/media-manager/AbstractMediaManager.js +2 -2
- package/lib/v4/model/adapter/sequelize.js +4 -0
- package/lib/v4/model/adapter/typeorm.d.ts +1 -0
- package/lib/v4/model/adapter/typeorm.js +21 -1
- package/lib/v4/notifications/AbstractNotificationService.d.ts +22 -0
- package/lib/v4/notifications/AbstractNotificationService.js +149 -0
- package/lib/v4/notifications/GeneralNotificationService.d.ts +8 -0
- package/lib/v4/notifications/GeneralNotificationService.js +56 -0
- package/lib/v4/notifications/NotificationHandler.d.ts +69 -0
- package/lib/v4/notifications/NotificationHandler.js +127 -0
- package/lib/v4/notifications/SystemNotificationService.d.ts +18 -0
- package/lib/v4/notifications/SystemNotificationService.js +123 -0
- package/models/NotificationAP.d.ts +36 -0
- package/models/NotificationAP.js +22 -0
- package/models/UserNotificationAP.d.ts +26 -0
- package/models/UserNotificationAP.js +18 -0
- package/package.json +13 -5
- package/system/Router.js +18 -0
- package/system/bindAssets.js +1 -0
- package/system/bindInertia.js +5 -0
- package/system/bindMediaManager.js +2 -1
- package/system/bindNavigation.js +2 -3
- package/system/bindNotifications.d.ts +2 -0
- package/system/bindNotifications.js +14 -0
- package/system/defaults.js +6 -0
- package/assets/add-DOaZgoTz.js +0 -1
- package/assets/add-group-Br44s2D6.js +0 -1
- package/assets/add-user-DWcH0s9-.js +0 -1
- package/assets/app-sidebar-header-CIKLfLsF.js +0 -1
- package/assets/form-BXfnz6h5.js +0 -1
- package/assets/home-BD4MWmm9.js +0 -2
- package/assets/input-error-Bab-HPbn.js +0 -1
- package/assets/list-C-27EH39.js +0 -1
- package/assets/material-icon-DsiW0iQG.js +0 -1
- package/assets/media-manager-CSB_XRcE.js +0 -14
package/controllers/add.js
CHANGED
|
@@ -87,6 +87,8 @@ export default async function add(req, res) {
|
|
|
87
87
|
}
|
|
88
88
|
try {
|
|
89
89
|
let record = await entity.model.create(reqData, dataAccessor);
|
|
90
|
+
// log system event notification
|
|
91
|
+
await req.adminizer.logSystemCreatedEvent(req.i18n.__('Created'), `user ${req.user.login} ${req.i18n.__('create')} ${entity.name} ${record.id}`, { test: 'test' });
|
|
90
92
|
// save associations media to json
|
|
91
93
|
await saveRelationsMediaManager(fields, rawReqData, entity.model.identity, record.id);
|
|
92
94
|
Adminizer.log.debug(`A new record was created: `, record);
|
package/controllers/edit.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { ControllerHelper } from "../helpers/controllerHelper";
|
|
2
2
|
import { RequestProcessor } from "../lib/requestProcessor";
|
|
3
3
|
import { FieldsHelper } from "../helpers/fieldsHelper";
|
|
4
|
+
import { diff } from 'deep-object-diff';
|
|
4
5
|
import { getRelationsMediaManager, saveRelationsMediaManager } from "../lib/media-manager/helpers/MediaManagerHelper";
|
|
5
6
|
import { DataAccessor } from "../lib/v4/DataAccessor";
|
|
6
7
|
import { Adminizer } from "../lib/Adminizer";
|
|
7
8
|
import inertiaAddHelper from "../helpers/inertiaAddHelper";
|
|
9
|
+
import { formatChanges, sanitizeForDiff } from "../helpers/diffHelpers";
|
|
8
10
|
export default async function edit(req, res) {
|
|
9
11
|
//Check id
|
|
10
12
|
if (!req.params.id) {
|
|
@@ -113,6 +115,18 @@ export default async function edit(req, res) {
|
|
|
113
115
|
try {
|
|
114
116
|
let newRecord = await entity.model.update(params, reqData, dataAccessor);
|
|
115
117
|
await saveRelationsMediaManager(fields, rawReqData, entity.model.identity, newRecord[0].id);
|
|
118
|
+
// Создаем clean объекты для сравнения (исключаем системные поля)
|
|
119
|
+
const cleanOldRecord = sanitizeForDiff(record);
|
|
120
|
+
const cleanNewRecord = sanitizeForDiff(newRecord[0]);
|
|
121
|
+
// Получаем diff
|
|
122
|
+
const changesDiff = diff(cleanOldRecord, cleanNewRecord);
|
|
123
|
+
// Форматируем для лога
|
|
124
|
+
const formattedChanges = formatChanges(changesDiff, cleanOldRecord, cleanNewRecord);
|
|
125
|
+
// log system event notification
|
|
126
|
+
await req.adminizer.logSystemUpdatedEvent(req.i18n.__('Updated'), `user ${req.user.login} ${req.i18n.__('update')} ${entity.name} ${record.id}`, {
|
|
127
|
+
changes: formattedChanges,
|
|
128
|
+
summary: `${req.i18n.__('Changes')} ${Object.keys(formattedChanges).length} ${req.i18n.__('fields')}`
|
|
129
|
+
});
|
|
116
130
|
Adminizer.log.debug(`Record was updated: `, newRecord);
|
|
117
131
|
if (req.body.jsonPopupCatalog) {
|
|
118
132
|
return res.json({ record: newRecord });
|
package/controllers/login.js
CHANGED
|
@@ -14,25 +14,25 @@ export default async function login(req, res) {
|
|
|
14
14
|
let login = req.body.login;
|
|
15
15
|
let password = req.body.password;
|
|
16
16
|
let captchaSolution = req.body.captchaSolution;
|
|
17
|
-
|
|
18
|
-
try {
|
|
19
|
-
// TODO refactor CRUD functions for DataAccessor usage
|
|
20
|
-
user = await req.adminizer.modelHandler.model.get("UserAP")["_findOne"]({ login: login });
|
|
21
|
-
}
|
|
22
|
-
catch (e) {
|
|
23
|
-
return res.status(500).send({ error: e.message || 'Internal Server Error' });
|
|
24
|
-
}
|
|
17
|
+
// Handle pretend (impersonation) first against internal users list
|
|
25
18
|
if (req.body.pretend) {
|
|
26
|
-
|
|
27
|
-
|
|
19
|
+
try {
|
|
20
|
+
// TODO refactor CRUD functions for DataAccessor usage
|
|
21
|
+
const pretendUser = await req.adminizer.modelHandler.model.get("UserAP")["_findOne"]({ login });
|
|
22
|
+
if (!pretendUser) {
|
|
23
|
+
return res.sendStatus(404);
|
|
24
|
+
}
|
|
25
|
+
if (req.user.isAdministrator) {
|
|
26
|
+
req.session.userPretended = pretendUser;
|
|
27
|
+
return req.Inertia.redirect(`${req.adminizer.config.routePrefix}`);
|
|
28
|
+
}
|
|
29
|
+
return req.Inertia.redirect(`${req.adminizer.config.routePrefix}/`);
|
|
28
30
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
// return res.sendStatus(200);
|
|
32
|
-
return req.Inertia.redirect(`${req.adminizer.config.routePrefix}`);
|
|
31
|
+
catch (e) {
|
|
32
|
+
return res.status(500).send({ error: e.message || 'Internal Server Error' });
|
|
33
33
|
}
|
|
34
|
-
return req.Inertia.redirect(`${req.adminizer.config.routePrefix}/`);
|
|
35
34
|
}
|
|
35
|
+
let authenticatedExternally = false;
|
|
36
36
|
// Verify CAPTCHA solution if enabled
|
|
37
37
|
if (req.adminizer.config.auth.captcha !== false) {
|
|
38
38
|
const isCaptchaValid = powCaptcha.check(captchaSolution, `login:${req.ip}`);
|
|
@@ -40,17 +40,22 @@ export default async function login(req, res) {
|
|
|
40
40
|
return inertiaAdminMessage(req, "Invalid CAPTCHA solution", 'captchaSolution');
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
// First check UserAP model (local auth)
|
|
44
|
+
let user;
|
|
45
|
+
try {
|
|
46
|
+
// TODO refactor CRUD functions for DataAccessor usage
|
|
47
|
+
user = await req.adminizer.modelHandler.model.get("UserAP")["_findOne"]({ login: login });
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
return res.status(500).send({ error: e.message || 'Internal Server Error' });
|
|
45
51
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
52
|
+
if (user) {
|
|
53
|
+
// User found in UserAP, validate password
|
|
54
|
+
if (req.adminizer.config.registration.confirmationRequired && user.isConfirmed === false && !user.isAdministrator) {
|
|
49
55
|
return inertiaAdminMessage(req, "Profile is not confirmed, please contact to administrator", 'captchaSolution');
|
|
50
56
|
}
|
|
51
57
|
if (passwordHash.verify(login + password + process.env.AP_PASSWORD_SALT, user.passwordHashed)) {
|
|
52
58
|
if (user.expires && Date.now() > Date.parse(user.expires)) {
|
|
53
|
-
//Here we use the captchaSolution key to output messages unrelated to the form fields.
|
|
54
59
|
return inertiaAdminMessage(req, "Profile expired, contact the administrator", 'captchaSolution');
|
|
55
60
|
}
|
|
56
61
|
const token = signUser(user, req.adminizer.jwtSecret);
|
|
@@ -66,6 +71,36 @@ export default async function login(req, res) {
|
|
|
66
71
|
return inertiaAdminMessage(req, "Wrong password", 'password');
|
|
67
72
|
}
|
|
68
73
|
}
|
|
74
|
+
// If user not found in UserAP and external auth handler is configured, try external auth
|
|
75
|
+
if (typeof req.adminizer.authHandler === 'function') {
|
|
76
|
+
try {
|
|
77
|
+
const extUser = await req.adminizer.authHandler(req, login, password);
|
|
78
|
+
if (!extUser) {
|
|
79
|
+
return inertiaAdminMessage(req, "Wrong username or password", 'login');
|
|
80
|
+
}
|
|
81
|
+
const externalUser = extUser;
|
|
82
|
+
if (req.adminizer.config.registration.confirmationRequired && externalUser.isConfirmed === false && !externalUser.isAdministrator) {
|
|
83
|
+
return inertiaAdminMessage(req, "Profile is not confirmed, please contact to administrator", 'captchaSolution');
|
|
84
|
+
}
|
|
85
|
+
if (externalUser.expires && Date.now() > Date.parse(externalUser.expires)) {
|
|
86
|
+
return inertiaAdminMessage(req, "Profile expired, contact the administrator", 'captchaSolution');
|
|
87
|
+
}
|
|
88
|
+
const token = signUser(externalUser, req.adminizer.jwtSecret);
|
|
89
|
+
res.setHeader('Set-Cookie', serialize('adminizer_jwt', token, {
|
|
90
|
+
httpOnly: true,
|
|
91
|
+
sameSite: 'lax',
|
|
92
|
+
path: '/',
|
|
93
|
+
maxAge: 60 * 60 * 24 * 7 * 2,
|
|
94
|
+
}));
|
|
95
|
+
return req.Inertia.redirect(`${req.adminizer.config.routePrefix}`);
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
Adminizer.log.error('External auth handler error', err);
|
|
99
|
+
return res.status(500).send({ error: 'Internal Server Error' });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// If neither UserAP nor external auth worked
|
|
103
|
+
return inertiaAdminMessage(req, "Wrong username or password", 'login');
|
|
69
104
|
}
|
|
70
105
|
if (req.method.toUpperCase() === "GET") {
|
|
71
106
|
// Generate new CAPTCHA task if enabled
|
|
@@ -9,7 +9,7 @@ export async function mediaManagerController(req, res) {
|
|
|
9
9
|
if (!req.user) {
|
|
10
10
|
return res.redirect(`${req.adminizer.config.routePrefix}/model/userap/login`);
|
|
11
11
|
}
|
|
12
|
-
else if (!req.adminizer.accessRightsHelper.hasPermission(`
|
|
12
|
+
else if (!req.adminizer.accessRightsHelper.hasPermission(`mediaManager-${id}`, req.user)) {
|
|
13
13
|
return res.sendStatus(403);
|
|
14
14
|
}
|
|
15
15
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare class NotificationController {
|
|
2
|
+
static viewAll(req: ReqType, res: ResType): Promise<void | import("express").Response<any, Record<string, any>>>;
|
|
3
|
+
static getNotificationsStream(req: ReqType, res: ResType): Promise<void>;
|
|
4
|
+
static getNotificationsByClass(req: ReqType, res: ResType): Promise<void>;
|
|
5
|
+
static getUserNotifications(req: ReqType, res: ResType): Promise<void>;
|
|
6
|
+
static getAllUserNotifications(req: ReqType, res: ResType): Promise<void>;
|
|
7
|
+
static markAsRead(req: ReqType, res: ResType): Promise<void>;
|
|
8
|
+
static sendNotification(req: ReqType, res: ResType): Promise<void>;
|
|
9
|
+
private static isAdmin;
|
|
10
|
+
private static checkPermission;
|
|
11
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { Adminizer } from '../../lib/Adminizer';
|
|
2
|
+
export class NotificationController {
|
|
3
|
+
static async viewAll(req, res) {
|
|
4
|
+
if (!req.user) {
|
|
5
|
+
return res.redirect(`${req.adminizer.config.routePrefix}/model/userap/login`);
|
|
6
|
+
}
|
|
7
|
+
return req.Inertia.render({
|
|
8
|
+
component: 'notification',
|
|
9
|
+
props: {
|
|
10
|
+
title: req.i18n.__('Notifications'),
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
// Единый SSE endpoint для всех уведомлений
|
|
15
|
+
static async getNotificationsStream(req, res) {
|
|
16
|
+
NotificationController.checkPermission(req, res);
|
|
17
|
+
// Устанавливаем заголовки для SSE
|
|
18
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
19
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
20
|
+
res.setHeader('Connection', 'keep-alive');
|
|
21
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
22
|
+
res.flushHeaders();
|
|
23
|
+
const clientId = `user-${req.user?.id}-${Date.now()}`;
|
|
24
|
+
// Функция для отправки событий клиенту
|
|
25
|
+
const sendEvent = (event) => {
|
|
26
|
+
// Фильтруем уведомления по правам пользователя
|
|
27
|
+
if (event.type === 'notification') {
|
|
28
|
+
// Системные уведомления только для админов
|
|
29
|
+
if (event.notificationClass === 'system' && !NotificationController.isAdmin(req.user)) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (event.userId !== null && event.userId !== req.user.id) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
res.write(`event: ${event.type}\n`);
|
|
37
|
+
res.write(`data: ${JSON.stringify(event.data)}\n\n`);
|
|
38
|
+
};
|
|
39
|
+
// Подключаем клиента ко всем сервисам
|
|
40
|
+
const services = req.adminizer.notificationHandler.getAllServices();
|
|
41
|
+
services.forEach(service => {
|
|
42
|
+
service.addClient(clientId, sendEvent);
|
|
43
|
+
// Для системного сервиса добавляем клиента в CRUD каналы
|
|
44
|
+
if (service.notificationClass === 'system' && NotificationController.isAdmin(req.user)) {
|
|
45
|
+
const systemService = service;
|
|
46
|
+
// Добавляем клиента в основные CRUD каналы
|
|
47
|
+
['created', 'updated', 'deleted', 'system'].forEach(channel => {
|
|
48
|
+
systemService.addClientToChannel(clientId, channel);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
// Отправляем приветственное сообщение
|
|
53
|
+
sendEvent({
|
|
54
|
+
type: 'connected',
|
|
55
|
+
data: {
|
|
56
|
+
message: 'Connected to unified notification stream',
|
|
57
|
+
clientId: clientId
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
// Отправляем историю уведомлений (с учетом прав доступа)
|
|
61
|
+
// try {
|
|
62
|
+
// const notifications = await req.adminizer.notificationHandler.getUserNotifications(
|
|
63
|
+
// req.user,
|
|
64
|
+
// 4,
|
|
65
|
+
// true
|
|
66
|
+
// );
|
|
67
|
+
// notifications.forEach(notification => {
|
|
68
|
+
// sendEvent({
|
|
69
|
+
// type: 'notification',
|
|
70
|
+
// data: notification,
|
|
71
|
+
// notificationClass: notification.notificationClass,
|
|
72
|
+
// userId: notification.userId ?? null
|
|
73
|
+
// });
|
|
74
|
+
// });
|
|
75
|
+
// } catch (error) {
|
|
76
|
+
// Adminizer.log.error('Error sending notification history:', error);
|
|
77
|
+
// }
|
|
78
|
+
// Обработка закрытия соединения
|
|
79
|
+
req.on('close', () => {
|
|
80
|
+
// Отключаем клиента от всех сервисов
|
|
81
|
+
services.forEach(service => {
|
|
82
|
+
service.removeClient(clientId);
|
|
83
|
+
// Для системного сервиса удаляем из всех каналов
|
|
84
|
+
if (service.notificationClass === 'system') {
|
|
85
|
+
const systemService = service;
|
|
86
|
+
if (systemService.removeClientFromAllChannels) {
|
|
87
|
+
systemService.removeClientFromAllChannels(clientId);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
res.end();
|
|
92
|
+
});
|
|
93
|
+
// Heartbeat для поддержания соединения
|
|
94
|
+
const heartbeatInterval = setInterval(() => {
|
|
95
|
+
if (!res.writableEnded) {
|
|
96
|
+
services.forEach(service => {
|
|
97
|
+
service.sendHeartbeat(clientId);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
clearInterval(heartbeatInterval);
|
|
102
|
+
}
|
|
103
|
+
}, 30000);
|
|
104
|
+
req.on('close', () => {
|
|
105
|
+
clearInterval(heartbeatInterval);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
// API для получения уведомлений по классу
|
|
109
|
+
static async getNotificationsByClass(req, res) {
|
|
110
|
+
NotificationController.checkPermission(req, res);
|
|
111
|
+
try {
|
|
112
|
+
const { notificationClass } = req.params;
|
|
113
|
+
const { limit = 50, unreadOnly = false } = req.query;
|
|
114
|
+
// Проверяем права доступа для системных уведомлений
|
|
115
|
+
if (notificationClass === 'system' && !NotificationController.isAdmin(req.user)) {
|
|
116
|
+
res.status(403).json({ error: 'Forbidden: Admin access required' });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const notifications = await req.adminizer.notificationHandler.getNotifications(notificationClass, req.user?.id, Number(limit), unreadOnly === 'true');
|
|
120
|
+
res.json(notifications);
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
Adminizer.log.error('Error getting notifications:', error);
|
|
124
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// API для получения всех уведомлений пользователя
|
|
128
|
+
static async getUserNotifications(req, res) {
|
|
129
|
+
NotificationController.checkPermission(req, res);
|
|
130
|
+
try {
|
|
131
|
+
const notifications = await req.adminizer.notificationHandler.getUserNotifications(req.user, 4, true);
|
|
132
|
+
res.json(notifications);
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
Adminizer.log.error('Error getting user notifications:', error);
|
|
136
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
static async getAllUserNotifications(req, res) {
|
|
140
|
+
NotificationController.checkPermission(req, res);
|
|
141
|
+
try {
|
|
142
|
+
const notifications = await req.adminizer.notificationHandler.getUserNotifications(req.user, 50, false);
|
|
143
|
+
res.json(notifications);
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
Adminizer.log.error('Error getting user notifications:', error);
|
|
147
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// API для пометки как прочитанного
|
|
151
|
+
static async markAsRead(req, res) {
|
|
152
|
+
NotificationController.checkPermission(req, res);
|
|
153
|
+
try {
|
|
154
|
+
const { notificationClass, id } = req.params;
|
|
155
|
+
// Проверяем права доступа для системных уведомлений
|
|
156
|
+
if (notificationClass === 'system' && !NotificationController.isAdmin(req.user)) {
|
|
157
|
+
res.status(403).json({ error: 'Forbidden: Admin access required' });
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
await req.adminizer.notificationHandler.markAsRead(notificationClass, id, req.user.id);
|
|
161
|
+
res.json({ success: true });
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
Adminizer.log.error('Error marking notification as read:', error);
|
|
165
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// API для отправки уведомления
|
|
169
|
+
static async sendNotification(req, res) {
|
|
170
|
+
NotificationController.checkPermission(req, res);
|
|
171
|
+
// Проверяем права админа
|
|
172
|
+
if (!NotificationController.isAdmin(req.user)) {
|
|
173
|
+
res.status(403).json({ error: 'Forbidden: Admin access required' });
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
const { title, message, type, priority, userId, channel, notificationClass = 'general' } = req.body;
|
|
178
|
+
const notificationId = await req.adminizer.notificationHandler.dispatchNotification(notificationClass, { title, message, userId, channel });
|
|
179
|
+
res.json({ success: true, id: notificationId });
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
Adminizer.log.error('Error sending notification:', error);
|
|
183
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// Проверка прав администратора
|
|
187
|
+
static isAdmin(user) {
|
|
188
|
+
if (!user)
|
|
189
|
+
return false;
|
|
190
|
+
return user.isAdministrator === true;
|
|
191
|
+
}
|
|
192
|
+
static checkPermission(req, res) {
|
|
193
|
+
if (!req.adminizer?.notificationHandler) {
|
|
194
|
+
res.status(500).json({ error: 'Notification system not initialized' });
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
// Проверяем аутентификацию
|
|
198
|
+
if (req.adminizer.config.auth.enable && !req.user) {
|
|
199
|
+
res.status(401).json({ error: 'Unauthorized' });
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
package/controllers/remove.js
CHANGED
|
@@ -67,6 +67,8 @@ export default async function remove(req, res) {
|
|
|
67
67
|
Adminizer.log.error('adminpanel > error', e);
|
|
68
68
|
}
|
|
69
69
|
if (destroyedRecord) {
|
|
70
|
+
// log system event notification
|
|
71
|
+
await req.adminizer.logSystemDeletedEvent(req.i18n.__('Deleted'), `user ${req.user.login} ${req.i18n.__('delete')} ${entity.name} ${destroyedRecord[0].id}`, { test: 'test' });
|
|
70
72
|
req.flash.setFlashMessage('success', req.i18n.__('Record was removed successfully !'));
|
|
71
73
|
}
|
|
72
74
|
else {
|
|
Binary file
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function sanitizeForDiff(data) {
|
|
2
|
+
if (!data)
|
|
3
|
+
return {};
|
|
4
|
+
const result = { ...data };
|
|
5
|
+
// Удаляем системные поля
|
|
6
|
+
const systemFields = ['id', 'createdAt', 'updatedAt', 'deletedAt', '__v', '_id'];
|
|
7
|
+
systemFields.forEach(field => delete result[field]);
|
|
8
|
+
// Очищаем чувствительные данные
|
|
9
|
+
const sensitiveFields = ['password', 'token', 'secret', 'apiKey', 'creditCard'];
|
|
10
|
+
sensitiveFields.forEach(field => {
|
|
11
|
+
if (result[field] !== undefined) {
|
|
12
|
+
result[field] = '***HIDDEN***';
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
export function formatChanges(diffObj, oldData, newData) {
|
|
18
|
+
return Object.entries(diffObj).map(([key, value]) => ({
|
|
19
|
+
field: key,
|
|
20
|
+
oldValue: oldData[key],
|
|
21
|
+
newValue: newData[key],
|
|
22
|
+
type: typeof value
|
|
23
|
+
}));
|
|
24
|
+
}
|
package/index.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export * from "./lib/v4/catalog/Navigation";
|
|
|
12
12
|
export * from "./lib/v4/catalog/CatalogHandler";
|
|
13
13
|
export * from "./lib/media-manager/MediaManagerHandler";
|
|
14
14
|
export * from "./interfaces/adminpanelConfig";
|
|
15
|
+
export * from "./interfaces/types";
|
|
16
|
+
export * from "./interfaces/MaaterialIcons";
|
|
15
17
|
export * from "./lib/v4/model/AbstractModel";
|
|
16
18
|
export * from "./lib/v4/model/adapter/waterline";
|
|
17
19
|
export * from "./lib/v4/model/adapter/typeorm";
|
package/index.js
CHANGED
|
@@ -12,6 +12,8 @@ export * from "./lib/v4/catalog/Navigation";
|
|
|
12
12
|
export * from "./lib/v4/catalog/CatalogHandler";
|
|
13
13
|
export * from "./lib/media-manager/MediaManagerHandler";
|
|
14
14
|
export * from "./interfaces/adminpanelConfig";
|
|
15
|
+
export * from "./interfaces/types";
|
|
16
|
+
export * from "./interfaces/MaaterialIcons";
|
|
15
17
|
export * from "./lib/v4/model/AbstractModel";
|
|
16
18
|
export * from "./lib/v4/model/adapter/waterline";
|
|
17
19
|
export * from "./lib/v4/model/adapter/typeorm";
|
|
@@ -173,6 +173,12 @@ export interface AdminpanelConfig {
|
|
|
173
173
|
/** Default ORM adapter for system models */
|
|
174
174
|
defaultORM: string;
|
|
175
175
|
};
|
|
176
|
+
bind?: {
|
|
177
|
+
public: boolean;
|
|
178
|
+
};
|
|
179
|
+
notification?: {
|
|
180
|
+
enabled: boolean;
|
|
181
|
+
};
|
|
176
182
|
}
|
|
177
183
|
export interface ModelConfig {
|
|
178
184
|
adapter?: string;
|
package/interfaces/types.d.ts
CHANGED
|
@@ -25,3 +25,25 @@ export interface PropsField {
|
|
|
25
25
|
isIn?: string[] | number[] | boolean[];
|
|
26
26
|
options?: Record<string, unknown> | Record<string, unknown>[];
|
|
27
27
|
}
|
|
28
|
+
export interface INotification {
|
|
29
|
+
id: string;
|
|
30
|
+
title: string;
|
|
31
|
+
message: string;
|
|
32
|
+
createdAt: Date;
|
|
33
|
+
userId?: number;
|
|
34
|
+
read?: boolean;
|
|
35
|
+
icon: {
|
|
36
|
+
icon: string;
|
|
37
|
+
iconColor: string;
|
|
38
|
+
};
|
|
39
|
+
notificationClass: string;
|
|
40
|
+
channel: string;
|
|
41
|
+
metadata?: Record<string | number, any>;
|
|
42
|
+
}
|
|
43
|
+
export interface INotificationEvent {
|
|
44
|
+
type: 'notification' | 'heartbeat' | 'connected' | 'error';
|
|
45
|
+
data: INotification | string;
|
|
46
|
+
notificationClass?: string;
|
|
47
|
+
channel?: string;
|
|
48
|
+
userId?: number;
|
|
49
|
+
}
|
package/lib/Adminizer.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Express } from "express";
|
|
1
|
+
import express, { Express } from "express";
|
|
2
2
|
import winston from "winston";
|
|
3
3
|
import EventEmitter from 'events';
|
|
4
4
|
import { AdminpanelConfig } from "../interfaces/adminpanelConfig";
|
|
@@ -12,6 +12,10 @@ import type { ViteDevServer } from 'vite';
|
|
|
12
12
|
import { MenuHelper } from "../helpers/menuHelper";
|
|
13
13
|
import { ControlsHandler } from "./v4/controls/ControlsHandler";
|
|
14
14
|
import { CatalogHandler } from "./v4/catalog/CatalogHandler";
|
|
15
|
+
import { NotificationHandler } from './v4/notifications/NotificationHandler';
|
|
16
|
+
import { INotification } from "../interfaces/types";
|
|
17
|
+
import { UserAP } from "models/UserAP";
|
|
18
|
+
export type AuthHandler = (req: ReqType, login: string, password: string) => Promise<Partial<UserAP> | null>;
|
|
15
19
|
export declare class Adminizer {
|
|
16
20
|
/**
|
|
17
21
|
* If you convey this default Middleware, it will add it to the very top of the router,
|
|
@@ -26,21 +30,33 @@ export declare class Adminizer {
|
|
|
26
30
|
accessRightsHelper: AccessRightsHelper;
|
|
27
31
|
configHelper: ConfigHelper;
|
|
28
32
|
menuHelper: MenuHelper;
|
|
33
|
+
notificationHandler: NotificationHandler;
|
|
29
34
|
modelHandler: ModelHandler;
|
|
30
35
|
widgetHandler: WidgetHandler;
|
|
31
36
|
vite: ViteDevServer;
|
|
32
37
|
controlsHandler: ControlsHandler;
|
|
33
38
|
catalogHandler: CatalogHandler;
|
|
34
39
|
jwtSecret: string;
|
|
40
|
+
authHandler?: AuthHandler;
|
|
35
41
|
static logger: winston.Logger;
|
|
36
42
|
constructor(ormAdapters: AbstractAdapter[]);
|
|
37
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Registers an external authentication handler. If a user is not found in UserAP
|
|
45
|
+
* during login, this handler will be invoked to authenticate against an external source.
|
|
46
|
+
*/
|
|
47
|
+
setAuthHandler(handler: AuthHandler): void;
|
|
48
|
+
getMiddleware(): (req: express.Request, res: express.Response, next: () => void) => void;
|
|
38
49
|
/**
|
|
39
50
|
* Vite middleware
|
|
40
51
|
* @protected
|
|
41
52
|
*/
|
|
42
53
|
protected viteMiddleware(): Promise<void>;
|
|
43
54
|
init(config: AdminpanelConfig): Promise<void>;
|
|
55
|
+
sendNotification(notification: Omit<INotification, 'id' | 'createdAt' | 'icon'>): Promise<boolean>;
|
|
56
|
+
logSystemEvent(title: string, message: string, metadata?: any): Promise<boolean>;
|
|
57
|
+
logSystemCreatedEvent(title: string, message: string, metadata?: any): Promise<boolean>;
|
|
58
|
+
logSystemUpdatedEvent(title: string, message: string, metadata?: any): Promise<boolean>;
|
|
59
|
+
logSystemDeletedEvent(title: string, message: string, metadata?: any): Promise<boolean>;
|
|
44
60
|
get emitter(): EventEmitter;
|
|
45
61
|
getOrmAdapter(ormType: string): AbstractAdapter;
|
|
46
62
|
static get log(): {
|
package/lib/Adminizer.js
CHANGED
|
@@ -29,6 +29,7 @@ import { bindControls } from "../system/bindControls";
|
|
|
29
29
|
import { ControlsHandler } from "./v4/controls/ControlsHandler";
|
|
30
30
|
import { CatalogHandler } from "./v4/catalog/CatalogHandler";
|
|
31
31
|
import { v4 as uuid } from "uuid";
|
|
32
|
+
import { bindNotifications } from "../system/bindNotifications";
|
|
32
33
|
export class Adminizer {
|
|
33
34
|
// Preconfigures
|
|
34
35
|
/**
|
|
@@ -45,6 +46,7 @@ export class Adminizer {
|
|
|
45
46
|
accessRightsHelper;
|
|
46
47
|
configHelper;
|
|
47
48
|
menuHelper;
|
|
49
|
+
notificationHandler;
|
|
48
50
|
modelHandler;
|
|
49
51
|
widgetHandler;
|
|
50
52
|
vite;
|
|
@@ -52,6 +54,8 @@ export class Adminizer {
|
|
|
52
54
|
catalogHandler;
|
|
53
55
|
// Constants
|
|
54
56
|
jwtSecret = process.env.JWT_SECRET ?? uuid();
|
|
57
|
+
// External auth handler
|
|
58
|
+
authHandler;
|
|
55
59
|
static logger = winston.createLogger({
|
|
56
60
|
level: process.env.LOG_LEVEL ?? "debug",
|
|
57
61
|
format: winston.format.combine(winston.format.timestamp(), winston.format.printf(({ timestamp, level, message, ...meta }) => {
|
|
@@ -68,8 +72,35 @@ export class Adminizer {
|
|
|
68
72
|
this._emitter = new EventEmitter();
|
|
69
73
|
this.ormAdapters = ormAdapters;
|
|
70
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Registers an external authentication handler. If a user is not found in UserAP
|
|
77
|
+
* during login, this handler will be invoked to authenticate against an external source.
|
|
78
|
+
*/
|
|
79
|
+
setAuthHandler(handler) {
|
|
80
|
+
this.authHandler = handler;
|
|
81
|
+
}
|
|
71
82
|
getMiddleware() {
|
|
72
|
-
return (req, res) => {
|
|
83
|
+
return (req, res, next) => {
|
|
84
|
+
try {
|
|
85
|
+
const prefix = (this.config && this.config.routePrefix) ? String(this.config.routePrefix) : '';
|
|
86
|
+
const normalizedPrefix = prefix.replace(/\/+/g, '/').replace(/\/+$/g, '');
|
|
87
|
+
if (normalizedPrefix) {
|
|
88
|
+
const url = req.url || req.originalUrl || '';
|
|
89
|
+
const conditions = [
|
|
90
|
+
url === normalizedPrefix,
|
|
91
|
+
url.startsWith(normalizedPrefix + '/')
|
|
92
|
+
];
|
|
93
|
+
if (process.env.IS_SAILS === undefined) {
|
|
94
|
+
conditions.push(url.startsWith('/public'));
|
|
95
|
+
}
|
|
96
|
+
if (!conditions.some(condition => condition)) {
|
|
97
|
+
return typeof next === 'function' ? next() : undefined;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
// fall back to handling the request
|
|
103
|
+
}
|
|
73
104
|
this.app(req, res, (err) => {
|
|
74
105
|
if (err) {
|
|
75
106
|
console.error("Error in Adminizer", err);
|
|
@@ -77,6 +108,9 @@ export class Adminizer {
|
|
|
77
108
|
res.end('Internal Server Error');
|
|
78
109
|
}
|
|
79
110
|
else {
|
|
111
|
+
if (typeof next === 'function') {
|
|
112
|
+
return next();
|
|
113
|
+
}
|
|
80
114
|
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
81
115
|
res.end('Route Not Found in Adminizer');
|
|
82
116
|
}
|
|
@@ -173,6 +207,9 @@ export class Adminizer {
|
|
|
173
207
|
// bind Inertia
|
|
174
208
|
bindInertia(this);
|
|
175
209
|
await bindAuthorization(this);
|
|
210
|
+
// Bind notifications
|
|
211
|
+
if (this.config.notification.enabled)
|
|
212
|
+
await bindNotifications(this);
|
|
176
213
|
await Router.bind(this); // must be after binding policies and req/res functions
|
|
177
214
|
/**
|
|
178
215
|
* Adminizer loaded
|
|
@@ -180,6 +217,29 @@ export class Adminizer {
|
|
|
180
217
|
*/
|
|
181
218
|
this._emitter.emit('adminizer:loaded');
|
|
182
219
|
}
|
|
220
|
+
// Хелпер для отправки уведомлений
|
|
221
|
+
async sendNotification(notification) {
|
|
222
|
+
const notificationClass = notification.notificationClass || 'general';
|
|
223
|
+
return this.notificationHandler.dispatchNotification(notificationClass, notification);
|
|
224
|
+
}
|
|
225
|
+
// Хелпер для системных событий
|
|
226
|
+
async logSystemEvent(title, message, metadata) {
|
|
227
|
+
const systemNotificationService = this.notificationHandler.getService('system');
|
|
228
|
+
return systemNotificationService.logSystemEvent(title, message, 'system', metadata);
|
|
229
|
+
}
|
|
230
|
+
// Хелпер для CRUD системных событий
|
|
231
|
+
async logSystemCreatedEvent(title, message, metadata) {
|
|
232
|
+
const systemNotificationService = this.notificationHandler.getService('system');
|
|
233
|
+
return systemNotificationService.logCreatedEvent(title, message, metadata);
|
|
234
|
+
}
|
|
235
|
+
async logSystemUpdatedEvent(title, message, metadata) {
|
|
236
|
+
const systemNotificationService = this.notificationHandler.getService('system');
|
|
237
|
+
return systemNotificationService.logUpdatedEvent(title, message, metadata);
|
|
238
|
+
}
|
|
239
|
+
async logSystemDeletedEvent(title, message, metadata) {
|
|
240
|
+
const systemNotificationService = this.notificationHandler.getService('system');
|
|
241
|
+
return systemNotificationService.logDeletedEvent(title, message, metadata);
|
|
242
|
+
}
|
|
183
243
|
get emitter() {
|
|
184
244
|
return this._emitter;
|
|
185
245
|
}
|
|
@@ -50,8 +50,8 @@ export class AbstractMediaManager {
|
|
|
50
50
|
adminizer.accessRightsHelper.registerToken({
|
|
51
51
|
id: `mediaManager-${this.id}`,
|
|
52
52
|
name: this.id,
|
|
53
|
-
description: `Access to edit
|
|
54
|
-
department: '
|
|
53
|
+
description: `Access to edit media-manager for ${this.id}`,
|
|
54
|
+
department: 'media-manager',
|
|
55
55
|
});
|
|
56
56
|
}, 100);
|
|
57
57
|
}
|