adminizer 4.5.0-build.187 → 4.5.0-build.189
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/index.d.ts +2 -0
- package/index.js +2 -0
- 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/index.d.ts
CHANGED
|
@@ -19,6 +19,8 @@ export * from "./lib/model/AbstractModel";
|
|
|
19
19
|
export * from "./lib/model/adapter/waterline";
|
|
20
20
|
export * from "./lib/model/adapter/sequelize";
|
|
21
21
|
export * from "./lib/media-manager/AbstractMediaManager";
|
|
22
|
+
export * from "./lib/controls/AbstractControls";
|
|
23
|
+
export * from "./lib/controls/ControlsHandler";
|
|
22
24
|
export * from "./lib/Adminizer";
|
|
23
25
|
export * from "./models/GroupAP";
|
|
24
26
|
export * from "./models/UserAP";
|
package/index.js
CHANGED
|
@@ -19,6 +19,8 @@ export * from "./lib/model/AbstractModel.js";
|
|
|
19
19
|
export * from "./lib/model/adapter/waterline.js";
|
|
20
20
|
export * from "./lib/model/adapter/sequelize.js";
|
|
21
21
|
export * from "./lib/media-manager/AbstractMediaManager.js";
|
|
22
|
+
export * from "./lib/controls/AbstractControls.js";
|
|
23
|
+
export * from "./lib/controls/ControlsHandler.js";
|
|
22
24
|
export * from "./lib/Adminizer.js";
|
|
23
25
|
export * from "./models/GroupAP.js";
|
|
24
26
|
export * from "./models/UserAP.js";
|
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, {}]] });
|