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.
- package/assets/add-BNb5A_qF.js +1 -0
- package/assets/{add-form-BjackXJe.js → add-form-CGi_qv3J.js} +1 -1
- package/assets/add-group-RRjHTu93.js +1 -0
- package/assets/{add-group-form-YI6KS37x.js → add-group-form-DSd4UrnU.js} +1 -1
- package/assets/add-user-DSuvcKp6.js +1 -0
- package/assets/{add-user-form-bv5BNX5A.js → add-user-form-BAssKDpI.js} +1 -1
- package/assets/{app-CVnUv8s_.css → app-GsYdx39W.css} +1 -1
- package/assets/{app-layout-DecN527i.js → app-layout-ContoN1K.js} +1 -1
- package/assets/{app-sidebar-4JFP8UdU.js → app-sidebar-D7l5BJPV.js} +1 -1
- package/assets/app-sidebar-header-CK8t32cm.js +1 -0
- package/assets/app.js +13 -13
- package/assets/{breadcrumbs-Bo2z-LNC.js → breadcrumbs-DnNOEnT4.js} +1 -1
- package/assets/{catalog-DVyrGTNR.js → catalog-BavcTVb4.js} +1 -1
- package/assets/{dashboard-Dy3pk9t7.js → dashboard-xr-OyVyG.js} +1 -1
- package/assets/{date-picker-B2z-aeuv.js → date-picker-DBHnomTY.js} +1 -1
- package/assets/{del-modal-l3LxJWCN.js → del-modal-BeF4r3gk.js} +1 -1
- package/assets/{field-renderer-BHo944Cn.js → field-renderer-Sd68OLyc.js} +3 -3
- package/assets/form-C9P9m_9q.js +1 -0
- package/assets/{form-hP8sjbND.js → form-DomTBI89.js} +1 -1
- package/assets/home-BWHeZm-1.js +2 -0
- package/assets/{icon-C_X2cQ6W.js → icon-ChQBPA5l.js} +1 -1
- package/assets/{init-user-DgCD4ZN1.js → init-user-DqMDju_E.js} +1 -1
- package/assets/input-error-g1h6ps7t.js +1 -0
- package/assets/list-EEgAn-nx.js +1 -0
- package/assets/{list-table-_OiQmr2A.js → list-table-Br0K3rXg.js} +1 -1
- package/assets/login-CUDpGsHl.js +1 -0
- package/assets/manifest.json +97 -64
- package/assets/material-icon-BIcI8APG.js +1 -0
- package/assets/media-manager-CTstvyHm.js +14 -0
- package/assets/{module-Ceq6vuwa.js → module-CpZBs0fN.js} +1 -1
- package/assets/{multi-select-BbKSrMw2.js → multi-select-BFWbEjH-.js} +1 -1
- package/assets/{nav-footer-B8gMV6Wp.js → nav-footer-bO8D-Yir.js} +1 -1
- package/assets/{nav-main-BUp9qA8R.js → nav-main-DVgvko8g.js} +1 -1
- package/assets/{nav-user-DeF3SDRj.js → nav-user-vIwYShwE.js} +1 -1
- package/assets/notification-BTXEslqa.js +1 -0
- package/assets/{pagination-render-B9U_hVbB.js → pagination-render-rZ5_6g0n.js} +1 -1
- package/assets/{register-C6acWsLe.js → register-Kfu8zmmu.js} +1 -1
- package/assets/tabs-CwgzWt48.js +1 -0
- package/assets/{theme-switcher-D8T9NgWG.js → theme-switcher-fzVA8btX.js} +1 -1
- package/assets/{user-info-CAgkqARx.js → user-info-Db5Y8KeN.js} +1 -1
- package/assets/{user-menu-content-BxL0QMC9.js → user-menu-content-Bc8YNZPf.js} +1 -1
- package/assets/{welcome-Dk7GoS5R.js → welcome-MKPFhPtG.js} +1 -1
- package/controllers/add.js +2 -0
- package/controllers/edit.js +14 -0
- package/controllers/login.js +56 -21
- package/controllers/media-manager/mediaManagerApi.js +1 -1
- package/controllers/notifications/NotificationController.d.ts +11 -0
- package/controllers/notifications/NotificationController.js +203 -0
- package/controllers/remove.js +2 -0
- package/files/favicon.png +0 -0
- package/helpers/diffHelpers.d.ts +2 -0
- package/helpers/diffHelpers.js +24 -0
- package/helpers/inertiaAutHelper.js +19 -0
- package/index.d.ts +2 -0
- package/index.js +2 -0
- package/interfaces/adminpanelConfig.d.ts +18 -0
- package/interfaces/types.d.ts +22 -0
- package/lib/Adminizer.d.ts +18 -2
- package/lib/Adminizer.js +61 -1
- package/lib/media-manager/AbstractMediaManager.js +2 -2
- package/lib/v4/model/adapter/sequelize.js +4 -0
- package/lib/v4/model/adapter/typeorm.d.ts +1 -0
- package/lib/v4/model/adapter/typeorm.js +21 -1
- package/lib/v4/notifications/AbstractNotificationService.d.ts +22 -0
- package/lib/v4/notifications/AbstractNotificationService.js +149 -0
- package/lib/v4/notifications/GeneralNotificationService.d.ts +8 -0
- package/lib/v4/notifications/GeneralNotificationService.js +56 -0
- package/lib/v4/notifications/NotificationHandler.d.ts +69 -0
- package/lib/v4/notifications/NotificationHandler.js +127 -0
- package/lib/v4/notifications/SystemNotificationService.d.ts +18 -0
- package/lib/v4/notifications/SystemNotificationService.js +123 -0
- package/models/NotificationAP.d.ts +36 -0
- package/models/NotificationAP.js +22 -0
- package/models/UserNotificationAP.d.ts +26 -0
- package/models/UserNotificationAP.js +18 -0
- package/package.json +13 -5
- package/system/Router.js +18 -0
- package/system/bindAssets.js +1 -0
- package/system/bindInertia.js +5 -0
- package/system/bindMediaManager.js +2 -1
- package/system/bindNavigation.js +2 -3
- package/system/bindNotifications.d.ts +2 -0
- package/system/bindNotifications.js +14 -0
- package/system/defaults.js +6 -0
- package/translations/ar.json +5 -4
- package/translations/cn.json +5 -4
- package/translations/de.json +72 -71
- package/translations/en.json +5 -4
- package/translations/es.json +5 -4
- package/translations/fr.json +5 -4
- package/translations/hi.json +5 -4
- package/translations/jp.json +5 -4
- package/translations/ko.json +5 -4
- package/translations/pt.json +5 -4
- package/translations/ru.json +2 -1
- package/translations/ua.json +5 -4
- package/translations/uz.json +73 -72
- package/translations/vi.json +5 -4
- package/assets/add-DOaZgoTz.js +0 -1
- package/assets/add-group-Br44s2D6.js +0 -1
- package/assets/add-user-DWcH0s9-.js +0 -1
- package/assets/app-sidebar-header-CIKLfLsF.js +0 -1
- package/assets/form-BXfnz6h5.js +0 -1
- package/assets/home-BD4MWmm9.js +0 -2
- package/assets/input-error-Bab-HPbn.js +0 -1
- package/assets/list-C-27EH39.js +0 -1
- package/assets/login-jzvVXgAV.js +0 -1
- package/assets/material-icon-DsiW0iQG.js +0 -1
- 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
|
|
54
|
-
department: '
|
|
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
|
-
|
|
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
|
+
}
|