adminizer 4.2.0 → 4.3.0-build.74

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 (109) 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-CUDpGsHl.js +1 -0
  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/helpers/inertiaAutHelper.js +19 -0
  54. package/index.d.ts +2 -0
  55. package/index.js +2 -0
  56. package/interfaces/adminpanelConfig.d.ts +18 -0
  57. package/interfaces/types.d.ts +22 -0
  58. package/lib/Adminizer.d.ts +18 -2
  59. package/lib/Adminizer.js +61 -1
  60. package/lib/media-manager/AbstractMediaManager.js +2 -2
  61. package/lib/v4/model/adapter/sequelize.js +4 -0
  62. package/lib/v4/model/adapter/typeorm.d.ts +1 -0
  63. package/lib/v4/model/adapter/typeorm.js +21 -1
  64. package/lib/v4/notifications/AbstractNotificationService.d.ts +22 -0
  65. package/lib/v4/notifications/AbstractNotificationService.js +149 -0
  66. package/lib/v4/notifications/GeneralNotificationService.d.ts +8 -0
  67. package/lib/v4/notifications/GeneralNotificationService.js +56 -0
  68. package/lib/v4/notifications/NotificationHandler.d.ts +69 -0
  69. package/lib/v4/notifications/NotificationHandler.js +127 -0
  70. package/lib/v4/notifications/SystemNotificationService.d.ts +18 -0
  71. package/lib/v4/notifications/SystemNotificationService.js +123 -0
  72. package/models/NotificationAP.d.ts +36 -0
  73. package/models/NotificationAP.js +22 -0
  74. package/models/UserNotificationAP.d.ts +26 -0
  75. package/models/UserNotificationAP.js +18 -0
  76. package/package.json +13 -5
  77. package/system/Router.js +18 -0
  78. package/system/bindAssets.js +1 -0
  79. package/system/bindInertia.js +5 -0
  80. package/system/bindMediaManager.js +2 -1
  81. package/system/bindNavigation.js +2 -3
  82. package/system/bindNotifications.d.ts +2 -0
  83. package/system/bindNotifications.js +14 -0
  84. package/system/defaults.js +6 -0
  85. package/translations/ar.json +5 -4
  86. package/translations/cn.json +5 -4
  87. package/translations/de.json +72 -71
  88. package/translations/en.json +5 -4
  89. package/translations/es.json +5 -4
  90. package/translations/fr.json +5 -4
  91. package/translations/hi.json +5 -4
  92. package/translations/jp.json +5 -4
  93. package/translations/ko.json +5 -4
  94. package/translations/pt.json +5 -4
  95. package/translations/ru.json +2 -1
  96. package/translations/ua.json +5 -4
  97. package/translations/uz.json +73 -72
  98. package/translations/vi.json +5 -4
  99. package/assets/add-DOaZgoTz.js +0 -1
  100. package/assets/add-group-Br44s2D6.js +0 -1
  101. package/assets/add-user-DWcH0s9-.js +0 -1
  102. package/assets/app-sidebar-header-CIKLfLsF.js +0 -1
  103. package/assets/form-BXfnz6h5.js +0 -1
  104. package/assets/home-BD4MWmm9.js +0 -2
  105. package/assets/input-error-Bab-HPbn.js +0 -1
  106. package/assets/list-C-27EH39.js +0 -1
  107. package/assets/login-jzvVXgAV.js +0 -1
  108. package/assets/material-icon-DsiW0iQG.js +0 -1
  109. 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
+ }
@@ -11,6 +11,25 @@ export function inertiaLoginHelper(req) {
11
11
  link: `${req.adminizer.config.routePrefix}/model/userap/register`
12
12
  };
13
13
  }
14
+ // Optional additional link at the bottom of login page
15
+ const addPage = req.adminizer.config.auth?.addishinalLoginPage;
16
+ if (addPage && typeof addPage.link === 'string' && addPage.link.length > 0) {
17
+ const raw = addPage.link;
18
+ let link = raw;
19
+ if (/^https?:\/\//i.test(raw)) {
20
+ link = raw; // absolute URL
21
+ }
22
+ else if (raw.startsWith(`/${req.adminizer.config.routePrefix}`)) {
23
+ link = `${raw}`;
24
+ }
25
+ else {
26
+ link = `${req.adminizer.config.routePrefix}/${raw}`;
27
+ }
28
+ props.addishinalLoginPage = {
29
+ title: req.i18n.__(addPage.textKey || 'Additional login page'),
30
+ link,
31
+ };
32
+ }
14
33
  return props;
15
34
  }
16
35
  export function inertiaRegisterHelper(req) {
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";
@@ -36,6 +36,18 @@ export interface AdminpanelConfig {
36
36
  captcha?: boolean;
37
37
  /** Description displayed on login page */
38
38
  description?: string;
39
+ /**
40
+ * Optional additional link at the bottom of the login page
41
+ * Example:
42
+ * addishinalLoginPage: { link: '/model/userap/register', textKey: 'Additional login page' }
43
+ * link can be relative to routePrefix (e.g. 'model/help' or '/model/help') or absolute (https://...)
44
+ */
45
+ addishinalLoginPage?: {
46
+ /** Target URL; relative to routePrefix unless absolute */
47
+ link: string;
48
+ /** Optional i18n key for label; defaults to 'Additional login page' */
49
+ textKey?: string;
50
+ };
39
51
  };
40
52
  /**
41
53
  * @alpha
@@ -173,6 +185,12 @@ export interface AdminpanelConfig {
173
185
  /** Default ORM adapter for system models */
174
186
  defaultORM: string;
175
187
  };
188
+ bind?: {
189
+ public: boolean;
190
+ };
191
+ notification?: {
192
+ enabled: boolean;
193
+ };
176
194
  }
177
195
  export interface ModelConfig {
178
196
  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(): {