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
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
  }
@@ -170,6 +170,10 @@ export class SequelizeModel extends AbstractModel {
170
170
  if (value === null) {
171
171
  result[targetKey] = { [Op.is]: null };
172
172
  }
173
+ else if (Array.isArray(value)) {
174
+ // ✅ Обработка массивов - используем оператор IN
175
+ result[targetKey] = { [Op.in]: value };
176
+ }
173
177
  else if (typeof value === "object" && !Array.isArray(value)) {
174
178
  const operatorEntries = Object.entries(value)
175
179
  .filter(([_, v]) => v !== undefined && v !== null)
@@ -9,6 +9,7 @@ export declare class TypeORMModel<T> extends AbstractModel<T> {
9
9
  protected _destroy(criteria: Partial<T>): Promise<T[]>;
10
10
  protected _destroyOne(criteria: Partial<T>): Promise<T | null>;
11
11
  protected _find(criteria: Partial<T>, options: FindOptions | undefined): Promise<T[]>;
12
+ private _convertCriteriaForTypeORM;
12
13
  protected _findOne(criteria: Partial<T>): Promise<T | null>;
13
14
  protected _update(criteria: Partial<T>, data: Partial<T>): Promise<T[]>;
14
15
  protected _updateOne(criteria: Partial<T>, data: Partial<T>): Promise<T | null>;
@@ -27,7 +27,27 @@ export class TypeORMModel extends AbstractModel {
27
27
  return null;
28
28
  }
29
29
  async _find(criteria, options) {
30
- return this.repository.find({ where: criteria, ...options });
30
+ // Преобразуем критерии для поддержки массивов
31
+ const where = this._convertCriteriaForTypeORM(criteria);
32
+ return this.repository.find({ where, ...options });
33
+ }
34
+ _convertCriteriaForTypeORM(criteria) {
35
+ const result = {};
36
+ for (const key in criteria) {
37
+ const value = criteria[key];
38
+ if (Array.isArray(value)) {
39
+ // Для массивов используем оператор IN
40
+ result[key] = { $in: value };
41
+ }
42
+ else if (typeof value === 'object' && value !== null) {
43
+ // Обработка вложенных объектов с операторами
44
+ result[key] = value;
45
+ }
46
+ else {
47
+ result[key] = value;
48
+ }
49
+ }
50
+ return result;
31
51
  }
32
52
  async _findOne(criteria) {
33
53
  return this.repository.findOne({ where: criteria });
@@ -0,0 +1,22 @@
1
+ import { EventEmitter } from 'events';
2
+ import { Adminizer } from '../../Adminizer';
3
+ import { INotification, INotificationEvent } from '../../../interfaces/types';
4
+ export declare abstract class AbstractNotificationService extends EventEmitter {
5
+ protected clients: Map<string, (event: INotificationEvent) => void>;
6
+ protected adminizer: Adminizer;
7
+ abstract readonly notificationClass: string;
8
+ abstract readonly icon: string;
9
+ abstract readonly iconColor: string;
10
+ constructor(adminizer: Adminizer);
11
+ addClient(clientId: string, sendFn: (event: INotificationEvent) => void): void;
12
+ removeClient(clientId: string): void;
13
+ abstract dispatchNotification(notification: Omit<INotification, 'id' | 'createdAt' | 'notificationClass' | 'icon'>): Promise<boolean>;
14
+ protected broadcast(event: INotificationEvent): void;
15
+ sendHeartbeat(clientId: string): void;
16
+ protected generateId(): string;
17
+ getClientCount(): number;
18
+ protected createUserNotification(notificationId: string, userId?: number): Promise<void>;
19
+ protected getUserNotification(notificationId: string, userId: number): Promise<any>;
20
+ getNotifications(userId: number, limit?: number, unreadOnly?: boolean): Promise<INotification[]>;
21
+ markAsRead(userId: number, id: string): Promise<void>;
22
+ }
@@ -0,0 +1,149 @@
1
+ import { EventEmitter } from 'events';
2
+ import { Adminizer } from '../../Adminizer';
3
+ export class AbstractNotificationService extends EventEmitter {
4
+ clients = new Map();
5
+ adminizer;
6
+ constructor(adminizer) {
7
+ super();
8
+ this.adminizer = adminizer;
9
+ }
10
+ // Добавление клиента
11
+ addClient(clientId, sendFn) {
12
+ this.clients.set(clientId, sendFn);
13
+ Adminizer.log.info(`[${this.notificationClass}] Client ${clientId} connected. Total: ${this.clients.size}`);
14
+ }
15
+ // Удаление клиента
16
+ removeClient(clientId) {
17
+ this.clients.delete(clientId);
18
+ Adminizer.log.info(`[${this.notificationClass}] Client ${clientId} disconnected. Total: ${this.clients.size}`);
19
+ }
20
+ // abstract getNotifications(userId?: number, limit?: number, unreadOnly?: boolean): Promise<INotification[]>;
21
+ // Рассылка события всем клиентам
22
+ broadcast(event) {
23
+ this.clients.forEach((sendFn, clientId) => {
24
+ try {
25
+ sendFn(event);
26
+ }
27
+ catch (error) {
28
+ Adminizer.log.error(`[${this.notificationClass}] Error sending to client ${clientId}:`, error);
29
+ this.removeClient(clientId);
30
+ }
31
+ });
32
+ }
33
+ // Отправка heartbeat
34
+ sendHeartbeat(clientId) {
35
+ const sendFn = this.clients.get(clientId);
36
+ if (sendFn) {
37
+ const heartbeatEvent = {
38
+ type: 'heartbeat',
39
+ data: 'ping'
40
+ };
41
+ sendFn(heartbeatEvent);
42
+ }
43
+ }
44
+ // Генерация ID
45
+ generateId() {
46
+ return `${this.notificationClass}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
47
+ }
48
+ // Получение количества подключенных клиентов
49
+ getClientCount() {
50
+ return this.clients.size;
51
+ }
52
+ // Метод для создания записи в UserNotificationAP
53
+ async createUserNotification(notificationId, userId) {
54
+ if (this.adminizer.modelHandler.model.has('usernotificationap') && userId) {
55
+ try {
56
+ await this.adminizer.modelHandler.model.get('usernotificationap')["_create"]({
57
+ userId: userId,
58
+ notificationId: notificationId,
59
+ read: false
60
+ });
61
+ }
62
+ catch (error) {
63
+ Adminizer.log.error(`Error creating user notification record:`, error);
64
+ }
65
+ }
66
+ }
67
+ // Метод для получения UserNotificationAP по ID уведомления и пользователя
68
+ async getUserNotification(notificationId, userId) {
69
+ if (this.adminizer.modelHandler.model.has('usernotificationap')) {
70
+ try {
71
+ return await this.adminizer.modelHandler.model.get('usernotificationap')["_findOne"]({
72
+ where: { notificationId: notificationId, userId: userId }
73
+ });
74
+ }
75
+ catch (error) {
76
+ Adminizer.log.error(`Error getting user notification:`, error);
77
+ return null;
78
+ }
79
+ }
80
+ return null;
81
+ }
82
+ async getNotifications(userId, limit = 50, unreadOnly = false) {
83
+ if (!this.adminizer.modelHandler.model.has('notificationap')) {
84
+ return [];
85
+ }
86
+ try {
87
+ let query = { notificationClass: this.notificationClass };
88
+ // Если запрашиваются уведомления для конкретного пользователя
89
+ // Получаем ID уведомлений пользователя
90
+ const userNotifications = await this.adminizer.modelHandler.model.get('usernotificationap')["_find"]({
91
+ where: {
92
+ userId: userId
93
+ },
94
+ include: ['notificationap']
95
+ });
96
+ const notificationIds = userNotifications.map((un) => un.notificationId.id);
97
+ if (unreadOnly) {
98
+ // Только непрочитанные
99
+ query.id = userNotifications
100
+ .filter((un) => !un.read)
101
+ .map((un) => un.notificationId.id);
102
+ }
103
+ else {
104
+ query.id = notificationIds;
105
+ }
106
+ let notificationsDB;
107
+ if (query.id.length === 0)
108
+ return [];
109
+ notificationsDB = await this.adminizer.modelHandler.model.get('notificationap')["_find"]({
110
+ where: query,
111
+ sort: 'createdAt DESC',
112
+ limit: limit
113
+ });
114
+ let notifications = [];
115
+ for (const notification of notificationsDB) {
116
+ let readStatus = false;
117
+ // Получаем статус прочтения из UserNotificationAP
118
+ const userNotification = await this.getUserNotification(notification.id, userId);
119
+ readStatus = userNotification ? userNotification.read : false;
120
+ notifications.push({
121
+ ...notification,
122
+ read: readStatus,
123
+ icon: {
124
+ icon: this.icon,
125
+ iconColor: this.iconColor
126
+ },
127
+ });
128
+ }
129
+ return notifications;
130
+ }
131
+ catch (error) {
132
+ Adminizer.log.error('Error fetching notifications:', error);
133
+ return [];
134
+ }
135
+ }
136
+ async markAsRead(userId, id) {
137
+ try {
138
+ const userNotification = await this.getUserNotification(id, userId);
139
+ if (userNotification) {
140
+ await this.adminizer.modelHandler.model.get('usernotificationap')["_update"]({
141
+ id: userNotification.id
142
+ }, { read: true });
143
+ }
144
+ }
145
+ catch (error) {
146
+ Adminizer.log.error('Error marking notification as read:', error);
147
+ }
148
+ }
149
+ }
@@ -0,0 +1,8 @@
1
+ import { AbstractNotificationService } from './AbstractNotificationService';
2
+ import { INotification } from '../../../interfaces/types';
3
+ export declare class GeneralNotificationService extends AbstractNotificationService {
4
+ readonly notificationClass = "general";
5
+ readonly icon: string;
6
+ readonly iconColor: string;
7
+ dispatchNotification(notification: Omit<INotification, 'id' | 'createdAt' | 'notificationClass' | 'icon'>): Promise<boolean>;
8
+ }
@@ -0,0 +1,56 @@
1
+ import { AbstractNotificationService } from './AbstractNotificationService';
2
+ import { Adminizer } from '../../Adminizer';
3
+ export class GeneralNotificationService extends AbstractNotificationService {
4
+ notificationClass = 'general';
5
+ icon = 'info';
6
+ iconColor = '#5987de';
7
+ async dispatchNotification(notification) {
8
+ const fullNotification = {
9
+ ...notification,
10
+ notificationClass: this.notificationClass
11
+ };
12
+ let notificationDB;
13
+ // Сохраняем в базу
14
+ if (this.adminizer.modelHandler.model.has('notificationap')) {
15
+ try {
16
+ notificationDB = await this.adminizer.modelHandler.model.get('notificationap')["_create"](fullNotification);
17
+ if (notification.userId) {
18
+ // For specific user
19
+ await this.createUserNotification(notificationDB.id, notification.userId);
20
+ }
21
+ else {
22
+ // For all users
23
+ const users = await this.adminizer.modelHandler.model.get('userap')["_find"]({});
24
+ for (const user of users) {
25
+ try {
26
+ await this.createUserNotification(notificationDB.id, user.id);
27
+ }
28
+ catch (error) {
29
+ Adminizer.log.error('Error creating UserNotificationAP:', error);
30
+ }
31
+ }
32
+ }
33
+ const event = {
34
+ type: 'notification',
35
+ data: {
36
+ ...notificationDB.toJSON(),
37
+ read: false,
38
+ icon: {
39
+ icon: this.icon,
40
+ iconColor: this.iconColor
41
+ },
42
+ },
43
+ notificationClass: this.notificationClass,
44
+ userId: notification.userId ?? null
45
+ };
46
+ this.broadcast(event);
47
+ Adminizer.log.info(`[General] Notification dispatched: ${fullNotification.title}`);
48
+ return true;
49
+ }
50
+ catch (error) {
51
+ Adminizer.log.error('Error saving notification to database:', error);
52
+ }
53
+ }
54
+ return false;
55
+ }
56
+ }
@@ -0,0 +1,69 @@
1
+ import { EventEmitter } from 'events';
2
+ import { AbstractNotificationService } from "./AbstractNotificationService";
3
+ import { INotification } from '../../../interfaces/types';
4
+ export declare class NotificationHandler extends EventEmitter {
5
+ private services;
6
+ constructor();
7
+ /**
8
+ * Register a new notification service
9
+ * @param service
10
+ */
11
+ registerService(service: AbstractNotificationService): void;
12
+ /**
13
+ * Remove a notification service
14
+ * @param notificationClass
15
+ */
16
+ removeService(notificationClass: string): void;
17
+ /**
18
+ * Get a notification service by class
19
+ * @param notificationClass
20
+ */
21
+ getService(notificationClass: string): AbstractNotificationService | undefined;
22
+ /**
23
+ * Get all registered services
24
+ */
25
+ getAllServices(): AbstractNotificationService[];
26
+ /**
27
+ * Dispatch a notification to a service
28
+ * @param notificationClass
29
+ * @param notification
30
+ */
31
+ dispatchNotification(notificationClass: string, notification: Omit<INotification, 'id' | 'createdAt' | 'notificationClass' | 'icon'>): Promise<boolean>;
32
+ /**
33
+ * Get notifications by class
34
+ * @param notificationClass
35
+ * @param userId
36
+ * @param limit
37
+ * @param unreadOnly
38
+ */
39
+ getNotifications(notificationClass: string, userId?: number, limit?: number, unreadOnly?: boolean): Promise<INotification[]>;
40
+ /**
41
+ * Get all notifications for a user
42
+ * @param user
43
+ * @param limit
44
+ * @param unreadOnly
45
+ */
46
+ getUserNotifications(user: any, limit?: number, unreadOnly?: boolean): Promise<INotification[]>;
47
+ /**
48
+ * Mark a notification as read
49
+ * @param notificationClass
50
+ * @param id
51
+ * @param userId
52
+ */
53
+ markAsRead(notificationClass: string, id: string, userId: number): Promise<void>;
54
+ /**
55
+ * Check if user is admin
56
+ * @param user
57
+ * @private
58
+ */
59
+ private isAdmin;
60
+ /**
61
+ * Get client count for a notification class
62
+ * @param notificationClass
63
+ */
64
+ getClientCount(notificationClass: string): number;
65
+ /**
66
+ * Get total client count
67
+ */
68
+ getTotalClientCount(): number;
69
+ }
@@ -0,0 +1,127 @@
1
+ import { EventEmitter } from 'events';
2
+ import { Adminizer } from '../../Adminizer';
3
+ export class NotificationHandler extends EventEmitter {
4
+ services = new Map();
5
+ constructor() {
6
+ super();
7
+ }
8
+ /**
9
+ * Register a new notification service
10
+ * @param service
11
+ */
12
+ registerService(service) {
13
+ this.services.set(service.notificationClass, service);
14
+ Adminizer.log.info(`Notification service registered: ${service.notificationClass}`);
15
+ }
16
+ /**
17
+ * Remove a notification service
18
+ * @param notificationClass
19
+ */
20
+ removeService(notificationClass) {
21
+ if (this.services.has(notificationClass)) {
22
+ this.services.delete(notificationClass);
23
+ Adminizer.log.info(`Notification service removed: ${notificationClass}`);
24
+ }
25
+ }
26
+ /**
27
+ * Get a notification service by class
28
+ * @param notificationClass
29
+ */
30
+ getService(notificationClass) {
31
+ return this.services.get(notificationClass);
32
+ }
33
+ /**
34
+ * Get all registered services
35
+ */
36
+ getAllServices() {
37
+ return Array.from(this.services.values());
38
+ }
39
+ /**
40
+ * Dispatch a notification to a service
41
+ * @param notificationClass
42
+ * @param notification
43
+ */
44
+ async dispatchNotification(notificationClass, notification) {
45
+ const service = this.getService(notificationClass);
46
+ if (!service) {
47
+ throw new Error(`Notification service not found: ${notificationClass}`);
48
+ }
49
+ return service.dispatchNotification(notification);
50
+ }
51
+ /**
52
+ * Get notifications by class
53
+ * @param notificationClass
54
+ * @param userId
55
+ * @param limit
56
+ * @param unreadOnly
57
+ */
58
+ async getNotifications(notificationClass, userId, limit, unreadOnly) {
59
+ const service = this.getService(notificationClass);
60
+ if (!service) {
61
+ throw new Error(`Notification service not found: ${notificationClass}`);
62
+ }
63
+ return service.getNotifications(userId, limit, unreadOnly);
64
+ }
65
+ /**
66
+ * Get all notifications for a user
67
+ * @param user
68
+ * @param limit
69
+ * @param unreadOnly
70
+ */
71
+ async getUserNotifications(user, limit = 50, unreadOnly = false) {
72
+ const allNotifications = [];
73
+ for (const service of this.getAllServices()) {
74
+ // Для системных уведомлений проверяем права админа
75
+ if (service.notificationClass === 'system') {
76
+ if (this.isAdmin(user)) {
77
+ const notifications = await service.getNotifications(user.id, limit, unreadOnly);
78
+ allNotifications.push(...notifications);
79
+ }
80
+ }
81
+ else {
82
+ // Для остальных уведомлений получаем по пользователю
83
+ const notifications = await service.getNotifications(user.id, limit, unreadOnly);
84
+ allNotifications.push(...notifications);
85
+ }
86
+ }
87
+ // Сортируем по дате создания
88
+ return allNotifications.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()).slice(0, limit);
89
+ }
90
+ /**
91
+ * Mark a notification as read
92
+ * @param notificationClass
93
+ * @param id
94
+ * @param userId
95
+ */
96
+ async markAsRead(notificationClass, id, userId) {
97
+ const service = this.getService(notificationClass);
98
+ if (!service) {
99
+ throw new Error(`Notification service not found: ${notificationClass}`);
100
+ }
101
+ return service.markAsRead(userId, id);
102
+ }
103
+ /**
104
+ * Check if user is admin
105
+ * @param user
106
+ * @private
107
+ */
108
+ isAdmin(user) {
109
+ if (!user)
110
+ return false;
111
+ return user.isAdministrator === true;
112
+ }
113
+ /**
114
+ * Get client count for a notification class
115
+ * @param notificationClass
116
+ */
117
+ getClientCount(notificationClass) {
118
+ const service = this.getService(notificationClass);
119
+ return service ? service.getClientCount() : 0;
120
+ }
121
+ /**
122
+ * Get total client count
123
+ */
124
+ getTotalClientCount() {
125
+ return this.getAllServices().reduce((total, service) => total + service.getClientCount(), 0);
126
+ }
127
+ }
@@ -0,0 +1,18 @@
1
+ import { AbstractNotificationService } from './AbstractNotificationService';
2
+ import { INotification } from '../../../interfaces/types';
3
+ export declare class SystemNotificationService extends AbstractNotificationService {
4
+ readonly notificationClass = "system";
5
+ readonly icon: string;
6
+ readonly iconColor: string;
7
+ private crudChannels;
8
+ dispatchNotification(notification: Omit<INotification, 'id' | 'createdAt' | 'notificationClass' | 'icon'>): Promise<boolean>;
9
+ private broadcastToChannel;
10
+ addClientToChannel(clientId: string, channel: string): void;
11
+ removeClientFromChannel(clientId: string, channel: string): void;
12
+ removeClientFromAllChannels(clientId: string): void;
13
+ removeClient(clientId: string): void;
14
+ logSystemEvent(title: string, message: string, channel?: string, metadata?: Record<string | number, any>): Promise<boolean>;
15
+ logCreatedEvent(title: string, message: string, metadata?: Record<string | number, any>): Promise<boolean>;
16
+ logUpdatedEvent(title: string, message: string, metadata?: Record<string | number, any>): Promise<boolean>;
17
+ logDeletedEvent(title: string, message: string, metadata?: Record<string | number, any>): Promise<boolean>;
18
+ }