adminizer 4.5.0-build.187 → 4.5.0-build.188
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/controllers/catalog/FrontendCatalogAdapter.js +3 -3
- package/controllers/catalog/FrontentCatalogAdapter.js +3 -3
- package/controllers/history-actions/HistoryController.js +15 -7
- package/controllers/list.js +1 -1
- package/controllers/media-manager/mediaManagerAdapter.js +3 -3
- package/controllers/notifications/NotificationController.d.ts +3 -3
- package/controllers/notifications/NotificationController.js +74 -52
- package/helpers/diffHelpers.js +8 -8
- package/lib/Adminizer.js +1 -1
- package/lib/history-actions/AbstractHistoryAdapter.js +2 -2
- package/lib/history-actions/DefaultHistoryAdapter.js +5 -5
- package/lib/inertia/inertiaAdapter.js +4 -4
- package/lib/media-manager/DefaultMediaManager.d.ts +1 -1
- package/lib/media-manager/DefaultMediaManager.js +5 -5
- package/lib/media-manager/Thumb.js +7 -7
- package/lib/media-manager/helpers/MediaManagerHelper.js +5 -5
- package/lib/model/adapter/sequelize.js +24 -24
- package/lib/notifications/AbstractNotificationService.js +13 -13
- package/lib/notifications/GeneralNotificationService.js +1 -1
- package/lib/notifications/SystemNotificationService.js +19 -19
- package/models/MediaManagerAssociationsAP.js +1 -1
- package/package.json +1 -1
- package/system/bindCors.js +6 -6
- package/system/bindModels.js +1 -1
- package/system/bindNotifications.js +2 -2
- package/system/defaults.js +9 -0
- package/ui/components/monaco-editor.js +1 -1
- package/ui/components/ui/dialog-stack.js +2 -2
- package/ui/lib/utils.js +4 -4
|
@@ -157,13 +157,13 @@ export class FrontendCatalog {
|
|
|
157
157
|
async deleteItem(item, req) {
|
|
158
158
|
if (item.id === 0)
|
|
159
159
|
item.id = null;
|
|
160
|
-
//
|
|
160
|
+
// Get all immediate children of the current element
|
|
161
161
|
const children = await this.catalog.getChilds(item.id, undefined, req);
|
|
162
|
-
//
|
|
162
|
+
// Recursively removing all children
|
|
163
163
|
for (const child of children) {
|
|
164
164
|
await this.deleteItem(child, req);
|
|
165
165
|
}
|
|
166
|
-
//
|
|
166
|
+
// After deleting all descendants, delete the element itself
|
|
167
167
|
await this.catalog.deleteItem(item.type, item.id, req);
|
|
168
168
|
return { ok: true };
|
|
169
169
|
}
|
|
@@ -154,13 +154,13 @@ export class FrontendCatalog {
|
|
|
154
154
|
async deleteItem(item, req) {
|
|
155
155
|
if (item.id === 0)
|
|
156
156
|
item.id = null;
|
|
157
|
-
//
|
|
157
|
+
// Get all immediate children of the current element
|
|
158
158
|
const children = await this.catalog.getChilds(item.id, undefined, req);
|
|
159
|
-
//
|
|
159
|
+
// Recursively removing all children
|
|
160
160
|
for (const child of children) {
|
|
161
161
|
await this.deleteItem(child, req);
|
|
162
162
|
}
|
|
163
|
-
//
|
|
163
|
+
// After deleting all descendants, delete the element itself
|
|
164
164
|
await this.catalog.deleteItem(item.type, item.id, req);
|
|
165
165
|
return { ok: true };
|
|
166
166
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Adminizer } from "../../lib/Adminizer.js";
|
|
2
2
|
export class HistoryController {
|
|
3
3
|
static async index(req, res) {
|
|
4
|
-
|
|
4
|
+
const isUiRequest = req.method.toUpperCase() === 'GET';
|
|
5
|
+
if (!HistoryController.checkHistoryPermission(req, res, isUiRequest))
|
|
5
6
|
return;
|
|
6
7
|
const adapter = HistoryController.getAdapter(req);
|
|
7
8
|
if (req.method.toUpperCase() === 'GET') {
|
|
@@ -21,7 +22,7 @@ export class HistoryController {
|
|
|
21
22
|
const models = rawModels.map(model => {
|
|
22
23
|
const normalizedModelName = model.toLowerCase();
|
|
23
24
|
const configModel = normalizedModelConfig.get(normalizedModelName);
|
|
24
|
-
const title = req.i18n.__(configModel?.title) ?? model; //
|
|
25
|
+
const title = req.i18n.__(configModel?.title) ?? model; // if there is no title, use the original name
|
|
25
26
|
return {
|
|
26
27
|
name: model,
|
|
27
28
|
title
|
|
@@ -115,18 +116,25 @@ export class HistoryController {
|
|
|
115
116
|
const adapter = req.adminizer.config.history?.adapter ?? 'default';
|
|
116
117
|
return req.adminizer.historyHandler.get(adapter);
|
|
117
118
|
}
|
|
118
|
-
static checkHistoryPermission(req, res) {
|
|
119
|
+
static checkHistoryPermission(req, res, shouldRedirectToLogin = false) {
|
|
119
120
|
if (!req.adminizer?.historyHandler) {
|
|
120
121
|
res.status(401).json({ error: 'History system not initialized' });
|
|
121
122
|
return false;
|
|
122
123
|
}
|
|
123
|
-
if (req.adminizer.config.auth.enable
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
if (req.adminizer.config.auth.enable) {
|
|
125
|
+
if (!req.user) {
|
|
126
|
+
if (shouldRedirectToLogin) {
|
|
127
|
+
res.redirect(`${req.adminizer.config.routePrefix}/model/userap/login`);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
res.status(401).json({ error: 'Unauthorized' });
|
|
131
|
+
}
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
126
134
|
}
|
|
127
135
|
const hasPermission = req.adminizer.accessRightsHelper.hasPermission(`history-${req.adminizer.config.history?.adapter ?? 'default'}`, req.user);
|
|
128
136
|
if (!hasPermission) {
|
|
129
|
-
res.
|
|
137
|
+
res.sendStatus(403);
|
|
130
138
|
return false;
|
|
131
139
|
}
|
|
132
140
|
return true;
|
package/controllers/list.js
CHANGED
|
@@ -104,7 +104,7 @@ function setColumns(fields, orderColumn, direction, searchPairs, req) {
|
|
|
104
104
|
title: req.i18n.__(field.config.title),
|
|
105
105
|
data: String(i),
|
|
106
106
|
direction: String(i) === orderColumn ? direction : undefined,
|
|
107
|
-
searchColumnValue: searchValue || undefined, // undefined
|
|
107
|
+
searchColumnValue: searchValue || undefined, // undefined if there is no search
|
|
108
108
|
};
|
|
109
109
|
nodeTreeColumns.push({
|
|
110
110
|
data: String(i),
|
|
@@ -200,9 +200,9 @@ export class MediaManagerAdapter {
|
|
|
200
200
|
checkMIMEType(allowedTypes, type) {
|
|
201
201
|
const [category] = type.split("/"); // "image/jpeg" → "image"
|
|
202
202
|
const wildcardType = `${category}/*`; // "image/*"
|
|
203
|
-
//
|
|
204
|
-
// 1.
|
|
205
|
-
// 2.
|
|
203
|
+
// Allowed if:
|
|
204
|
+
// 1. Exact match (e.g. "image/jpeg" in allowedTypes)
|
|
205
|
+
// 2. The entire category is allowed (for example, "image/*" in allowedTypes)
|
|
206
206
|
return allowedTypes.includes(type) || allowedTypes.includes(wildcardType);
|
|
207
207
|
}
|
|
208
208
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export declare class NotificationController {
|
|
2
2
|
static search(req: ReqType, res: ResType): Promise<void>;
|
|
3
|
-
static viewAll(req: ReqType, res: ResType): Promise<
|
|
4
|
-
static getNotificationClasses(req: ReqType, res: ResType): Promise<
|
|
3
|
+
static viewAll(req: ReqType, res: ResType): Promise<void>;
|
|
4
|
+
static getNotificationClasses(req: ReqType, res: ResType): Promise<void>;
|
|
5
5
|
static getNotificationsStream(req: ReqType, res: ResType): Promise<void>;
|
|
6
|
-
static getNotificationsByClass(req: ReqType, res: ResType): Promise<
|
|
6
|
+
static getNotificationsByClass(req: ReqType, res: ResType): Promise<void>;
|
|
7
7
|
static getUserNotifications(req: ReqType, res: ResType): Promise<void>;
|
|
8
8
|
static markAsRead(req: ReqType, res: ResType): Promise<void>;
|
|
9
9
|
static markAllAsRead(req: ReqType, res: ResType): Promise<void>;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Adminizer } from '../../lib/Adminizer.js';
|
|
2
2
|
export class NotificationController {
|
|
3
3
|
static async search(req, res) {
|
|
4
|
-
NotificationController.checkNotifPermission(req, res)
|
|
4
|
+
if (!NotificationController.checkNotifPermission(req, res))
|
|
5
|
+
return;
|
|
5
6
|
if (req.method.toUpperCase() === 'POST') {
|
|
6
7
|
const { s, notificationClass } = req.body;
|
|
7
8
|
const hasPermission = req.adminizer.accessRightsHelper.hasPermission(`notification-${notificationClass}`, req.user);
|
|
@@ -14,7 +15,9 @@ export class NotificationController {
|
|
|
14
15
|
}
|
|
15
16
|
}
|
|
16
17
|
static async viewAll(req, res) {
|
|
17
|
-
|
|
18
|
+
const isUiRequest = req.method.toUpperCase() === 'GET';
|
|
19
|
+
if (!NotificationController.checkNotifPermission(req, res, isUiRequest))
|
|
20
|
+
return;
|
|
18
21
|
if (req.method.toUpperCase() === 'POST') {
|
|
19
22
|
const messages = {
|
|
20
23
|
"Make read": "",
|
|
@@ -27,82 +30,89 @@ export class NotificationController {
|
|
|
27
30
|
"Diff": "",
|
|
28
31
|
"The end of the list has been reached": "",
|
|
29
32
|
};
|
|
30
|
-
|
|
33
|
+
res.json(Object.fromEntries(Object.keys(messages).map(key => [key, req.i18n.__(key)])));
|
|
34
|
+
return;
|
|
31
35
|
}
|
|
32
36
|
if (req.method.toUpperCase() === 'GET') {
|
|
33
|
-
|
|
37
|
+
req.Inertia.render({
|
|
34
38
|
component: 'notification',
|
|
35
39
|
props: {
|
|
36
40
|
title: req.i18n.__('Notifications'),
|
|
37
41
|
}
|
|
38
42
|
});
|
|
43
|
+
return;
|
|
39
44
|
}
|
|
40
|
-
|
|
45
|
+
res.status(405).end();
|
|
41
46
|
}
|
|
42
47
|
static async getNotificationClasses(req, res) {
|
|
43
|
-
NotificationController.checkNotifPermission(req, res)
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
if (!NotificationController.checkNotifPermission(req, res))
|
|
49
|
+
return;
|
|
50
|
+
if (req.adminizer.config.notifications.enabled === false) {
|
|
51
|
+
Adminizer.log.warn('[Notifications] Notifications disabled in config');
|
|
52
|
+
res.json([]);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
46
55
|
const services = req.adminizer.notificationHandler.getAllServices();
|
|
47
56
|
let activeServices = [];
|
|
48
57
|
for (const service of services) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (userClients.size > 0) {
|
|
58
|
+
const hasPermission = req.adminizer.accessRightsHelper.hasPermission(`notification-${service.notificationClass}`, req.user);
|
|
59
|
+
if (hasPermission) {
|
|
52
60
|
activeServices.push({
|
|
53
61
|
displayName: req.i18n.__(service.displayName),
|
|
54
62
|
notificationClass: service.notificationClass,
|
|
55
63
|
});
|
|
56
64
|
}
|
|
57
65
|
}
|
|
58
|
-
|
|
66
|
+
res.json({
|
|
59
67
|
activeServices: activeServices,
|
|
60
68
|
initTab: req.adminizer.config?.notifications?.initTab || null
|
|
61
69
|
});
|
|
62
70
|
}
|
|
63
|
-
// Единый SSE endpoint для всех уведомлений
|
|
64
71
|
static async getNotificationsStream(req, res) {
|
|
65
|
-
NotificationController.checkNotifPermission(req, res)
|
|
66
|
-
|
|
72
|
+
if (!NotificationController.checkNotifPermission(req, res))
|
|
73
|
+
return;
|
|
67
74
|
res.setHeader('Content-Type', 'text/event-stream');
|
|
68
|
-
res.setHeader('Cache-Control', 'no-cache');
|
|
75
|
+
res.setHeader('Cache-Control', 'no-cache, no-transform');
|
|
69
76
|
res.setHeader('Connection', 'keep-alive');
|
|
77
|
+
res.setHeader('X-Accel-Buffering', 'no');
|
|
78
|
+
res.setHeader('Content-Encoding', 'identity');
|
|
70
79
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
71
80
|
res.flushHeaders();
|
|
81
|
+
res.write(': stream-open\n\n');
|
|
72
82
|
const clientId = `user-${req.user?.id}-${Date.now()}`;
|
|
73
|
-
//
|
|
83
|
+
// Function for sending events to the client
|
|
74
84
|
const sendEvent = (event) => {
|
|
75
|
-
//
|
|
85
|
+
// Filtering notifications by user rights
|
|
76
86
|
if (event.type === 'notification') {
|
|
77
87
|
const notificationClass = event.notificationClass;
|
|
78
|
-
//
|
|
88
|
+
// UNIFIED rights check via AccessRightsHelper
|
|
79
89
|
const hasPermission = req.adminizer.accessRightsHelper.hasPermission(`notification-${notificationClass}`, req.user);
|
|
80
90
|
if (!hasPermission) {
|
|
81
|
-
return; //
|
|
91
|
+
return; // The user does not have rights to this notification class
|
|
82
92
|
}
|
|
83
|
-
//
|
|
93
|
+
// Checking personal notifications (target user only)
|
|
84
94
|
if (event.userId !== null && event.userId !== req.user.id) {
|
|
85
95
|
return;
|
|
86
96
|
}
|
|
87
97
|
}
|
|
88
98
|
res.write(`event: ${event.type}\n`);
|
|
89
99
|
res.write(`data: ${JSON.stringify(event.data)}\n\n`);
|
|
100
|
+
res.flush?.();
|
|
90
101
|
};
|
|
91
|
-
//
|
|
102
|
+
// We connect the client to all services
|
|
92
103
|
const services = req.adminizer.notificationHandler.getAllServices();
|
|
93
104
|
const allowedServices = services.filter(service => req.adminizer.accessRightsHelper.hasPermission(`notification-${service.notificationClass}`, req.user));
|
|
94
105
|
allowedServices.forEach(service => {
|
|
95
106
|
service.addClient(clientId, sendEvent, req.user);
|
|
96
|
-
//
|
|
107
|
+
// For a system service, add a client to CRUD channels
|
|
97
108
|
// if (service.notificationClass === 'system') {
|
|
98
109
|
// const systemService = service as SystemNotificationService;
|
|
99
|
-
// //
|
|
110
|
+
// // Add the client to the main CRUD channels indicating userId
|
|
100
111
|
// ['created', 'updated', 'deleted', 'system'].forEach(channel => {
|
|
101
112
|
// systemService.addClientToChannel(clientId, channel, req.user.id);
|
|
102
113
|
// });
|
|
103
114
|
// }
|
|
104
115
|
});
|
|
105
|
-
// Отправляем приветственное сообщение
|
|
106
116
|
sendEvent({
|
|
107
117
|
type: 'connected',
|
|
108
118
|
data: {
|
|
@@ -110,12 +120,12 @@ export class NotificationController {
|
|
|
110
120
|
clientId: clientId
|
|
111
121
|
}
|
|
112
122
|
});
|
|
113
|
-
//
|
|
123
|
+
// Handling connection closure
|
|
114
124
|
req.on('close', () => {
|
|
115
|
-
//
|
|
125
|
+
// Disconnecting the client from all services
|
|
116
126
|
services.forEach(service => {
|
|
117
127
|
service.removeClient(clientId);
|
|
118
|
-
//
|
|
128
|
+
// For system service, remove from all channels
|
|
119
129
|
// if (service.notificationClass === 'system') {
|
|
120
130
|
// const systemService = service as SystemNotificationService;
|
|
121
131
|
// if (systemService.removeClientFromAllChannels) {
|
|
@@ -125,12 +135,11 @@ export class NotificationController {
|
|
|
125
135
|
});
|
|
126
136
|
res.end();
|
|
127
137
|
});
|
|
128
|
-
// Heartbeat
|
|
138
|
+
// Heartbeat to keep you connected
|
|
129
139
|
const heartbeatInterval = setInterval(() => {
|
|
130
140
|
if (!res.writableEnded) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
});
|
|
141
|
+
res.write(': keepalive\n\n');
|
|
142
|
+
res.flush?.();
|
|
134
143
|
}
|
|
135
144
|
else {
|
|
136
145
|
clearInterval(heartbeatInterval);
|
|
@@ -140,34 +149,39 @@ export class NotificationController {
|
|
|
140
149
|
clearInterval(heartbeatInterval);
|
|
141
150
|
});
|
|
142
151
|
}
|
|
143
|
-
// API
|
|
152
|
+
// API for receiving class notifications
|
|
144
153
|
static async getNotificationsByClass(req, res) {
|
|
145
|
-
NotificationController.checkNotifPermission(req, res)
|
|
154
|
+
if (!NotificationController.checkNotifPermission(req, res))
|
|
155
|
+
return;
|
|
146
156
|
try {
|
|
147
157
|
const { notificationClass } = req.params;
|
|
148
158
|
const { limit = 20, skip = 0, unreadOnly = false } = req.query;
|
|
149
|
-
//
|
|
159
|
+
// Checking access rights
|
|
150
160
|
const hasPermission = req.adminizer.accessRightsHelper.hasPermission(`notification-${notificationClass}`, req.user);
|
|
151
161
|
if (!hasPermission) {
|
|
152
|
-
|
|
162
|
+
res.status(403).json({ error: 'Forbidden' });
|
|
163
|
+
return;
|
|
153
164
|
}
|
|
154
165
|
const service = req.adminizer.notificationHandler.getService(notificationClass);
|
|
155
|
-
if (!service)
|
|
156
|
-
|
|
166
|
+
if (!service) {
|
|
167
|
+
res.json({});
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
157
170
|
const notifications = await service.getNotifications(req.user?.id, Number(limit), Number(skip), unreadOnly === 'true');
|
|
158
|
-
|
|
171
|
+
res.json(notifications);
|
|
159
172
|
}
|
|
160
173
|
catch (error) {
|
|
161
174
|
Adminizer.log.error('Error getting notifications:', error);
|
|
162
|
-
|
|
175
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
163
176
|
}
|
|
164
177
|
}
|
|
165
|
-
// API
|
|
178
|
+
// API for receiving all user notifications
|
|
166
179
|
static async getUserNotifications(req, res) {
|
|
167
|
-
NotificationController.checkNotifPermission(req, res)
|
|
180
|
+
if (!NotificationController.checkNotifPermission(req, res))
|
|
181
|
+
return;
|
|
168
182
|
const { limit = 4, skip = 0, unreadOnly = false } = req.query;
|
|
169
183
|
try {
|
|
170
|
-
//
|
|
184
|
+
// Filtering services by access rights
|
|
171
185
|
const services = req.adminizer.notificationHandler.getAllServices();
|
|
172
186
|
const allowedServices = services.filter(service => req.adminizer.accessRightsHelper.hasPermission(`notification-${service.notificationClass}`, req.user));
|
|
173
187
|
const allNotifications = [];
|
|
@@ -175,7 +189,7 @@ export class NotificationController {
|
|
|
175
189
|
const notifications = await service.getNotifications(req.user.id, Number(limit), Number(skip), unreadOnly === 'true');
|
|
176
190
|
allNotifications.push(...notifications);
|
|
177
191
|
}
|
|
178
|
-
//
|
|
192
|
+
// Sort by creation date
|
|
179
193
|
const sortedNotifications = allNotifications.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()).slice(0, Number(limit));
|
|
180
194
|
res.json(sortedNotifications);
|
|
181
195
|
}
|
|
@@ -184,9 +198,10 @@ export class NotificationController {
|
|
|
184
198
|
res.status(500).json({ error: 'Internal server error' });
|
|
185
199
|
}
|
|
186
200
|
}
|
|
187
|
-
// API
|
|
201
|
+
// API for marking as read
|
|
188
202
|
static async markAsRead(req, res) {
|
|
189
|
-
NotificationController.checkNotifPermission(req, res)
|
|
203
|
+
if (!NotificationController.checkNotifPermission(req, res))
|
|
204
|
+
return;
|
|
190
205
|
try {
|
|
191
206
|
const { notificationClass, id } = req.params;
|
|
192
207
|
const service = req.adminizer.notificationHandler.getService(notificationClass);
|
|
@@ -199,7 +214,8 @@ export class NotificationController {
|
|
|
199
214
|
}
|
|
200
215
|
}
|
|
201
216
|
static async markAllAsRead(req, res) {
|
|
202
|
-
NotificationController.checkNotifPermission(req, res)
|
|
217
|
+
if (!NotificationController.checkNotifPermission(req, res))
|
|
218
|
+
return;
|
|
203
219
|
try {
|
|
204
220
|
const services = req.adminizer.notificationHandler.getAllServices();
|
|
205
221
|
for (const service of services) {
|
|
@@ -212,15 +228,21 @@ export class NotificationController {
|
|
|
212
228
|
res.status(500).json({ error: 'Internal server error' });
|
|
213
229
|
}
|
|
214
230
|
}
|
|
215
|
-
static checkNotifPermission(req, res) {
|
|
231
|
+
static checkNotifPermission(req, res, shouldRedirectToLogin = false) {
|
|
216
232
|
if (!req.adminizer?.notificationHandler) {
|
|
217
233
|
res.status(500).json({ error: 'Notification system not initialized' });
|
|
218
|
-
return;
|
|
234
|
+
return false;
|
|
219
235
|
}
|
|
220
|
-
//
|
|
236
|
+
// We use the redirect only for UI pages; API requests receive 401.
|
|
221
237
|
if (req.adminizer.config.auth.enable && !req.user) {
|
|
222
|
-
|
|
223
|
-
|
|
238
|
+
if (shouldRedirectToLogin) {
|
|
239
|
+
res.redirect(`${req.adminizer.config.routePrefix}/model/userap/login`);
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
res.status(401).json({ error: 'Unauthorized' });
|
|
243
|
+
}
|
|
244
|
+
return false;
|
|
224
245
|
}
|
|
246
|
+
return true;
|
|
225
247
|
}
|
|
226
248
|
}
|
package/helpers/diffHelpers.js
CHANGED
|
@@ -2,11 +2,11 @@ export function sanitizeForDiff(data) {
|
|
|
2
2
|
if (!data)
|
|
3
3
|
return {};
|
|
4
4
|
const result = { ...data };
|
|
5
|
-
//
|
|
5
|
+
// Removing system fields
|
|
6
6
|
const systemFields = ['id', 'createdAt', 'updatedAt', 'deletedAt', '__v', '_id'];
|
|
7
7
|
systemFields.forEach(field => delete result[field]);
|
|
8
|
-
//
|
|
9
|
-
// TODO:
|
|
8
|
+
// Cleaning sensitive data
|
|
9
|
+
// TODO: Place in environment variables or config
|
|
10
10
|
const sensitiveFields = ['password', 'token', 'secret', 'apiKey', 'creditCard'];
|
|
11
11
|
sensitiveFields.forEach(field => {
|
|
12
12
|
if (result[field] !== undefined) {
|
|
@@ -17,7 +17,7 @@ export function sanitizeForDiff(data) {
|
|
|
17
17
|
}
|
|
18
18
|
export function formatChanges(diffObj, oldData, newData, operation) {
|
|
19
19
|
const changes = [];
|
|
20
|
-
//
|
|
20
|
+
// Special handling for append operation
|
|
21
21
|
if (operation === 'add') {
|
|
22
22
|
for (const [key, value] of Object.entries(newData)) {
|
|
23
23
|
if (value === '***HIDDEN***')
|
|
@@ -32,9 +32,9 @@ export function formatChanges(diffObj, oldData, newData, operation) {
|
|
|
32
32
|
}
|
|
33
33
|
return changes;
|
|
34
34
|
}
|
|
35
|
-
//
|
|
35
|
+
// Standard handling for remove and update
|
|
36
36
|
for (const [key, value] of Object.entries(diffObj)) {
|
|
37
|
-
//
|
|
37
|
+
// For added fields (when oldData[key] is undefined)
|
|
38
38
|
if (oldData[key] === undefined && newData[key] !== undefined) {
|
|
39
39
|
changes.push({
|
|
40
40
|
field: key,
|
|
@@ -44,7 +44,7 @@ export function formatChanges(diffObj, oldData, newData, operation) {
|
|
|
44
44
|
operation: 'add'
|
|
45
45
|
});
|
|
46
46
|
}
|
|
47
|
-
//
|
|
47
|
+
// For removed fields (when newData[key] is undefined)
|
|
48
48
|
else if (newData[key] === undefined && oldData[key] !== undefined) {
|
|
49
49
|
changes.push({
|
|
50
50
|
field: key,
|
|
@@ -54,7 +54,7 @@ export function formatChanges(diffObj, oldData, newData, operation) {
|
|
|
54
54
|
operation: 'remove'
|
|
55
55
|
});
|
|
56
56
|
}
|
|
57
|
-
//
|
|
57
|
+
// For changed fields
|
|
58
58
|
else {
|
|
59
59
|
changes.push({
|
|
60
60
|
field: key,
|
package/lib/Adminizer.js
CHANGED
|
@@ -226,7 +226,7 @@ export class Adminizer {
|
|
|
226
226
|
*/
|
|
227
227
|
this._emitter.emit('adminizer:loaded');
|
|
228
228
|
}
|
|
229
|
-
//
|
|
229
|
+
// Helper for sending notifications
|
|
230
230
|
async sendNotification(notification) {
|
|
231
231
|
if (this.config.notifications.enabled) {
|
|
232
232
|
const notificationClass = notification.notificationClass || 'general';
|
|
@@ -144,12 +144,12 @@ export class AbstractHistoryAdapter {
|
|
|
144
144
|
accessHistory.push(historyRecord);
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
|
-
//
|
|
147
|
+
// Grouping records by model for optimization
|
|
148
148
|
const fieldsCache = new Map();
|
|
149
149
|
for (const historyRecord of accessHistory) {
|
|
150
150
|
const entity = this.findEntityObject(historyRecord);
|
|
151
151
|
const modelKey = historyRecord.modelName;
|
|
152
|
-
//
|
|
152
|
+
// We use a cache so as not to create a DataAccessor for each record
|
|
153
153
|
if (!fieldsCache.has(modelKey)) {
|
|
154
154
|
const dataAccessor = new DataAccessor(this.adminizer, user, entity, "edit");
|
|
155
155
|
let fields = dataAccessor.getFieldsConfig();
|
|
@@ -29,9 +29,9 @@ export class DefaultHistoryAdapter extends AbstractHistoryAdapter {
|
|
|
29
29
|
let totalFetched = 0;
|
|
30
30
|
let resultItems = [];
|
|
31
31
|
let currentSkip = skip;
|
|
32
|
-
//
|
|
32
|
+
// Add more until you get the required amount
|
|
33
33
|
while (resultItems.length < limit) {
|
|
34
|
-
//
|
|
34
|
+
// We request with a reserve to reduce the number of requests to the database
|
|
35
35
|
const fetchLimit = Math.min(limit * 2, 50);
|
|
36
36
|
const history = await this.adminizer.modelHandler.model.get(this.model)["_find"]({
|
|
37
37
|
where: query,
|
|
@@ -40,10 +40,10 @@ export class DefaultHistoryAdapter extends AbstractHistoryAdapter {
|
|
|
40
40
|
skip: currentSkip
|
|
41
41
|
});
|
|
42
42
|
if (history.length === 0) {
|
|
43
|
-
break; //
|
|
43
|
+
break; // No more data
|
|
44
44
|
}
|
|
45
45
|
const filteredHistory = await this._getAllHistory(history, user);
|
|
46
|
-
//
|
|
46
|
+
// Add filtered records to the result
|
|
47
47
|
for (const item of filteredHistory) {
|
|
48
48
|
if (resultItems.length < limit) {
|
|
49
49
|
resultItems.push(item);
|
|
@@ -51,7 +51,7 @@ export class DefaultHistoryAdapter extends AbstractHistoryAdapter {
|
|
|
51
51
|
}
|
|
52
52
|
totalFetched += history.length;
|
|
53
53
|
currentSkip += history.length;
|
|
54
|
-
//
|
|
54
|
+
// If you receive less than what you requested, it means the database has run out of data
|
|
55
55
|
if (history.length < fetchLimit) {
|
|
56
56
|
break;
|
|
57
57
|
}
|
|
@@ -19,7 +19,7 @@ const inertiaExpressAdapter = function ({ version, html, flashMessages, enableRe
|
|
|
19
19
|
secure: process.env.NODE_ENV === 'production' && process.env.CSRF_COOKIE_INSECURE !== '1',
|
|
20
20
|
sameSite: 'lax',
|
|
21
21
|
});
|
|
22
|
-
//
|
|
22
|
+
// Checking CSRF only for non-GET requests AND non-API routes
|
|
23
23
|
if (!['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
|
|
24
24
|
const csrfCookie = req.cookies['XSRF-TOKEN'];
|
|
25
25
|
const csrfHeader = req.headers[csrf.headerName || 'x-xsrf-token'];
|
|
@@ -134,7 +134,7 @@ const inertiaExpressAdapter = function ({ version, html, flashMessages, enableRe
|
|
|
134
134
|
return next();
|
|
135
135
|
};
|
|
136
136
|
};
|
|
137
|
-
//
|
|
137
|
+
// Helper function for defining API routes
|
|
138
138
|
function isApiRequest(req) {
|
|
139
139
|
const adminizer = req.adminizer;
|
|
140
140
|
if (!adminizer?.config?.cors?.enabled) {
|
|
@@ -142,9 +142,9 @@ function isApiRequest(req) {
|
|
|
142
142
|
}
|
|
143
143
|
const corsConfig = adminizer.config.cors;
|
|
144
144
|
const routePrefix = adminizer.config.routePrefix || '';
|
|
145
|
-
//
|
|
145
|
+
// Create a base path for the API
|
|
146
146
|
const apiBasePath = `${routePrefix}/${corsConfig.path?.replace('*', '')}`;
|
|
147
|
-
//
|
|
147
|
+
// Checking if the path starts with API base path
|
|
148
148
|
return req.path.startsWith(apiBasePath);
|
|
149
149
|
}
|
|
150
150
|
export default inertiaExpressAdapter;
|
|
@@ -12,7 +12,7 @@ export declare class DefaultMediaManager extends AbstractMediaManager {
|
|
|
12
12
|
next: boolean;
|
|
13
13
|
}>;
|
|
14
14
|
searchAll(s: string, group: string): Promise<MediaManagerItem[]>;
|
|
15
|
-
setRelations(data: MediaManagerWidgetData[], model: string, modelId: string | number,
|
|
15
|
+
setRelations(data: MediaManagerWidgetData[], model: string, modelId: string | number, //Updated
|
|
16
16
|
widgetName: string): Promise<void>;
|
|
17
17
|
getRelations(model: string, widgetName: string, modelId: string | number): Promise<MediaManagerWidgetClientItem[]>;
|
|
18
18
|
}
|
|
@@ -55,12 +55,12 @@ export class DefaultMediaManager extends AbstractMediaManager {
|
|
|
55
55
|
}
|
|
56
56
|
return data;
|
|
57
57
|
}
|
|
58
|
-
async setRelations(data, model, modelId,
|
|
58
|
+
async setRelations(data, model, modelId, //Updated
|
|
59
59
|
widgetName) {
|
|
60
60
|
if (modelId == null) {
|
|
61
61
|
throw new Error("modelId must be a string or number");
|
|
62
62
|
}
|
|
63
|
-
const modelIdStr = String(modelId);
|
|
63
|
+
const modelIdStr = String(modelId); //Normalize to string
|
|
64
64
|
let modelAssociations = await this.adminizer.modelHandler.model.get(this.modelAssoc)["_find"]({
|
|
65
65
|
where: {
|
|
66
66
|
modelId: modelIdStr,
|
|
@@ -79,7 +79,7 @@ export class DefaultMediaManager extends AbstractMediaManager {
|
|
|
79
79
|
await this.adminizer.modelHandler.model.get(this.modelAssoc)["_create"]({
|
|
80
80
|
mediaManagerId: this.id,
|
|
81
81
|
model: model.toLowerCase(),
|
|
82
|
-
modelId: modelIdStr,
|
|
82
|
+
modelId: modelIdStr, //Save as a string
|
|
83
83
|
[fieldName]: widgetItem.id,
|
|
84
84
|
widgetName: widgetName,
|
|
85
85
|
sortOrder: key + 1,
|
|
@@ -90,14 +90,14 @@ export class DefaultMediaManager extends AbstractMediaManager {
|
|
|
90
90
|
if (modelId == null) {
|
|
91
91
|
throw new Error("modelId must be a string or number");
|
|
92
92
|
}
|
|
93
|
-
const modelIdStr = String(modelId);
|
|
93
|
+
const modelIdStr = String(modelId); //Normalize to string
|
|
94
94
|
let widgetItems = [];
|
|
95
95
|
const fieldName = this.adminizer.ormAdapters[0].ormType === 'sequelize' ? 'fileRef' : 'file';
|
|
96
96
|
let files = await this.adminizer.modelHandler.model.get(this.modelAssoc)['_find']({
|
|
97
97
|
where: {
|
|
98
98
|
model: model.toLowerCase(),
|
|
99
99
|
widgetName: widgetName,
|
|
100
|
-
modelId: modelIdStr,
|
|
100
|
+
modelId: modelIdStr, //Search by string
|
|
101
101
|
},
|
|
102
102
|
sort: "sortOrder ASC"
|
|
103
103
|
}, { populate: [[fieldName, {}]] });
|
|
@@ -14,26 +14,26 @@ export class MediaManagerThumb {
|
|
|
14
14
|
};
|
|
15
15
|
const manager = adminizer.mediaManagerHandler.get(managerId);
|
|
16
16
|
const filePath = await manager.getOrigin(id);
|
|
17
|
-
//
|
|
17
|
+
// Checking if the path is already absolute
|
|
18
18
|
const isAbsolute = path.isAbsolute(filePath);
|
|
19
|
-
//
|
|
19
|
+
// Forming the correct path to the source file
|
|
20
20
|
const sourcePath = isAbsolute ?
|
|
21
|
-
path.normalize(filePath) : //
|
|
22
|
-
path.join(process.cwd(), filePath); //
|
|
23
|
-
//
|
|
21
|
+
path.normalize(filePath) : // If the path is already absolute, normalize it
|
|
22
|
+
path.join(process.cwd(), filePath); // If relative, add base
|
|
23
|
+
// Path for thumbnail
|
|
24
24
|
const baseThumbPath = path.join(process.cwd(), '.tmp', 'thumbs');
|
|
25
25
|
await fs.mkdir(baseThumbPath, { recursive: true });
|
|
26
26
|
const thumbPath = path.join(baseThumbPath, `${id}_thumb.webp`);
|
|
27
27
|
if (await fileExists(thumbPath)) {
|
|
28
28
|
return await fs.readFile(thumbPath);
|
|
29
29
|
}
|
|
30
|
-
//
|
|
30
|
+
// Checking the existence of the file
|
|
31
31
|
if (!await fileExists(sourcePath)) {
|
|
32
32
|
throw new Error(`Source file not found: ${sourcePath}\n` +
|
|
33
33
|
`Check if file exists at: ${sourcePath}\n` +
|
|
34
34
|
`Original path from manager: ${filePath}`);
|
|
35
35
|
}
|
|
36
|
-
//
|
|
36
|
+
// Create a thumbnail
|
|
37
37
|
await sharp(sourcePath)
|
|
38
38
|
.resize({ width: 150, height: 150, fit: 'cover' })
|
|
39
39
|
.toFile(thumbPath);
|
|
@@ -74,18 +74,18 @@ export async function populateVariants(adminizer, variants, model) {
|
|
|
74
74
|
}
|
|
75
75
|
export function getAssociationFieldName(model, associationName) {
|
|
76
76
|
const attributes = model.attributes || {};
|
|
77
|
-
//
|
|
77
|
+
// For your case: file connection uses fileId
|
|
78
78
|
if (associationName === 'file') {
|
|
79
|
-
//
|
|
79
|
+
// We check whether there is a file connection and what via it has
|
|
80
80
|
if (attributes.file?.type === 'association' && attributes.file.via) {
|
|
81
|
-
return attributes.file.via; //
|
|
81
|
+
return attributes.file.via; // Returns 'fileId'
|
|
82
82
|
}
|
|
83
|
-
//
|
|
83
|
+
// Or just check for the presence of fileId
|
|
84
84
|
if (attributes.fileId) {
|
|
85
85
|
return 'fileId';
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
|
-
//
|
|
88
|
+
// General case
|
|
89
89
|
const idField = `${associationName}Id`;
|
|
90
90
|
return attributes[idField] ? idField : associationName;
|
|
91
91
|
}
|
|
@@ -21,7 +21,7 @@ function generateAssociationsFromSchema(models, schemas) {
|
|
|
21
21
|
.sort()
|
|
22
22
|
.join("")
|
|
23
23
|
.toLowerCase();
|
|
24
|
-
// 💡 M:N
|
|
24
|
+
// 💡 M:N connection
|
|
25
25
|
if (inverseField && inverseField.collection === modelName) {
|
|
26
26
|
model.belongsToMany(targetModel, {
|
|
27
27
|
through: throughTableName,
|
|
@@ -30,7 +30,7 @@ function generateAssociationsFromSchema(models, schemas) {
|
|
|
30
30
|
otherKey: `${field.collection}Id`
|
|
31
31
|
});
|
|
32
32
|
}
|
|
33
|
-
// 💡 O:M
|
|
33
|
+
// 💡 O:M communication (one to many)
|
|
34
34
|
else {
|
|
35
35
|
let foreignKey = `${modelName}Id`;
|
|
36
36
|
if (field.collection === modelName) {
|
|
@@ -51,7 +51,7 @@ function generateAssociationsFromSchema(models, schemas) {
|
|
|
51
51
|
targetModel.belongsTo(model, belongsToOptions);
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
|
-
// 💡 O:1
|
|
54
|
+
// 💡 O:1 or 1:1 (belongsTo)
|
|
55
55
|
if (field.model) {
|
|
56
56
|
const targetModel = models[field.model];
|
|
57
57
|
if (!targetModel)
|
|
@@ -166,7 +166,7 @@ export class SequelizeModel extends AbstractModel {
|
|
|
166
166
|
(typeof value === "object" && value !== null && Object.keys(value).length === 0)) {
|
|
167
167
|
continue;
|
|
168
168
|
}
|
|
169
|
-
// 🧠
|
|
169
|
+
// 🧠 Replace the key with `via` if this is an association
|
|
170
170
|
const attr = this.attributes?.[key];
|
|
171
171
|
let targetKey = key;
|
|
172
172
|
if (attr?.type === "association" && attr.via) {
|
|
@@ -176,7 +176,7 @@ export class SequelizeModel extends AbstractModel {
|
|
|
176
176
|
result[targetKey] = { [Op.is]: null };
|
|
177
177
|
}
|
|
178
178
|
else if (Array.isArray(value)) {
|
|
179
|
-
// ✅
|
|
179
|
+
// ✅ Array processing - use the IN operator
|
|
180
180
|
result[targetKey] = { [Op.in]: value };
|
|
181
181
|
}
|
|
182
182
|
else if (typeof value === "object" && !Array.isArray(value)) {
|
|
@@ -275,8 +275,8 @@ export class SequelizeModel extends AbstractModel {
|
|
|
275
275
|
const assocNames = Object.keys(this.model.associations);
|
|
276
276
|
const plainData = {};
|
|
277
277
|
const assocData = {};
|
|
278
|
-
// console.debug(">> _create:
|
|
279
|
-
// console.debug(">>
|
|
278
|
+
// console.debug(">> _create: input data:", data);
|
|
279
|
+
// console.debug(">> Available associations:", assocNames);
|
|
280
280
|
for (const [key, val] of Object.entries(data)) {
|
|
281
281
|
if (assocNames.includes(key)) {
|
|
282
282
|
assocData[key] = val;
|
|
@@ -285,19 +285,19 @@ export class SequelizeModel extends AbstractModel {
|
|
|
285
285
|
plainData[key] = val;
|
|
286
286
|
}
|
|
287
287
|
}
|
|
288
|
-
// console.debug(">>
|
|
289
|
-
// console.debug(">>
|
|
288
|
+
// console.debug(">> Normal fields for create():", plainData);
|
|
289
|
+
// console.debug(">> Association data:", assocData);
|
|
290
290
|
let instance;
|
|
291
291
|
try {
|
|
292
292
|
instance = await this.model.create(plainData);
|
|
293
|
-
// console.debug(">>
|
|
293
|
+
// console.debug(">> Instance created (without associations):", instance.toJSON());
|
|
294
294
|
}
|
|
295
295
|
catch (err) {
|
|
296
|
-
// console.error("!!
|
|
296
|
+
// console.error("!! Error during create(plainData):", err);
|
|
297
297
|
throw err;
|
|
298
298
|
}
|
|
299
299
|
// assocData = { example: 5, userAPs: [1,2,3], category: 7, tags: [11,22] }
|
|
300
|
-
// this.model.associations
|
|
300
|
+
// this.model.associations - your associations object
|
|
301
301
|
for (const [alias, ids] of Object.entries(assocData)) {
|
|
302
302
|
const assoc = this.model.associations[alias];
|
|
303
303
|
if (!assoc) {
|
|
@@ -331,37 +331,37 @@ export class SequelizeModel extends AbstractModel {
|
|
|
331
331
|
await instance.reload({ include: Object.values(this.model.associations) });
|
|
332
332
|
const pk = this.primaryKey;
|
|
333
333
|
const fresh = (await this.model.findByPk(instance.get(pk), { include: assocNames.map(a => ({ association: a })) })).toJSON();
|
|
334
|
-
// console.debug(">>
|
|
334
|
+
// console.debug(">> Result after reload:", fresh?.toJSON());
|
|
335
335
|
return fresh;
|
|
336
336
|
}
|
|
337
337
|
// --- FIND ONE ---
|
|
338
338
|
async _findOne(criteria) {
|
|
339
|
-
// console.debug(">> _findOne:
|
|
339
|
+
// console.debug(">> _findOne: input criteria:", criteria);
|
|
340
340
|
const { where } = this._convertWaterlineCriteriaToSequelizeOptions(criteria);
|
|
341
341
|
const includes = this._buildIncludes();
|
|
342
|
-
// console.debug(">> _findOne:
|
|
342
|
+
// console.debug(">> _findOne: converted where:", where);
|
|
343
343
|
// console.debug(">> _findOne: includes:", includes);
|
|
344
344
|
let instance = null;
|
|
345
345
|
try {
|
|
346
346
|
instance = await this.model.findOne({ where, include: includes });
|
|
347
|
-
// console.debug(">> _findOne:
|
|
347
|
+
// console.debug(">> _findOne: raw instance:", instance ? instance.toJSON() : null);
|
|
348
348
|
}
|
|
349
349
|
catch (err) {
|
|
350
|
-
// console.error("!! _findOne:
|
|
350
|
+
// console.error("!! _findOne: error when calling findOne:", err);
|
|
351
351
|
throw err;
|
|
352
352
|
}
|
|
353
353
|
if (!instance) {
|
|
354
|
-
// console.debug(">> _findOne:
|
|
354
|
+
// console.debug(">> _findOne: nothing found");
|
|
355
355
|
return null;
|
|
356
356
|
}
|
|
357
357
|
const plain = instance.get({ plain: true });
|
|
358
|
-
// console.debug(">> _findOne: plain
|
|
358
|
+
// console.debug(">> _findOne: plain result:", plain);
|
|
359
359
|
return plain;
|
|
360
360
|
}
|
|
361
361
|
// --- FIND MANY ---
|
|
362
362
|
async _find(criteria = {}, options = {}) {
|
|
363
363
|
const assocNames = Object.keys(this.model.associations);
|
|
364
|
-
// console.debug(">> _find:
|
|
364
|
+
// console.debug(">> _find: input criteria:", criteria, "options:", options);
|
|
365
365
|
const { where, limit, offset, order } = this._convertWaterlineCriteriaToSequelizeOptions(criteria);
|
|
366
366
|
const includes = options.populate
|
|
367
367
|
? options.populate.map(([field, opts]) => ({ association: field, ...opts }))
|
|
@@ -395,13 +395,13 @@ export class SequelizeModel extends AbstractModel {
|
|
|
395
395
|
// console.debug(`---- get${alias}():`, mapped);
|
|
396
396
|
}
|
|
397
397
|
catch (e) {
|
|
398
|
-
// console.error(`!!
|
|
398
|
+
// console.error(`!! error when calling ${getAccessor}():`, e);
|
|
399
399
|
}
|
|
400
400
|
}
|
|
401
401
|
}
|
|
402
402
|
}
|
|
403
403
|
const plain = instances.map(i => i.get({ plain: true }));
|
|
404
|
-
// console.debug(">> _find: plain
|
|
404
|
+
// console.debug(">> _find: plain results:", plain);
|
|
405
405
|
return plain;
|
|
406
406
|
}
|
|
407
407
|
// --- UPDATE ONE ---
|
|
@@ -465,7 +465,7 @@ export class SequelizeModel extends AbstractModel {
|
|
|
465
465
|
if (typeof record[getAccessor] === "function") {
|
|
466
466
|
try {
|
|
467
467
|
const related = await record[getAccessor]();
|
|
468
|
-
// 🧹
|
|
468
|
+
// 🧹Deleting related posts
|
|
469
469
|
if (Array.isArray(related)) {
|
|
470
470
|
for (const r of related) {
|
|
471
471
|
if (typeof r.destroy === "function") {
|
|
@@ -494,7 +494,7 @@ export class SequelizeModel extends AbstractModel {
|
|
|
494
494
|
for (const record of records) {
|
|
495
495
|
for (const alias of assocNames) {
|
|
496
496
|
const assoc = this.model.associations[alias];
|
|
497
|
-
// 🛑
|
|
497
|
+
// 🛑 We don’t touch parental ties
|
|
498
498
|
if (assoc.associationType === "BelongsTo")
|
|
499
499
|
continue;
|
|
500
500
|
// @ts-ignore accessor exists
|
|
@@ -73,11 +73,11 @@ export class AbstractNotificationService extends EventEmitter {
|
|
|
73
73
|
*/
|
|
74
74
|
addClient(clientId, sendFn, user) {
|
|
75
75
|
const userId = user.id;
|
|
76
|
-
//
|
|
76
|
+
// If the user does not yet have Map clients, create one
|
|
77
77
|
if (!this.clients.has(userId)) {
|
|
78
78
|
this.clients.set(userId, new Map());
|
|
79
79
|
}
|
|
80
|
-
//
|
|
80
|
+
// We get the Map of the user's clients and add a new client
|
|
81
81
|
const userClients = this.clients.get(userId);
|
|
82
82
|
userClients.set(clientId, sendFn);
|
|
83
83
|
Adminizer.log.info(`[${this.notificationClass}] Client ${clientId} connected for user ${userId}. Total users: ${this.clients.size}, user clients: ${userClients.size}`);
|
|
@@ -88,11 +88,11 @@ export class AbstractNotificationService extends EventEmitter {
|
|
|
88
88
|
* @param {string} clientId - The unique identifier of the client to be removed.
|
|
89
89
|
*/
|
|
90
90
|
removeClient(clientId) {
|
|
91
|
-
//
|
|
91
|
+
// We are looking for a client in all custom Maps
|
|
92
92
|
for (const [userId, userClients] of this.clients.entries()) {
|
|
93
93
|
if (userClients.has(clientId)) {
|
|
94
94
|
userClients.delete(clientId);
|
|
95
|
-
//
|
|
95
|
+
// If the user no longer has clients, delete his Map
|
|
96
96
|
if (userClients.size === 0) {
|
|
97
97
|
this.clients.delete(userId);
|
|
98
98
|
}
|
|
@@ -225,7 +225,7 @@ export class AbstractNotificationService extends EventEmitter {
|
|
|
225
225
|
}
|
|
226
226
|
try {
|
|
227
227
|
let query = { notificationClass: this.notificationClass };
|
|
228
|
-
//
|
|
228
|
+
// If notifications are requested for a specific user, we obtain the user's notification ID
|
|
229
229
|
const userNotifications = await this.adminizer.modelHandler.model.get('usernotificationap')["_find"]({
|
|
230
230
|
where: {
|
|
231
231
|
userId: userId
|
|
@@ -233,7 +233,7 @@ export class AbstractNotificationService extends EventEmitter {
|
|
|
233
233
|
}, { populate: [['notificationId', {}]] });
|
|
234
234
|
const notificationIds = userNotifications.map((un) => un.notificationId.id);
|
|
235
235
|
if (unreadOnly) {
|
|
236
|
-
//
|
|
236
|
+
// Only unread
|
|
237
237
|
query.id = userNotifications
|
|
238
238
|
.filter((un) => !un.read)
|
|
239
239
|
.map((un) => un.notificationId.id);
|
|
@@ -266,7 +266,7 @@ export class AbstractNotificationService extends EventEmitter {
|
|
|
266
266
|
let notifications = [];
|
|
267
267
|
for (const notification of notificationsDB) {
|
|
268
268
|
let readStatus = false;
|
|
269
|
-
//
|
|
269
|
+
// Getting read status from UserNotificationAP
|
|
270
270
|
const userNotification = await this.getUserNotification(notification.id, userId);
|
|
271
271
|
readStatus = userNotification ? userNotification.read : false;
|
|
272
272
|
notifications.push({
|
|
@@ -365,25 +365,25 @@ export class AbstractNotificationService extends EventEmitter {
|
|
|
365
365
|
*/
|
|
366
366
|
async getNotificationsCount(userId, unreadOnly = false) {
|
|
367
367
|
try {
|
|
368
|
-
//
|
|
368
|
+
// We create a filtering condition by reading status
|
|
369
369
|
const whereClause = { userId: userId };
|
|
370
370
|
if (unreadOnly) {
|
|
371
|
-
whereClause.read = false; //
|
|
371
|
+
whereClause.read = false; // only unread
|
|
372
372
|
}
|
|
373
|
-
//
|
|
374
|
-
//
|
|
373
|
+
// if unreadOnly = false - we get ALL records (both read and not)
|
|
374
|
+
// We receive all user-notification communications
|
|
375
375
|
const userNotifications = await this.adminizer.modelHandler.model.get('usernotificationap')["_find"]({
|
|
376
376
|
where: whereClause
|
|
377
377
|
}, { populate: [['notificationId', {}]] });
|
|
378
378
|
if (userNotifications.length === 0)
|
|
379
379
|
return 0;
|
|
380
|
-
//
|
|
380
|
+
// Filter by notification class using notificationId
|
|
381
381
|
const validNotificationIds = userNotifications
|
|
382
382
|
.map((un) => un.notificationId.id)
|
|
383
383
|
.filter((id) => id != null);
|
|
384
384
|
if (validNotificationIds.length === 0)
|
|
385
385
|
return 0;
|
|
386
|
-
//
|
|
386
|
+
// We recalculate only those whose notificationClass matches
|
|
387
387
|
return await this.adminizer.modelHandler.model.get('notificationap')["_count"]({
|
|
388
388
|
id: validNotificationIds,
|
|
389
389
|
notificationClass: this.notificationClass
|
|
@@ -11,7 +11,7 @@ export class GeneralNotificationService extends AbstractNotificationService {
|
|
|
11
11
|
notificationClass: this.notificationClass
|
|
12
12
|
};
|
|
13
13
|
let notificationDB;
|
|
14
|
-
//
|
|
14
|
+
// Save to the database
|
|
15
15
|
if (this.adminizer.modelHandler.model.has('notificationap')) {
|
|
16
16
|
try {
|
|
17
17
|
notificationDB = await this.adminizer.modelHandler.model.get('notificationap')["_create"](fullNotification);
|
|
@@ -5,7 +5,7 @@ export class SystemNotificationService extends AbstractNotificationService {
|
|
|
5
5
|
displayName = 'System';
|
|
6
6
|
icon = 'settings';
|
|
7
7
|
iconColor = '#1eb707';
|
|
8
|
-
//
|
|
8
|
+
// We change the channel structure: store by userId -> channel -> clientIds
|
|
9
9
|
crudChannels = new Map();
|
|
10
10
|
async dispatchNotification(notification) {
|
|
11
11
|
const fullNotification = {
|
|
@@ -14,7 +14,7 @@ export class SystemNotificationService extends AbstractNotificationService {
|
|
|
14
14
|
channel: notification.channel ?? ''
|
|
15
15
|
};
|
|
16
16
|
let notificationDB;
|
|
17
|
-
//
|
|
17
|
+
// Save to the database
|
|
18
18
|
if (this.adminizer.modelHandler.model.has('notificationap')) {
|
|
19
19
|
try {
|
|
20
20
|
notificationDB = await this.adminizer.modelHandler.model.get('notificationap')["_create"](fullNotification);
|
|
@@ -42,7 +42,7 @@ export class SystemNotificationService extends AbstractNotificationService {
|
|
|
42
42
|
userId: notification.userId ?? null,
|
|
43
43
|
channel: notification.channel ?? 'system'
|
|
44
44
|
};
|
|
45
|
-
//
|
|
45
|
+
// Send to all channels or to a specific channel
|
|
46
46
|
if (notification.channel) {
|
|
47
47
|
this.broadcastToChannel(notification.channel, event);
|
|
48
48
|
}
|
|
@@ -58,9 +58,9 @@ export class SystemNotificationService extends AbstractNotificationService {
|
|
|
58
58
|
}
|
|
59
59
|
return false;
|
|
60
60
|
}
|
|
61
|
-
//
|
|
61
|
+
// Updating broadcastToChannel to work with the new structure
|
|
62
62
|
broadcastToChannel(channel, event) {
|
|
63
|
-
//
|
|
63
|
+
// Sent to all users subscribed to this channel
|
|
64
64
|
this.crudChannels.forEach((userChannels, userId) => {
|
|
65
65
|
const channelClients = userChannels.get(channel);
|
|
66
66
|
if (channelClients) {
|
|
@@ -82,7 +82,7 @@ export class SystemNotificationService extends AbstractNotificationService {
|
|
|
82
82
|
}
|
|
83
83
|
});
|
|
84
84
|
}
|
|
85
|
-
//
|
|
85
|
+
// Adding a client to a channel linked to a user
|
|
86
86
|
addClientToChannel(clientId, channel, userId) {
|
|
87
87
|
if (!this.crudChannels.has(userId)) {
|
|
88
88
|
this.crudChannels.set(userId, new Map());
|
|
@@ -94,7 +94,7 @@ export class SystemNotificationService extends AbstractNotificationService {
|
|
|
94
94
|
userChannels.get(channel).add(clientId);
|
|
95
95
|
Adminizer.log.info(`[${this.notificationClass}] Client ${clientId} (user ${userId}) added to channel ${channel}`);
|
|
96
96
|
}
|
|
97
|
-
//
|
|
97
|
+
// Removing a client from a specific user's channel
|
|
98
98
|
removeClientFromChannel(clientId, channel, userId) {
|
|
99
99
|
const userChannels = this.crudChannels.get(userId);
|
|
100
100
|
if (userChannels) {
|
|
@@ -102,38 +102,38 @@ export class SystemNotificationService extends AbstractNotificationService {
|
|
|
102
102
|
if (channelClients) {
|
|
103
103
|
channelClients.delete(clientId);
|
|
104
104
|
Adminizer.log.info(`[${this.notificationClass}] Client ${clientId} (user ${userId}) removed from channel ${channel}`);
|
|
105
|
-
//
|
|
105
|
+
// If there are no more clients in the channel, delete the channel
|
|
106
106
|
if (channelClients.size === 0) {
|
|
107
107
|
userChannels.delete(channel);
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
|
-
//
|
|
110
|
+
// If the user no longer has channels, delete the user's entry
|
|
111
111
|
if (userChannels.size === 0) {
|
|
112
112
|
this.crudChannels.delete(userId);
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
|
-
//
|
|
116
|
+
// We remove the client from all user channels
|
|
117
117
|
removeClientFromAllChannels(clientId, userId) {
|
|
118
118
|
const userChannels = this.crudChannels.get(userId);
|
|
119
119
|
if (userChannels) {
|
|
120
120
|
userChannels.forEach((clients, channel) => {
|
|
121
121
|
clients.delete(clientId);
|
|
122
122
|
Adminizer.log.info(`[${this.notificationClass}] Client ${clientId} (user ${userId}) removed from channel ${channel}`);
|
|
123
|
-
//
|
|
123
|
+
// If there are no more clients in the channel, delete the channel
|
|
124
124
|
if (clients.size === 0) {
|
|
125
125
|
userChannels.delete(channel);
|
|
126
126
|
}
|
|
127
127
|
});
|
|
128
|
-
//
|
|
128
|
+
// If the user no longer has channels, delete the user's entry
|
|
129
129
|
if (userChannels.size === 0) {
|
|
130
130
|
this.crudChannels.delete(userId);
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
|
-
//
|
|
134
|
+
// Overriding removeClient to clear channels
|
|
135
135
|
removeClient(clientId) {
|
|
136
|
-
//
|
|
136
|
+
// Find userId by clientId
|
|
137
137
|
let foundUserId = null;
|
|
138
138
|
for (const [userId, userClients] of this.clients.entries()) {
|
|
139
139
|
if (userClients.has(clientId)) {
|
|
@@ -141,14 +141,14 @@ export class SystemNotificationService extends AbstractNotificationService {
|
|
|
141
141
|
break;
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
|
-
//
|
|
144
|
+
// Removing the client from the main storage
|
|
145
145
|
super.removeClient(clientId);
|
|
146
|
-
//
|
|
146
|
+
// Removing a client from channels
|
|
147
147
|
if (foundUserId !== null) {
|
|
148
148
|
this.removeClientFromAllChannels(clientId, foundUserId);
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
|
-
//
|
|
151
|
+
// Special method for system events specifying the channel
|
|
152
152
|
async logSystemEvent(title, message, channel, metadata) {
|
|
153
153
|
return this.dispatchNotification({
|
|
154
154
|
title: title,
|
|
@@ -157,7 +157,7 @@ export class SystemNotificationService extends AbstractNotificationService {
|
|
|
157
157
|
channel: channel
|
|
158
158
|
});
|
|
159
159
|
}
|
|
160
|
-
//
|
|
160
|
+
// Methods for CRUD operations
|
|
161
161
|
async logCreatedEvent(title, message, metadata) {
|
|
162
162
|
return this.logSystemEvent(title, message, 'created', metadata);
|
|
163
163
|
}
|
|
@@ -167,7 +167,7 @@ export class SystemNotificationService extends AbstractNotificationService {
|
|
|
167
167
|
async logDeletedEvent(title, message, metadata) {
|
|
168
168
|
return this.logSystemEvent(title, message, 'deleted', metadata);
|
|
169
169
|
}
|
|
170
|
-
//
|
|
170
|
+
// New method to get user channels
|
|
171
171
|
getUserChannels(userId) {
|
|
172
172
|
return this.crudChannels.get(userId) || new Map();
|
|
173
173
|
}
|
|
@@ -11,7 +11,7 @@ export default {
|
|
|
11
11
|
type: "string"
|
|
12
12
|
},
|
|
13
13
|
modelId: {
|
|
14
|
-
type: "string" // ✅
|
|
14
|
+
type: "string" // ✅ Changed: support for strings and numbers via casting to string
|
|
15
15
|
},
|
|
16
16
|
widgetName: {
|
|
17
17
|
type: "string"
|
package/package.json
CHANGED
package/system/bindCors.js
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
1
|
export function bindCors(adminizer) {
|
|
2
2
|
if (adminizer.config?.cors?.enabled) {
|
|
3
3
|
const corsConfig = adminizer.config.cors;
|
|
4
|
-
//
|
|
4
|
+
// We support an array of allowed origins
|
|
5
5
|
const allowedOrigins = Array.isArray(corsConfig.origin)
|
|
6
6
|
? corsConfig.origin
|
|
7
7
|
: [corsConfig.origin];
|
|
8
8
|
adminizer.app.all(`${adminizer.config.routePrefix}/${corsConfig.path}`, (req, res, next) => {
|
|
9
9
|
const requestOrigin = req.headers.origin;
|
|
10
|
-
//
|
|
10
|
+
// Checking if origin is allowed
|
|
11
11
|
const isOriginAllowed = !requestOrigin || allowedOrigins.includes(requestOrigin);
|
|
12
12
|
if (requestOrigin && !isOriginAllowed) {
|
|
13
13
|
console.log(`❌ CORS: Blocked request from ${requestOrigin}`);
|
|
14
14
|
if (req.method === 'OPTIONS') {
|
|
15
|
-
//
|
|
15
|
+
// For preflight - 200 without CORS headers
|
|
16
16
|
return res.status(200).end();
|
|
17
17
|
}
|
|
18
18
|
else {
|
|
19
|
-
//
|
|
19
|
+
// For basic queries - error
|
|
20
20
|
return res.status(403).json({
|
|
21
21
|
error: 'CORS policy: Origin not allowed'
|
|
22
22
|
});
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
|
-
//
|
|
25
|
+
// Request from allowed origin or without Origin
|
|
26
26
|
if (isOriginAllowed) {
|
|
27
|
-
//
|
|
27
|
+
// For CORS requests, return the same origin (or the first one from the list)
|
|
28
28
|
const allowOrigin = requestOrigin || allowedOrigins[0];
|
|
29
29
|
res.header('Access-Control-Allow-Origin', allowOrigin);
|
|
30
30
|
res.header('Access-Control-Allow-Credentials', corsConfig.credentials !== false ? 'true' : 'false');
|
package/system/bindModels.js
CHANGED
|
@@ -11,7 +11,7 @@ export default async function bindModels(adminizer) {
|
|
|
11
11
|
throw new Error("Default ORM adapter was not provided");
|
|
12
12
|
}
|
|
13
13
|
const systemModelsDir = path.resolve(import.meta.dirname, "../models");
|
|
14
|
-
//
|
|
14
|
+
// We filter only .js and .ts files, excluding .d.ts
|
|
15
15
|
const systemModelsFiles = fs.readdirSync(systemModelsDir).filter(file => (file.endsWith(".js") || (file.endsWith(".ts") && !file.endsWith(".d.ts"))));
|
|
16
16
|
// Bind system models reading them from ../models and get the whole list of them for further checks
|
|
17
17
|
const systemModels = systemModelsFiles.map((file) => {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { NotificationHandler } from '../lib/notifications/NotificationHandler.js';
|
|
2
2
|
import { GeneralNotificationService } from '../lib/notifications/GeneralNotificationService.js';
|
|
3
3
|
export async function bindNotifications(adminizer) {
|
|
4
|
-
//
|
|
4
|
+
// Create a handler
|
|
5
5
|
adminizer.notificationHandler = new NotificationHandler();
|
|
6
|
-
//
|
|
6
|
+
// Registering services
|
|
7
7
|
// const systemService = new SystemNotificationService(adminizer);
|
|
8
8
|
// adminizer.notificationHandler.registerService(systemService);
|
|
9
9
|
if (adminizer.config.notifications.enableGeneral) {
|
package/system/defaults.js
CHANGED
|
@@ -142,9 +142,18 @@ let adminpanelConfig = {
|
|
|
142
142
|
bind: {
|
|
143
143
|
public: true
|
|
144
144
|
},
|
|
145
|
+
/**
|
|
146
|
+
* Experimental feature
|
|
147
|
+
*/
|
|
145
148
|
notifications: {
|
|
146
149
|
enabled: false
|
|
147
150
|
},
|
|
151
|
+
/**
|
|
152
|
+
* Experimental feature
|
|
153
|
+
*/
|
|
154
|
+
history: {
|
|
155
|
+
enabled: false
|
|
156
|
+
},
|
|
148
157
|
aiAssistant: {
|
|
149
158
|
enabled: false,
|
|
150
159
|
defaultModel: 'dummy',
|
|
@@ -46,7 +46,7 @@ const MonacoEditor = ({ onChange, value, options, disabled }) => {
|
|
|
46
46
|
useEffect(() => {
|
|
47
47
|
updateEditorOptions();
|
|
48
48
|
}, [width]);
|
|
49
|
-
//
|
|
49
|
+
// Calculating the height of the editor
|
|
50
50
|
const editorHeight = isMobile ? '300px' : isTablet ? '400px' : '500px';
|
|
51
51
|
return (_jsx("div", { className: `border rounded-lg ${isMobile ? 'p-1' : 'p-2'}`, ref: editorWrapperRef, children: _jsx(Editor, { height: editorHeight, language: options.language, value: value, theme: theme, options: {
|
|
52
52
|
automaticLayout: true,
|
|
@@ -21,7 +21,7 @@ const DialogStackContext = createContext({
|
|
|
21
21
|
setShouldAnimate: () => {
|
|
22
22
|
},
|
|
23
23
|
closeDialog: () => {
|
|
24
|
-
}, //
|
|
24
|
+
}, // Added
|
|
25
25
|
});
|
|
26
26
|
export const DialogStack = React.forwardRef(({ children, className, open = false, onOpenChange, clickable = false, ...props }, ref) => {
|
|
27
27
|
const [activeIndex, setActiveIndex] = useState(0);
|
|
@@ -38,7 +38,7 @@ export const DialogStack = React.forwardRef(({ children, className, open = false
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
};
|
|
41
|
-
//
|
|
41
|
+
// Imperative methods
|
|
42
42
|
React.useImperativeHandle(ref, () => ({
|
|
43
43
|
open: () => {
|
|
44
44
|
setIsOpen(true);
|
package/ui/lib/utils.js
CHANGED
|
@@ -5,17 +5,17 @@ export function cn(...inputs) {
|
|
|
5
5
|
}
|
|
6
6
|
export function simpleSanitizeHtml(html) {
|
|
7
7
|
return html
|
|
8
|
-
//
|
|
8
|
+
// Removing script tags
|
|
9
9
|
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
|
|
10
|
-
//
|
|
10
|
+
// Removing iframe, embed, object
|
|
11
11
|
.replace(/<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe>/gi, '')
|
|
12
12
|
.replace(/<embed\b[^<]*(?:(?!<\/embed>)<[^<]*)*<\/embed>/gi, '')
|
|
13
13
|
.replace(/<object\b[^<]*(?:(?!<\/object>)<[^<]*)*<\/object>/gi, '')
|
|
14
|
-
//
|
|
14
|
+
// Removing dangerous event attributes
|
|
15
15
|
.replace(/ on\w+="[^"]*"/g, '')
|
|
16
16
|
.replace(/ on\w+='[^']*'/g, '')
|
|
17
17
|
.replace(/ on\w+=[^ >]+/g, '')
|
|
18
|
-
//
|
|
18
|
+
// Removing javascript: links
|
|
19
19
|
.replace(/href="javascript:[^"]*"/gi, 'href="#"')
|
|
20
20
|
.replace(/href='javascript:[^']*'/gi, 'href="#"');
|
|
21
21
|
}
|