adminizer 4.2.0 → 4.3.0-build.74
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/add-BNb5A_qF.js +1 -0
- package/assets/{add-form-BjackXJe.js → add-form-CGi_qv3J.js} +1 -1
- package/assets/add-group-RRjHTu93.js +1 -0
- package/assets/{add-group-form-YI6KS37x.js → add-group-form-DSd4UrnU.js} +1 -1
- package/assets/add-user-DSuvcKp6.js +1 -0
- package/assets/{add-user-form-bv5BNX5A.js → add-user-form-BAssKDpI.js} +1 -1
- package/assets/{app-CVnUv8s_.css → app-GsYdx39W.css} +1 -1
- package/assets/{app-layout-DecN527i.js → app-layout-ContoN1K.js} +1 -1
- package/assets/{app-sidebar-4JFP8UdU.js → app-sidebar-D7l5BJPV.js} +1 -1
- package/assets/app-sidebar-header-CK8t32cm.js +1 -0
- package/assets/app.js +13 -13
- package/assets/{breadcrumbs-Bo2z-LNC.js → breadcrumbs-DnNOEnT4.js} +1 -1
- package/assets/{catalog-DVyrGTNR.js → catalog-BavcTVb4.js} +1 -1
- package/assets/{dashboard-Dy3pk9t7.js → dashboard-xr-OyVyG.js} +1 -1
- package/assets/{date-picker-B2z-aeuv.js → date-picker-DBHnomTY.js} +1 -1
- package/assets/{del-modal-l3LxJWCN.js → del-modal-BeF4r3gk.js} +1 -1
- package/assets/{field-renderer-BHo944Cn.js → field-renderer-Sd68OLyc.js} +3 -3
- package/assets/form-C9P9m_9q.js +1 -0
- package/assets/{form-hP8sjbND.js → form-DomTBI89.js} +1 -1
- package/assets/home-BWHeZm-1.js +2 -0
- package/assets/{icon-C_X2cQ6W.js → icon-ChQBPA5l.js} +1 -1
- package/assets/{init-user-DgCD4ZN1.js → init-user-DqMDju_E.js} +1 -1
- package/assets/input-error-g1h6ps7t.js +1 -0
- package/assets/list-EEgAn-nx.js +1 -0
- package/assets/{list-table-_OiQmr2A.js → list-table-Br0K3rXg.js} +1 -1
- package/assets/login-CUDpGsHl.js +1 -0
- package/assets/manifest.json +97 -64
- package/assets/material-icon-BIcI8APG.js +1 -0
- package/assets/media-manager-CTstvyHm.js +14 -0
- package/assets/{module-Ceq6vuwa.js → module-CpZBs0fN.js} +1 -1
- package/assets/{multi-select-BbKSrMw2.js → multi-select-BFWbEjH-.js} +1 -1
- package/assets/{nav-footer-B8gMV6Wp.js → nav-footer-bO8D-Yir.js} +1 -1
- package/assets/{nav-main-BUp9qA8R.js → nav-main-DVgvko8g.js} +1 -1
- package/assets/{nav-user-DeF3SDRj.js → nav-user-vIwYShwE.js} +1 -1
- package/assets/notification-BTXEslqa.js +1 -0
- package/assets/{pagination-render-B9U_hVbB.js → pagination-render-rZ5_6g0n.js} +1 -1
- package/assets/{register-C6acWsLe.js → register-Kfu8zmmu.js} +1 -1
- package/assets/tabs-CwgzWt48.js +1 -0
- package/assets/{theme-switcher-D8T9NgWG.js → theme-switcher-fzVA8btX.js} +1 -1
- package/assets/{user-info-CAgkqARx.js → user-info-Db5Y8KeN.js} +1 -1
- package/assets/{user-menu-content-BxL0QMC9.js → user-menu-content-Bc8YNZPf.js} +1 -1
- package/assets/{welcome-Dk7GoS5R.js → welcome-MKPFhPtG.js} +1 -1
- package/controllers/add.js +2 -0
- package/controllers/edit.js +14 -0
- package/controllers/login.js +56 -21
- package/controllers/media-manager/mediaManagerApi.js +1 -1
- package/controllers/notifications/NotificationController.d.ts +11 -0
- package/controllers/notifications/NotificationController.js +203 -0
- package/controllers/remove.js +2 -0
- package/files/favicon.png +0 -0
- package/helpers/diffHelpers.d.ts +2 -0
- package/helpers/diffHelpers.js +24 -0
- package/helpers/inertiaAutHelper.js +19 -0
- package/index.d.ts +2 -0
- package/index.js +2 -0
- package/interfaces/adminpanelConfig.d.ts +18 -0
- package/interfaces/types.d.ts +22 -0
- package/lib/Adminizer.d.ts +18 -2
- package/lib/Adminizer.js +61 -1
- package/lib/media-manager/AbstractMediaManager.js +2 -2
- package/lib/v4/model/adapter/sequelize.js +4 -0
- package/lib/v4/model/adapter/typeorm.d.ts +1 -0
- package/lib/v4/model/adapter/typeorm.js +21 -1
- package/lib/v4/notifications/AbstractNotificationService.d.ts +22 -0
- package/lib/v4/notifications/AbstractNotificationService.js +149 -0
- package/lib/v4/notifications/GeneralNotificationService.d.ts +8 -0
- package/lib/v4/notifications/GeneralNotificationService.js +56 -0
- package/lib/v4/notifications/NotificationHandler.d.ts +69 -0
- package/lib/v4/notifications/NotificationHandler.js +127 -0
- package/lib/v4/notifications/SystemNotificationService.d.ts +18 -0
- package/lib/v4/notifications/SystemNotificationService.js +123 -0
- package/models/NotificationAP.d.ts +36 -0
- package/models/NotificationAP.js +22 -0
- package/models/UserNotificationAP.d.ts +26 -0
- package/models/UserNotificationAP.js +18 -0
- package/package.json +13 -5
- package/system/Router.js +18 -0
- package/system/bindAssets.js +1 -0
- package/system/bindInertia.js +5 -0
- package/system/bindMediaManager.js +2 -1
- package/system/bindNavigation.js +2 -3
- package/system/bindNotifications.d.ts +2 -0
- package/system/bindNotifications.js +14 -0
- package/system/defaults.js +6 -0
- package/translations/ar.json +5 -4
- package/translations/cn.json +5 -4
- package/translations/de.json +72 -71
- package/translations/en.json +5 -4
- package/translations/es.json +5 -4
- package/translations/fr.json +5 -4
- package/translations/hi.json +5 -4
- package/translations/jp.json +5 -4
- package/translations/ko.json +5 -4
- package/translations/pt.json +5 -4
- package/translations/ru.json +2 -1
- package/translations/ua.json +5 -4
- package/translations/uz.json +73 -72
- package/translations/vi.json +5 -4
- package/assets/add-DOaZgoTz.js +0 -1
- package/assets/add-group-Br44s2D6.js +0 -1
- package/assets/add-user-DWcH0s9-.js +0 -1
- package/assets/app-sidebar-header-CIKLfLsF.js +0 -1
- package/assets/form-BXfnz6h5.js +0 -1
- package/assets/home-BD4MWmm9.js +0 -2
- package/assets/input-error-Bab-HPbn.js +0 -1
- package/assets/list-C-27EH39.js +0 -1
- package/assets/login-jzvVXgAV.js +0 -1
- package/assets/material-icon-DsiW0iQG.js +0 -1
- package/assets/media-manager-CSB_XRcE.js +0 -14
package/controllers/add.js
CHANGED
|
@@ -87,6 +87,8 @@ export default async function add(req, res) {
|
|
|
87
87
|
}
|
|
88
88
|
try {
|
|
89
89
|
let record = await entity.model.create(reqData, dataAccessor);
|
|
90
|
+
// log system event notification
|
|
91
|
+
await req.adminizer.logSystemCreatedEvent(req.i18n.__('Created'), `user ${req.user.login} ${req.i18n.__('create')} ${entity.name} ${record.id}`, { test: 'test' });
|
|
90
92
|
// save associations media to json
|
|
91
93
|
await saveRelationsMediaManager(fields, rawReqData, entity.model.identity, record.id);
|
|
92
94
|
Adminizer.log.debug(`A new record was created: `, record);
|
package/controllers/edit.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { ControllerHelper } from "../helpers/controllerHelper";
|
|
2
2
|
import { RequestProcessor } from "../lib/requestProcessor";
|
|
3
3
|
import { FieldsHelper } from "../helpers/fieldsHelper";
|
|
4
|
+
import { diff } from 'deep-object-diff';
|
|
4
5
|
import { getRelationsMediaManager, saveRelationsMediaManager } from "../lib/media-manager/helpers/MediaManagerHelper";
|
|
5
6
|
import { DataAccessor } from "../lib/v4/DataAccessor";
|
|
6
7
|
import { Adminizer } from "../lib/Adminizer";
|
|
7
8
|
import inertiaAddHelper from "../helpers/inertiaAddHelper";
|
|
9
|
+
import { formatChanges, sanitizeForDiff } from "../helpers/diffHelpers";
|
|
8
10
|
export default async function edit(req, res) {
|
|
9
11
|
//Check id
|
|
10
12
|
if (!req.params.id) {
|
|
@@ -113,6 +115,18 @@ export default async function edit(req, res) {
|
|
|
113
115
|
try {
|
|
114
116
|
let newRecord = await entity.model.update(params, reqData, dataAccessor);
|
|
115
117
|
await saveRelationsMediaManager(fields, rawReqData, entity.model.identity, newRecord[0].id);
|
|
118
|
+
// Создаем clean объекты для сравнения (исключаем системные поля)
|
|
119
|
+
const cleanOldRecord = sanitizeForDiff(record);
|
|
120
|
+
const cleanNewRecord = sanitizeForDiff(newRecord[0]);
|
|
121
|
+
// Получаем diff
|
|
122
|
+
const changesDiff = diff(cleanOldRecord, cleanNewRecord);
|
|
123
|
+
// Форматируем для лога
|
|
124
|
+
const formattedChanges = formatChanges(changesDiff, cleanOldRecord, cleanNewRecord);
|
|
125
|
+
// log system event notification
|
|
126
|
+
await req.adminizer.logSystemUpdatedEvent(req.i18n.__('Updated'), `user ${req.user.login} ${req.i18n.__('update')} ${entity.name} ${record.id}`, {
|
|
127
|
+
changes: formattedChanges,
|
|
128
|
+
summary: `${req.i18n.__('Changes')} ${Object.keys(formattedChanges).length} ${req.i18n.__('fields')}`
|
|
129
|
+
});
|
|
116
130
|
Adminizer.log.debug(`Record was updated: `, newRecord);
|
|
117
131
|
if (req.body.jsonPopupCatalog) {
|
|
118
132
|
return res.json({ record: newRecord });
|
package/controllers/login.js
CHANGED
|
@@ -14,25 +14,25 @@ export default async function login(req, res) {
|
|
|
14
14
|
let login = req.body.login;
|
|
15
15
|
let password = req.body.password;
|
|
16
16
|
let captchaSolution = req.body.captchaSolution;
|
|
17
|
-
|
|
18
|
-
try {
|
|
19
|
-
// TODO refactor CRUD functions for DataAccessor usage
|
|
20
|
-
user = await req.adminizer.modelHandler.model.get("UserAP")["_findOne"]({ login: login });
|
|
21
|
-
}
|
|
22
|
-
catch (e) {
|
|
23
|
-
return res.status(500).send({ error: e.message || 'Internal Server Error' });
|
|
24
|
-
}
|
|
17
|
+
// Handle pretend (impersonation) first against internal users list
|
|
25
18
|
if (req.body.pretend) {
|
|
26
|
-
|
|
27
|
-
|
|
19
|
+
try {
|
|
20
|
+
// TODO refactor CRUD functions for DataAccessor usage
|
|
21
|
+
const pretendUser = await req.adminizer.modelHandler.model.get("UserAP")["_findOne"]({ login });
|
|
22
|
+
if (!pretendUser) {
|
|
23
|
+
return res.sendStatus(404);
|
|
24
|
+
}
|
|
25
|
+
if (req.user.isAdministrator) {
|
|
26
|
+
req.session.userPretended = pretendUser;
|
|
27
|
+
return req.Inertia.redirect(`${req.adminizer.config.routePrefix}`);
|
|
28
|
+
}
|
|
29
|
+
return req.Inertia.redirect(`${req.adminizer.config.routePrefix}/`);
|
|
28
30
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
// return res.sendStatus(200);
|
|
32
|
-
return req.Inertia.redirect(`${req.adminizer.config.routePrefix}`);
|
|
31
|
+
catch (e) {
|
|
32
|
+
return res.status(500).send({ error: e.message || 'Internal Server Error' });
|
|
33
33
|
}
|
|
34
|
-
return req.Inertia.redirect(`${req.adminizer.config.routePrefix}/`);
|
|
35
34
|
}
|
|
35
|
+
let authenticatedExternally = false;
|
|
36
36
|
// Verify CAPTCHA solution if enabled
|
|
37
37
|
if (req.adminizer.config.auth.captcha !== false) {
|
|
38
38
|
const isCaptchaValid = powCaptcha.check(captchaSolution, `login:${req.ip}`);
|
|
@@ -40,17 +40,22 @@ export default async function login(req, res) {
|
|
|
40
40
|
return inertiaAdminMessage(req, "Invalid CAPTCHA solution", 'captchaSolution');
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
// First check UserAP model (local auth)
|
|
44
|
+
let user;
|
|
45
|
+
try {
|
|
46
|
+
// TODO refactor CRUD functions for DataAccessor usage
|
|
47
|
+
user = await req.adminizer.modelHandler.model.get("UserAP")["_findOne"]({ login: login });
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
return res.status(500).send({ error: e.message || 'Internal Server Error' });
|
|
45
51
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
52
|
+
if (user) {
|
|
53
|
+
// User found in UserAP, validate password
|
|
54
|
+
if (req.adminizer.config.registration.confirmationRequired && user.isConfirmed === false && !user.isAdministrator) {
|
|
49
55
|
return inertiaAdminMessage(req, "Profile is not confirmed, please contact to administrator", 'captchaSolution');
|
|
50
56
|
}
|
|
51
57
|
if (passwordHash.verify(login + password + process.env.AP_PASSWORD_SALT, user.passwordHashed)) {
|
|
52
58
|
if (user.expires && Date.now() > Date.parse(user.expires)) {
|
|
53
|
-
//Here we use the captchaSolution key to output messages unrelated to the form fields.
|
|
54
59
|
return inertiaAdminMessage(req, "Profile expired, contact the administrator", 'captchaSolution');
|
|
55
60
|
}
|
|
56
61
|
const token = signUser(user, req.adminizer.jwtSecret);
|
|
@@ -66,6 +71,36 @@ export default async function login(req, res) {
|
|
|
66
71
|
return inertiaAdminMessage(req, "Wrong password", 'password');
|
|
67
72
|
}
|
|
68
73
|
}
|
|
74
|
+
// If user not found in UserAP and external auth handler is configured, try external auth
|
|
75
|
+
if (typeof req.adminizer.authHandler === 'function') {
|
|
76
|
+
try {
|
|
77
|
+
const extUser = await req.adminizer.authHandler(req, login, password);
|
|
78
|
+
if (!extUser) {
|
|
79
|
+
return inertiaAdminMessage(req, "Wrong username or password", 'login');
|
|
80
|
+
}
|
|
81
|
+
const externalUser = extUser;
|
|
82
|
+
if (req.adminizer.config.registration.confirmationRequired && externalUser.isConfirmed === false && !externalUser.isAdministrator) {
|
|
83
|
+
return inertiaAdminMessage(req, "Profile is not confirmed, please contact to administrator", 'captchaSolution');
|
|
84
|
+
}
|
|
85
|
+
if (externalUser.expires && Date.now() > Date.parse(externalUser.expires)) {
|
|
86
|
+
return inertiaAdminMessage(req, "Profile expired, contact the administrator", 'captchaSolution');
|
|
87
|
+
}
|
|
88
|
+
const token = signUser(externalUser, req.adminizer.jwtSecret);
|
|
89
|
+
res.setHeader('Set-Cookie', serialize('adminizer_jwt', token, {
|
|
90
|
+
httpOnly: true,
|
|
91
|
+
sameSite: 'lax',
|
|
92
|
+
path: '/',
|
|
93
|
+
maxAge: 60 * 60 * 24 * 7 * 2,
|
|
94
|
+
}));
|
|
95
|
+
return req.Inertia.redirect(`${req.adminizer.config.routePrefix}`);
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
Adminizer.log.error('External auth handler error', err);
|
|
99
|
+
return res.status(500).send({ error: 'Internal Server Error' });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// If neither UserAP nor external auth worked
|
|
103
|
+
return inertiaAdminMessage(req, "Wrong username or password", 'login');
|
|
69
104
|
}
|
|
70
105
|
if (req.method.toUpperCase() === "GET") {
|
|
71
106
|
// Generate new CAPTCHA task if enabled
|
|
@@ -9,7 +9,7 @@ export async function mediaManagerController(req, res) {
|
|
|
9
9
|
if (!req.user) {
|
|
10
10
|
return res.redirect(`${req.adminizer.config.routePrefix}/model/userap/login`);
|
|
11
11
|
}
|
|
12
|
-
else if (!req.adminizer.accessRightsHelper.hasPermission(`
|
|
12
|
+
else if (!req.adminizer.accessRightsHelper.hasPermission(`mediaManager-${id}`, req.user)) {
|
|
13
13
|
return res.sendStatus(403);
|
|
14
14
|
}
|
|
15
15
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare class NotificationController {
|
|
2
|
+
static viewAll(req: ReqType, res: ResType): Promise<void | import("express").Response<any, Record<string, any>>>;
|
|
3
|
+
static getNotificationsStream(req: ReqType, res: ResType): Promise<void>;
|
|
4
|
+
static getNotificationsByClass(req: ReqType, res: ResType): Promise<void>;
|
|
5
|
+
static getUserNotifications(req: ReqType, res: ResType): Promise<void>;
|
|
6
|
+
static getAllUserNotifications(req: ReqType, res: ResType): Promise<void>;
|
|
7
|
+
static markAsRead(req: ReqType, res: ResType): Promise<void>;
|
|
8
|
+
static sendNotification(req: ReqType, res: ResType): Promise<void>;
|
|
9
|
+
private static isAdmin;
|
|
10
|
+
private static checkPermission;
|
|
11
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { Adminizer } from '../../lib/Adminizer';
|
|
2
|
+
export class NotificationController {
|
|
3
|
+
static async viewAll(req, res) {
|
|
4
|
+
if (!req.user) {
|
|
5
|
+
return res.redirect(`${req.adminizer.config.routePrefix}/model/userap/login`);
|
|
6
|
+
}
|
|
7
|
+
return req.Inertia.render({
|
|
8
|
+
component: 'notification',
|
|
9
|
+
props: {
|
|
10
|
+
title: req.i18n.__('Notifications'),
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
// Единый SSE endpoint для всех уведомлений
|
|
15
|
+
static async getNotificationsStream(req, res) {
|
|
16
|
+
NotificationController.checkPermission(req, res);
|
|
17
|
+
// Устанавливаем заголовки для SSE
|
|
18
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
19
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
20
|
+
res.setHeader('Connection', 'keep-alive');
|
|
21
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
22
|
+
res.flushHeaders();
|
|
23
|
+
const clientId = `user-${req.user?.id}-${Date.now()}`;
|
|
24
|
+
// Функция для отправки событий клиенту
|
|
25
|
+
const sendEvent = (event) => {
|
|
26
|
+
// Фильтруем уведомления по правам пользователя
|
|
27
|
+
if (event.type === 'notification') {
|
|
28
|
+
// Системные уведомления только для админов
|
|
29
|
+
if (event.notificationClass === 'system' && !NotificationController.isAdmin(req.user)) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (event.userId !== null && event.userId !== req.user.id) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
res.write(`event: ${event.type}\n`);
|
|
37
|
+
res.write(`data: ${JSON.stringify(event.data)}\n\n`);
|
|
38
|
+
};
|
|
39
|
+
// Подключаем клиента ко всем сервисам
|
|
40
|
+
const services = req.adminizer.notificationHandler.getAllServices();
|
|
41
|
+
services.forEach(service => {
|
|
42
|
+
service.addClient(clientId, sendEvent);
|
|
43
|
+
// Для системного сервиса добавляем клиента в CRUD каналы
|
|
44
|
+
if (service.notificationClass === 'system' && NotificationController.isAdmin(req.user)) {
|
|
45
|
+
const systemService = service;
|
|
46
|
+
// Добавляем клиента в основные CRUD каналы
|
|
47
|
+
['created', 'updated', 'deleted', 'system'].forEach(channel => {
|
|
48
|
+
systemService.addClientToChannel(clientId, channel);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
// Отправляем приветственное сообщение
|
|
53
|
+
sendEvent({
|
|
54
|
+
type: 'connected',
|
|
55
|
+
data: {
|
|
56
|
+
message: 'Connected to unified notification stream',
|
|
57
|
+
clientId: clientId
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
// Отправляем историю уведомлений (с учетом прав доступа)
|
|
61
|
+
// try {
|
|
62
|
+
// const notifications = await req.adminizer.notificationHandler.getUserNotifications(
|
|
63
|
+
// req.user,
|
|
64
|
+
// 4,
|
|
65
|
+
// true
|
|
66
|
+
// );
|
|
67
|
+
// notifications.forEach(notification => {
|
|
68
|
+
// sendEvent({
|
|
69
|
+
// type: 'notification',
|
|
70
|
+
// data: notification,
|
|
71
|
+
// notificationClass: notification.notificationClass,
|
|
72
|
+
// userId: notification.userId ?? null
|
|
73
|
+
// });
|
|
74
|
+
// });
|
|
75
|
+
// } catch (error) {
|
|
76
|
+
// Adminizer.log.error('Error sending notification history:', error);
|
|
77
|
+
// }
|
|
78
|
+
// Обработка закрытия соединения
|
|
79
|
+
req.on('close', () => {
|
|
80
|
+
// Отключаем клиента от всех сервисов
|
|
81
|
+
services.forEach(service => {
|
|
82
|
+
service.removeClient(clientId);
|
|
83
|
+
// Для системного сервиса удаляем из всех каналов
|
|
84
|
+
if (service.notificationClass === 'system') {
|
|
85
|
+
const systemService = service;
|
|
86
|
+
if (systemService.removeClientFromAllChannels) {
|
|
87
|
+
systemService.removeClientFromAllChannels(clientId);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
res.end();
|
|
92
|
+
});
|
|
93
|
+
// Heartbeat для поддержания соединения
|
|
94
|
+
const heartbeatInterval = setInterval(() => {
|
|
95
|
+
if (!res.writableEnded) {
|
|
96
|
+
services.forEach(service => {
|
|
97
|
+
service.sendHeartbeat(clientId);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
clearInterval(heartbeatInterval);
|
|
102
|
+
}
|
|
103
|
+
}, 30000);
|
|
104
|
+
req.on('close', () => {
|
|
105
|
+
clearInterval(heartbeatInterval);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
// API для получения уведомлений по классу
|
|
109
|
+
static async getNotificationsByClass(req, res) {
|
|
110
|
+
NotificationController.checkPermission(req, res);
|
|
111
|
+
try {
|
|
112
|
+
const { notificationClass } = req.params;
|
|
113
|
+
const { limit = 50, unreadOnly = false } = req.query;
|
|
114
|
+
// Проверяем права доступа для системных уведомлений
|
|
115
|
+
if (notificationClass === 'system' && !NotificationController.isAdmin(req.user)) {
|
|
116
|
+
res.status(403).json({ error: 'Forbidden: Admin access required' });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const notifications = await req.adminizer.notificationHandler.getNotifications(notificationClass, req.user?.id, Number(limit), unreadOnly === 'true');
|
|
120
|
+
res.json(notifications);
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
Adminizer.log.error('Error getting notifications:', error);
|
|
124
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// API для получения всех уведомлений пользователя
|
|
128
|
+
static async getUserNotifications(req, res) {
|
|
129
|
+
NotificationController.checkPermission(req, res);
|
|
130
|
+
try {
|
|
131
|
+
const notifications = await req.adminizer.notificationHandler.getUserNotifications(req.user, 4, true);
|
|
132
|
+
res.json(notifications);
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
Adminizer.log.error('Error getting user notifications:', error);
|
|
136
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
static async getAllUserNotifications(req, res) {
|
|
140
|
+
NotificationController.checkPermission(req, res);
|
|
141
|
+
try {
|
|
142
|
+
const notifications = await req.adminizer.notificationHandler.getUserNotifications(req.user, 50, false);
|
|
143
|
+
res.json(notifications);
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
Adminizer.log.error('Error getting user notifications:', error);
|
|
147
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// API для пометки как прочитанного
|
|
151
|
+
static async markAsRead(req, res) {
|
|
152
|
+
NotificationController.checkPermission(req, res);
|
|
153
|
+
try {
|
|
154
|
+
const { notificationClass, id } = req.params;
|
|
155
|
+
// Проверяем права доступа для системных уведомлений
|
|
156
|
+
if (notificationClass === 'system' && !NotificationController.isAdmin(req.user)) {
|
|
157
|
+
res.status(403).json({ error: 'Forbidden: Admin access required' });
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
await req.adminizer.notificationHandler.markAsRead(notificationClass, id, req.user.id);
|
|
161
|
+
res.json({ success: true });
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
Adminizer.log.error('Error marking notification as read:', error);
|
|
165
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// API для отправки уведомления
|
|
169
|
+
static async sendNotification(req, res) {
|
|
170
|
+
NotificationController.checkPermission(req, res);
|
|
171
|
+
// Проверяем права админа
|
|
172
|
+
if (!NotificationController.isAdmin(req.user)) {
|
|
173
|
+
res.status(403).json({ error: 'Forbidden: Admin access required' });
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
const { title, message, type, priority, userId, channel, notificationClass = 'general' } = req.body;
|
|
178
|
+
const notificationId = await req.adminizer.notificationHandler.dispatchNotification(notificationClass, { title, message, userId, channel });
|
|
179
|
+
res.json({ success: true, id: notificationId });
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
Adminizer.log.error('Error sending notification:', error);
|
|
183
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// Проверка прав администратора
|
|
187
|
+
static isAdmin(user) {
|
|
188
|
+
if (!user)
|
|
189
|
+
return false;
|
|
190
|
+
return user.isAdministrator === true;
|
|
191
|
+
}
|
|
192
|
+
static checkPermission(req, res) {
|
|
193
|
+
if (!req.adminizer?.notificationHandler) {
|
|
194
|
+
res.status(500).json({ error: 'Notification system not initialized' });
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
// Проверяем аутентификацию
|
|
198
|
+
if (req.adminizer.config.auth.enable && !req.user) {
|
|
199
|
+
res.status(401).json({ error: 'Unauthorized' });
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
package/controllers/remove.js
CHANGED
|
@@ -67,6 +67,8 @@ export default async function remove(req, res) {
|
|
|
67
67
|
Adminizer.log.error('adminpanel > error', e);
|
|
68
68
|
}
|
|
69
69
|
if (destroyedRecord) {
|
|
70
|
+
// log system event notification
|
|
71
|
+
await req.adminizer.logSystemDeletedEvent(req.i18n.__('Deleted'), `user ${req.user.login} ${req.i18n.__('delete')} ${entity.name} ${destroyedRecord[0].id}`, { test: 'test' });
|
|
70
72
|
req.flash.setFlashMessage('success', req.i18n.__('Record was removed successfully !'));
|
|
71
73
|
}
|
|
72
74
|
else {
|
|
Binary file
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function sanitizeForDiff(data) {
|
|
2
|
+
if (!data)
|
|
3
|
+
return {};
|
|
4
|
+
const result = { ...data };
|
|
5
|
+
// Удаляем системные поля
|
|
6
|
+
const systemFields = ['id', 'createdAt', 'updatedAt', 'deletedAt', '__v', '_id'];
|
|
7
|
+
systemFields.forEach(field => delete result[field]);
|
|
8
|
+
// Очищаем чувствительные данные
|
|
9
|
+
const sensitiveFields = ['password', 'token', 'secret', 'apiKey', 'creditCard'];
|
|
10
|
+
sensitiveFields.forEach(field => {
|
|
11
|
+
if (result[field] !== undefined) {
|
|
12
|
+
result[field] = '***HIDDEN***';
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
export function formatChanges(diffObj, oldData, newData) {
|
|
18
|
+
return Object.entries(diffObj).map(([key, value]) => ({
|
|
19
|
+
field: key,
|
|
20
|
+
oldValue: oldData[key],
|
|
21
|
+
newValue: newData[key],
|
|
22
|
+
type: typeof value
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
@@ -11,6 +11,25 @@ export function inertiaLoginHelper(req) {
|
|
|
11
11
|
link: `${req.adminizer.config.routePrefix}/model/userap/register`
|
|
12
12
|
};
|
|
13
13
|
}
|
|
14
|
+
// Optional additional link at the bottom of login page
|
|
15
|
+
const addPage = req.adminizer.config.auth?.addishinalLoginPage;
|
|
16
|
+
if (addPage && typeof addPage.link === 'string' && addPage.link.length > 0) {
|
|
17
|
+
const raw = addPage.link;
|
|
18
|
+
let link = raw;
|
|
19
|
+
if (/^https?:\/\//i.test(raw)) {
|
|
20
|
+
link = raw; // absolute URL
|
|
21
|
+
}
|
|
22
|
+
else if (raw.startsWith(`/${req.adminizer.config.routePrefix}`)) {
|
|
23
|
+
link = `${raw}`;
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
link = `${req.adminizer.config.routePrefix}/${raw}`;
|
|
27
|
+
}
|
|
28
|
+
props.addishinalLoginPage = {
|
|
29
|
+
title: req.i18n.__(addPage.textKey || 'Additional login page'),
|
|
30
|
+
link,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
14
33
|
return props;
|
|
15
34
|
}
|
|
16
35
|
export function inertiaRegisterHelper(req) {
|
package/index.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export * from "./lib/v4/catalog/Navigation";
|
|
|
12
12
|
export * from "./lib/v4/catalog/CatalogHandler";
|
|
13
13
|
export * from "./lib/media-manager/MediaManagerHandler";
|
|
14
14
|
export * from "./interfaces/adminpanelConfig";
|
|
15
|
+
export * from "./interfaces/types";
|
|
16
|
+
export * from "./interfaces/MaaterialIcons";
|
|
15
17
|
export * from "./lib/v4/model/AbstractModel";
|
|
16
18
|
export * from "./lib/v4/model/adapter/waterline";
|
|
17
19
|
export * from "./lib/v4/model/adapter/typeorm";
|
package/index.js
CHANGED
|
@@ -12,6 +12,8 @@ export * from "./lib/v4/catalog/Navigation";
|
|
|
12
12
|
export * from "./lib/v4/catalog/CatalogHandler";
|
|
13
13
|
export * from "./lib/media-manager/MediaManagerHandler";
|
|
14
14
|
export * from "./interfaces/adminpanelConfig";
|
|
15
|
+
export * from "./interfaces/types";
|
|
16
|
+
export * from "./interfaces/MaaterialIcons";
|
|
15
17
|
export * from "./lib/v4/model/AbstractModel";
|
|
16
18
|
export * from "./lib/v4/model/adapter/waterline";
|
|
17
19
|
export * from "./lib/v4/model/adapter/typeorm";
|
|
@@ -36,6 +36,18 @@ export interface AdminpanelConfig {
|
|
|
36
36
|
captcha?: boolean;
|
|
37
37
|
/** Description displayed on login page */
|
|
38
38
|
description?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Optional additional link at the bottom of the login page
|
|
41
|
+
* Example:
|
|
42
|
+
* addishinalLoginPage: { link: '/model/userap/register', textKey: 'Additional login page' }
|
|
43
|
+
* link can be relative to routePrefix (e.g. 'model/help' or '/model/help') or absolute (https://...)
|
|
44
|
+
*/
|
|
45
|
+
addishinalLoginPage?: {
|
|
46
|
+
/** Target URL; relative to routePrefix unless absolute */
|
|
47
|
+
link: string;
|
|
48
|
+
/** Optional i18n key for label; defaults to 'Additional login page' */
|
|
49
|
+
textKey?: string;
|
|
50
|
+
};
|
|
39
51
|
};
|
|
40
52
|
/**
|
|
41
53
|
* @alpha
|
|
@@ -173,6 +185,12 @@ export interface AdminpanelConfig {
|
|
|
173
185
|
/** Default ORM adapter for system models */
|
|
174
186
|
defaultORM: string;
|
|
175
187
|
};
|
|
188
|
+
bind?: {
|
|
189
|
+
public: boolean;
|
|
190
|
+
};
|
|
191
|
+
notification?: {
|
|
192
|
+
enabled: boolean;
|
|
193
|
+
};
|
|
176
194
|
}
|
|
177
195
|
export interface ModelConfig {
|
|
178
196
|
adapter?: string;
|
package/interfaces/types.d.ts
CHANGED
|
@@ -25,3 +25,25 @@ export interface PropsField {
|
|
|
25
25
|
isIn?: string[] | number[] | boolean[];
|
|
26
26
|
options?: Record<string, unknown> | Record<string, unknown>[];
|
|
27
27
|
}
|
|
28
|
+
export interface INotification {
|
|
29
|
+
id: string;
|
|
30
|
+
title: string;
|
|
31
|
+
message: string;
|
|
32
|
+
createdAt: Date;
|
|
33
|
+
userId?: number;
|
|
34
|
+
read?: boolean;
|
|
35
|
+
icon: {
|
|
36
|
+
icon: string;
|
|
37
|
+
iconColor: string;
|
|
38
|
+
};
|
|
39
|
+
notificationClass: string;
|
|
40
|
+
channel: string;
|
|
41
|
+
metadata?: Record<string | number, any>;
|
|
42
|
+
}
|
|
43
|
+
export interface INotificationEvent {
|
|
44
|
+
type: 'notification' | 'heartbeat' | 'connected' | 'error';
|
|
45
|
+
data: INotification | string;
|
|
46
|
+
notificationClass?: string;
|
|
47
|
+
channel?: string;
|
|
48
|
+
userId?: number;
|
|
49
|
+
}
|
package/lib/Adminizer.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Express } from "express";
|
|
1
|
+
import express, { Express } from "express";
|
|
2
2
|
import winston from "winston";
|
|
3
3
|
import EventEmitter from 'events';
|
|
4
4
|
import { AdminpanelConfig } from "../interfaces/adminpanelConfig";
|
|
@@ -12,6 +12,10 @@ import type { ViteDevServer } from 'vite';
|
|
|
12
12
|
import { MenuHelper } from "../helpers/menuHelper";
|
|
13
13
|
import { ControlsHandler } from "./v4/controls/ControlsHandler";
|
|
14
14
|
import { CatalogHandler } from "./v4/catalog/CatalogHandler";
|
|
15
|
+
import { NotificationHandler } from './v4/notifications/NotificationHandler';
|
|
16
|
+
import { INotification } from "../interfaces/types";
|
|
17
|
+
import { UserAP } from "models/UserAP";
|
|
18
|
+
export type AuthHandler = (req: ReqType, login: string, password: string) => Promise<Partial<UserAP> | null>;
|
|
15
19
|
export declare class Adminizer {
|
|
16
20
|
/**
|
|
17
21
|
* If you convey this default Middleware, it will add it to the very top of the router,
|
|
@@ -26,21 +30,33 @@ export declare class Adminizer {
|
|
|
26
30
|
accessRightsHelper: AccessRightsHelper;
|
|
27
31
|
configHelper: ConfigHelper;
|
|
28
32
|
menuHelper: MenuHelper;
|
|
33
|
+
notificationHandler: NotificationHandler;
|
|
29
34
|
modelHandler: ModelHandler;
|
|
30
35
|
widgetHandler: WidgetHandler;
|
|
31
36
|
vite: ViteDevServer;
|
|
32
37
|
controlsHandler: ControlsHandler;
|
|
33
38
|
catalogHandler: CatalogHandler;
|
|
34
39
|
jwtSecret: string;
|
|
40
|
+
authHandler?: AuthHandler;
|
|
35
41
|
static logger: winston.Logger;
|
|
36
42
|
constructor(ormAdapters: AbstractAdapter[]);
|
|
37
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Registers an external authentication handler. If a user is not found in UserAP
|
|
45
|
+
* during login, this handler will be invoked to authenticate against an external source.
|
|
46
|
+
*/
|
|
47
|
+
setAuthHandler(handler: AuthHandler): void;
|
|
48
|
+
getMiddleware(): (req: express.Request, res: express.Response, next: () => void) => void;
|
|
38
49
|
/**
|
|
39
50
|
* Vite middleware
|
|
40
51
|
* @protected
|
|
41
52
|
*/
|
|
42
53
|
protected viteMiddleware(): Promise<void>;
|
|
43
54
|
init(config: AdminpanelConfig): Promise<void>;
|
|
55
|
+
sendNotification(notification: Omit<INotification, 'id' | 'createdAt' | 'icon'>): Promise<boolean>;
|
|
56
|
+
logSystemEvent(title: string, message: string, metadata?: any): Promise<boolean>;
|
|
57
|
+
logSystemCreatedEvent(title: string, message: string, metadata?: any): Promise<boolean>;
|
|
58
|
+
logSystemUpdatedEvent(title: string, message: string, metadata?: any): Promise<boolean>;
|
|
59
|
+
logSystemDeletedEvent(title: string, message: string, metadata?: any): Promise<boolean>;
|
|
44
60
|
get emitter(): EventEmitter;
|
|
45
61
|
getOrmAdapter(ormType: string): AbstractAdapter;
|
|
46
62
|
static get log(): {
|