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.
Files changed (93) hide show
  1. package/assets/add-BNb5A_qF.js +1 -0
  2. package/assets/{add-form-BjackXJe.js → add-form-CGi_qv3J.js} +1 -1
  3. package/assets/add-group-RRjHTu93.js +1 -0
  4. package/assets/{add-group-form-YI6KS37x.js → add-group-form-DSd4UrnU.js} +1 -1
  5. package/assets/add-user-DSuvcKp6.js +1 -0
  6. package/assets/{add-user-form-bv5BNX5A.js → add-user-form-BAssKDpI.js} +1 -1
  7. package/assets/{app-CVnUv8s_.css → app-GsYdx39W.css} +1 -1
  8. package/assets/{app-layout-DecN527i.js → app-layout-ContoN1K.js} +1 -1
  9. package/assets/{app-sidebar-4JFP8UdU.js → app-sidebar-D7l5BJPV.js} +1 -1
  10. package/assets/app-sidebar-header-CK8t32cm.js +1 -0
  11. package/assets/app.js +13 -13
  12. package/assets/{breadcrumbs-Bo2z-LNC.js → breadcrumbs-DnNOEnT4.js} +1 -1
  13. package/assets/{catalog-DVyrGTNR.js → catalog-BavcTVb4.js} +1 -1
  14. package/assets/{dashboard-Dy3pk9t7.js → dashboard-xr-OyVyG.js} +1 -1
  15. package/assets/{date-picker-B2z-aeuv.js → date-picker-DBHnomTY.js} +1 -1
  16. package/assets/{del-modal-l3LxJWCN.js → del-modal-BeF4r3gk.js} +1 -1
  17. package/assets/{field-renderer-BHo944Cn.js → field-renderer-Sd68OLyc.js} +3 -3
  18. package/assets/form-C9P9m_9q.js +1 -0
  19. package/assets/{form-hP8sjbND.js → form-DomTBI89.js} +1 -1
  20. package/assets/home-BWHeZm-1.js +2 -0
  21. package/assets/{icon-C_X2cQ6W.js → icon-ChQBPA5l.js} +1 -1
  22. package/assets/{init-user-DgCD4ZN1.js → init-user-DqMDju_E.js} +1 -1
  23. package/assets/input-error-g1h6ps7t.js +1 -0
  24. package/assets/list-EEgAn-nx.js +1 -0
  25. package/assets/{list-table-_OiQmr2A.js → list-table-Br0K3rXg.js} +1 -1
  26. package/assets/{login-jzvVXgAV.js → login-BR9cqsPn.js} +1 -1
  27. package/assets/manifest.json +97 -64
  28. package/assets/material-icon-BIcI8APG.js +1 -0
  29. package/assets/media-manager-CTstvyHm.js +14 -0
  30. package/assets/{module-Ceq6vuwa.js → module-CpZBs0fN.js} +1 -1
  31. package/assets/{multi-select-BbKSrMw2.js → multi-select-BFWbEjH-.js} +1 -1
  32. package/assets/{nav-footer-B8gMV6Wp.js → nav-footer-bO8D-Yir.js} +1 -1
  33. package/assets/{nav-main-BUp9qA8R.js → nav-main-DVgvko8g.js} +1 -1
  34. package/assets/{nav-user-DeF3SDRj.js → nav-user-vIwYShwE.js} +1 -1
  35. package/assets/notification-BTXEslqa.js +1 -0
  36. package/assets/{pagination-render-B9U_hVbB.js → pagination-render-rZ5_6g0n.js} +1 -1
  37. package/assets/{register-C6acWsLe.js → register-Kfu8zmmu.js} +1 -1
  38. package/assets/tabs-CwgzWt48.js +1 -0
  39. package/assets/{theme-switcher-D8T9NgWG.js → theme-switcher-fzVA8btX.js} +1 -1
  40. package/assets/{user-info-CAgkqARx.js → user-info-Db5Y8KeN.js} +1 -1
  41. package/assets/{user-menu-content-BxL0QMC9.js → user-menu-content-Bc8YNZPf.js} +1 -1
  42. package/assets/{welcome-Dk7GoS5R.js → welcome-MKPFhPtG.js} +1 -1
  43. package/controllers/add.js +2 -0
  44. package/controllers/edit.js +14 -0
  45. package/controllers/login.js +56 -21
  46. package/controllers/media-manager/mediaManagerApi.js +1 -1
  47. package/controllers/notifications/NotificationController.d.ts +11 -0
  48. package/controllers/notifications/NotificationController.js +203 -0
  49. package/controllers/remove.js +2 -0
  50. package/files/favicon.png +0 -0
  51. package/helpers/diffHelpers.d.ts +2 -0
  52. package/helpers/diffHelpers.js +24 -0
  53. package/index.d.ts +2 -0
  54. package/index.js +2 -0
  55. package/interfaces/adminpanelConfig.d.ts +6 -0
  56. package/interfaces/types.d.ts +22 -0
  57. package/lib/Adminizer.d.ts +18 -2
  58. package/lib/Adminizer.js +61 -1
  59. package/lib/media-manager/AbstractMediaManager.js +2 -2
  60. package/lib/v4/model/adapter/sequelize.js +4 -0
  61. package/lib/v4/model/adapter/typeorm.d.ts +1 -0
  62. package/lib/v4/model/adapter/typeorm.js +21 -1
  63. package/lib/v4/notifications/AbstractNotificationService.d.ts +22 -0
  64. package/lib/v4/notifications/AbstractNotificationService.js +149 -0
  65. package/lib/v4/notifications/GeneralNotificationService.d.ts +8 -0
  66. package/lib/v4/notifications/GeneralNotificationService.js +56 -0
  67. package/lib/v4/notifications/NotificationHandler.d.ts +69 -0
  68. package/lib/v4/notifications/NotificationHandler.js +127 -0
  69. package/lib/v4/notifications/SystemNotificationService.d.ts +18 -0
  70. package/lib/v4/notifications/SystemNotificationService.js +123 -0
  71. package/models/NotificationAP.d.ts +36 -0
  72. package/models/NotificationAP.js +22 -0
  73. package/models/UserNotificationAP.d.ts +26 -0
  74. package/models/UserNotificationAP.js +18 -0
  75. package/package.json +13 -5
  76. package/system/Router.js +18 -0
  77. package/system/bindAssets.js +1 -0
  78. package/system/bindInertia.js +5 -0
  79. package/system/bindMediaManager.js +2 -1
  80. package/system/bindNavigation.js +2 -3
  81. package/system/bindNotifications.d.ts +2 -0
  82. package/system/bindNotifications.js +14 -0
  83. package/system/defaults.js +6 -0
  84. package/assets/add-DOaZgoTz.js +0 -1
  85. package/assets/add-group-Br44s2D6.js +0 -1
  86. package/assets/add-user-DWcH0s9-.js +0 -1
  87. package/assets/app-sidebar-header-CIKLfLsF.js +0 -1
  88. package/assets/form-BXfnz6h5.js +0 -1
  89. package/assets/home-BD4MWmm9.js +0 -2
  90. package/assets/input-error-Bab-HPbn.js +0 -1
  91. package/assets/list-C-27EH39.js +0 -1
  92. package/assets/material-icon-DsiW0iQG.js +0 -1
  93. package/assets/media-manager-CSB_XRcE.js +0 -14
@@ -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);
@@ -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 });
@@ -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
- let user;
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
- if (!user) {
27
- return res.sendStatus(404);
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
- if (req.user.isAdministrator) {
30
- req.session.userPretended = user;
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
- if (!user) {
44
- return inertiaAdminMessage(req, "Wrong username", 'login');
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
- else {
47
- if (req.adminizer.config.registration.confirmationRequired && !user.isConfirmed && !user.isAdministrator) {
48
- //Here we use the captchaSolution key to output messages unrelated to the form fields.
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(`catalog-${id}`, req.user)) {
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
+ }
@@ -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,2 @@
1
+ export declare function sanitizeForDiff(data: any): any;
2
+ export declare function formatChanges(diffObj: any, oldData: any, newData: any): any[];
@@ -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;
@@ -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
+ }
@@ -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
- getMiddleware(): (req: any, res: any) => void;
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 catalog for ${this.id}`,
54
- department: 'catalog'
53
+ description: `Access to edit media-manager for ${this.id}`,
54
+ department: 'media-manager',
55
55
  });
56
56
  }, 100);
57
57
  }