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.
@@ -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
- if (!HistoryController.checkHistoryPermission(req, res))
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; // если title нет использовать оригинальное имя
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 && !req.user) {
124
- res.status(401).json({ error: 'Unauthorized' });
125
- return false;
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.status(403).json({ error: 'Forbidden' });
137
+ res.sendStatus(403);
130
138
  return false;
131
139
  }
132
140
  return true;
@@ -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. Точное совпадение (например, "image/jpeg" в allowedTypes)
205
- // 2. Разрешена вся категория (например, "image/*" в allowedTypes)
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<import("express").Response<any, Record<string, any>>>;
4
- static getNotificationClasses(req: ReqType, res: ResType): Promise<ResType>;
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<ResType>;
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
- NotificationController.checkNotifPermission(req, res);
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
- return res.json(Object.fromEntries(Object.keys(messages).map(key => [key, req.i18n.__(key)])));
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
- return req.Inertia.render({
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
- return res.status(405);
45
+ res.status(405).end();
41
46
  }
42
47
  static async getNotificationClasses(req, res) {
43
- NotificationController.checkNotifPermission(req, res);
44
- if (req.adminizer.config.notifications.enabled === false)
45
- return res.json([]);
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
- const userClients = service.getUserClients(req.user.id);
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
- return res.json({
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
- // Устанавливаем заголовки для SSE
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
- // ЕДИНАЯ проверка прав через AccessRightsHelper
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
- // Для системного сервиса добавляем клиента в CRUD каналы
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
- // // Добавляем клиента в основные CRUD каналы с указанием userId
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
- services.forEach(service => {
132
- service.sendHeartbeat(clientId);
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
- return res.status(403).json({ error: 'Forbidden' });
162
+ res.status(403).json({ error: 'Forbidden' });
163
+ return;
153
164
  }
154
165
  const service = req.adminizer.notificationHandler.getService(notificationClass);
155
- if (!service)
156
- return res.json({});
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
- return res.json(notifications);
171
+ res.json(notifications);
159
172
  }
160
173
  catch (error) {
161
174
  Adminizer.log.error('Error getting notifications:', error);
162
- return res.status(500).json({ error: 'Internal server error' });
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
- res.status(401).json({ error: 'Unauthorized' });
223
- return;
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
  }
@@ -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
- // Стандартная обработка для remove и update
35
+ // Standard handling for remove and update
36
36
  for (const [key, value] of Object.entries(diffObj)) {
37
- // Для добавленных полей (когда oldData[key] undefined)
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
- // Для удаленных полей (когда newData[key] undefined)
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
- // Используем кэш, чтобы не создавать DataAccessor для каждой записи
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
- // Проверяем CSRF только для не-GET запросов И не-API routes
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
- // Вспомогательная функция для определения API routes
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
- // Создаем базовый путь для API
145
+ // Create a base path for the API
146
146
  const apiBasePath = `${routePrefix}/${corsConfig.path?.replace('*', '')}`;
147
- // Проверяем начинается ли путь с API base path
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); // Если относительный, добавляем base
23
- // Путь для thumbnail
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
- // Создаем thumbnail
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
- // Для вашего случая: file связь использует fileId
77
+ // For your case: file connection uses fileId
78
78
  if (associationName === 'file') {
79
- // Проверяем есть ли связь file и какое у нее via
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; // Вернет 'fileId'
81
+ return attributes.file.via; // Returns 'fileId'
82
82
  }
83
- // Или просто проверяем наличие fileId
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 или 1:1 (belongsTo)
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
- // 🧠 Заменяем ключ на `via`, если это ассоциация
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
- // ✅ Обработка массивов - используем оператор IN
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: входные данные:", data);
279
- // console.debug(">> Доступные ассоциации:", assocNames);
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(">> Обычные поля для create():", plainData);
289
- // console.debug(">> Данные ассоциаций:", assocData);
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(">> Создан экземпляр (без ассоциаций):", instance.toJSON());
293
+ // console.debug(">> Instance created (without associations):", instance.toJSON());
294
294
  }
295
295
  catch (err) {
296
- // console.error("!! Ошибка при create(plainData):", err);
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(">> Результат после reload:", fresh?.toJSON());
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: входные критерии:", criteria);
339
+ // console.debug(">> _findOne: input criteria:", criteria);
340
340
  const { where } = this._convertWaterlineCriteriaToSequelizeOptions(criteria);
341
341
  const includes = this._buildIncludes();
342
- // console.debug(">> _findOne: преобразованные where:", where);
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: сырое instance:", instance ? instance.toJSON() : null);
347
+ // console.debug(">> _findOne: raw instance:", instance ? instance.toJSON() : null);
348
348
  }
349
349
  catch (err) {
350
- // console.error("!! _findOne: ошибка при вызове findOne:", err);
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 результат:", 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: входные criteria:", criteria, "options:", options);
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(`!! ошибка при вызове ${getAccessor}():`, e);
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 результаты:", 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
- // Если у пользователя еще нет Map клиентов - создаем
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
- // Получаем Map клиентов пользователя и добавляем нового клиента
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
- // Ищем клиента во всех пользовательских Map
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
- // Если у пользователя больше нет клиентов - удаляем его Map
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
- // Если запрашиваются уведомления для конкретного пользователя получаем ID уведомлений пользователя
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
- // Получаем статус прочтения из UserNotificationAP
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
- // если unreadOnly = false получаем ВСЕ записи (и прочитанные, и нет)
374
- // Получаем все user-notification связи
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
- // Фильтруем по классу уведомлений через notificationId
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
- // Пересчитываем только те, у которых notificationClass совпадает
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
- // Изменяем структуру каналов: храним по userId -> channel -> clientIds
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
- // Обновляем broadcastToChannel для работы с новой структурой
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
- // Переопределяем removeClient для очистки каналов
134
+ // Overriding removeClient to clear channels
135
135
  removeClient(clientId) {
136
- // Находим userId по clientId
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
- // Методы для CRUD операций
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "adminizer",
3
3
  "type": "module",
4
- "version": "4.5.0-build.187",
4
+ "version": "4.5.0-build.188",
5
5
  "main": "index.js",
6
6
  "exports": {
7
7
  ".": "./index.js",
@@ -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
- // Поддерживаем массив разрешенных origin
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
- // Проверяем разрешен ли origin
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
- // Для preflight - 200 без CORS headers
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
- // Запрос с разрешенного origin или без Origin
25
+ // Request from allowed origin or without Origin
26
26
  if (isOriginAllowed) {
27
- // Для CORS запросов возвращаем тот же origin (или первый из списка)
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');
@@ -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
- // Фильтруем только .js и .ts файлы, исключая .d.ts
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) {
@@ -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
- // Удаляем script теги
8
+ // Removing script tags
9
9
  .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
10
- // Удаляем iframe, embed, object
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
- // Удаляем javascript: ссылки
18
+ // Removing javascript: links
19
19
  .replace(/href="javascript:[^"]*"/gi, 'href="#"')
20
20
  .replace(/href='javascript:[^']*'/gi, 'href="#"');
21
21
  }