adminforth 1.1.19 → 1.1.21
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/dist/index.js +101 -18
- package/dist/modules/codeInjector.js +1 -1
- package/dist/plugins/AccessControl/index.js +3 -56
- package/dist/plugins/AuditLog/index.js +66 -0
- package/dist/plugins/AuditLog/types.js +1 -0
- package/dist/plugins/ForeignInlineListPlugin/custom/InlineList.vue +3 -9
- package/dist/spa/spa/src/components/ResourceListTable.vue +39 -3
- package/dist/spa/spa/src/composables/useFrontendApi.ts +14 -0
- package/dist/spa/spa/src/utils.ts +2 -0
- package/dist/spa/spa/src/views/CreateView.vue +3 -5
- package/dist/spa/spa/src/views/EditView.vue +2 -5
- package/dist/spa/spa/src/views/ListView.vue +4 -33
- package/index.ts +124 -18
- package/modules/codeInjector.ts +1 -1
- package/package.json +1 -1
- package/plugins/AccessControl/index.ts +1 -55
- package/plugins/AccessControl/types.ts +1 -9
- package/plugins/ForeignInlineListPlugin/custom/InlineList.vue +3 -9
- package/spa/src/components/ResourceListTable.vue +39 -3
- package/spa/src/composables/useFrontendApi.ts +14 -0
- package/spa/src/utils.ts +2 -0
- package/spa/src/views/CreateView.vue +3 -5
- package/spa/src/views/EditView.vue +2 -5
- package/spa/src/views/ListView.vue +4 -33
- package/types/AdminForthConfig.ts +12 -1
package/dist/index.js
CHANGED
|
@@ -23,10 +23,9 @@ import ExpressServer from './servers/express.js';
|
|
|
23
23
|
import { v1 as uuid } from 'uuid';
|
|
24
24
|
import fs from 'fs';
|
|
25
25
|
import { ADMINFORTH_VERSION, listify } from './modules/utils.js';
|
|
26
|
-
import { AdminForthFilterOperators, AdminForthDataTypes, AdminForthResourcePages } from './types/AdminForthConfig.js';
|
|
26
|
+
import { AdminForthFilterOperators, AdminForthDataTypes, AdminForthResourcePages, AllowedActionsEnum } from './types/AdminForthConfig.js';
|
|
27
27
|
import path from 'path';
|
|
28
28
|
//get array from enum AdminForthResourcePages
|
|
29
|
-
const DEFAULT_ALLOWED_ACTIONS = { create: true, edit: true, show: true, delete: true };
|
|
30
29
|
class AdminForth {
|
|
31
30
|
constructor(config) {
|
|
32
31
|
_AdminForth_defaultConfig.set(this, {
|
|
@@ -211,6 +210,30 @@ class AdminForth {
|
|
|
211
210
|
if (!res.options) {
|
|
212
211
|
res.options = { bulkActions: [], allowedActions: {} };
|
|
213
212
|
}
|
|
213
|
+
if (!res.options.allowedActions) {
|
|
214
|
+
res.options.allowedActions = {
|
|
215
|
+
all: true,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
if (Object.keys(res.options.allowedActions).includes('all')) {
|
|
219
|
+
if (Object.keys(res.options.allowedActions).length > 1) {
|
|
220
|
+
errors.push(`Resource "${res.resourceId}" allowedActions cannot have "all" and other keys at same time: ${Object.keys(res.options.allowedActions).join(', ')}`);
|
|
221
|
+
}
|
|
222
|
+
for (const key of Object.keys(AllowedActionsEnum)) {
|
|
223
|
+
if (key !== 'all') {
|
|
224
|
+
res.options.allowedActions[key] = res.options.allowedActions.all;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
delete res.options.allowedActions.all;
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
// by default allow all actions
|
|
231
|
+
for (const key of Object.keys(AllowedActionsEnum)) {
|
|
232
|
+
if (!Object.keys(res.options.allowedActions).includes(key)) {
|
|
233
|
+
res.options.allowedActions[key] = true;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
214
237
|
//check if resource has bulkActions
|
|
215
238
|
if ((_b = res.options) === null || _b === void 0 ? void 0 : _b.bulkActions) {
|
|
216
239
|
let bulkActions = res.options.bulkActions;
|
|
@@ -256,18 +279,6 @@ class AdminForth {
|
|
|
256
279
|
});
|
|
257
280
|
}
|
|
258
281
|
}
|
|
259
|
-
//add default allowedActions to resources
|
|
260
|
-
if (res.options.allowedActions) {
|
|
261
|
-
//check if allowedActions is an object
|
|
262
|
-
if (typeof res.options.allowedActions !== 'object') {
|
|
263
|
-
errors.push(`Resource "${res.resourceId}" allowedActions must be an object`);
|
|
264
|
-
}
|
|
265
|
-
const userAllowedActions = res.options.allowedActions;
|
|
266
|
-
res.options.allowedActions = Object.assign({}, DEFAULT_ALLOWED_ACTIONS, userAllowedActions);
|
|
267
|
-
}
|
|
268
|
-
else {
|
|
269
|
-
res.options.allowedActions = DEFAULT_ALLOWED_ACTIONS;
|
|
270
|
-
}
|
|
271
282
|
// transform all hooks Functions to array of functions
|
|
272
283
|
if (res.hooks) {
|
|
273
284
|
for (const value of [res.hooks.show, res.hooks.list]) {
|
|
@@ -568,13 +579,45 @@ class AdminForth {
|
|
|
568
579
|
[this.config.auth.usernameField]: username,
|
|
569
580
|
[this.config.auth.userFullNameField]: userFullName
|
|
570
581
|
};
|
|
582
|
+
const checkIsMenuItemVisible = (menuItem) => {
|
|
583
|
+
if (typeof menuItem.visible === 'function') {
|
|
584
|
+
const toReturn = menuItem.visible(adminUser);
|
|
585
|
+
if (typeof toReturn !== 'boolean') {
|
|
586
|
+
throw new Error(`'visible' function of ${menuItem.label || menuItem.type} must return boolean value`);
|
|
587
|
+
}
|
|
588
|
+
return toReturn;
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
let newMenu = [];
|
|
592
|
+
for (let menuItem of this.config.menu) {
|
|
593
|
+
let newMenuItem = Object.assign({}, menuItem);
|
|
594
|
+
if (menuItem.visible) {
|
|
595
|
+
if (!checkIsMenuItemVisible(menuItem)) {
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
if (menuItem.children) {
|
|
600
|
+
let newChildren = [];
|
|
601
|
+
for (let child of menuItem.children) {
|
|
602
|
+
let newChild = Object.assign({}, child);
|
|
603
|
+
if (child.visible) {
|
|
604
|
+
if (!checkIsMenuItemVisible(child)) {
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
newChildren.push(newChild);
|
|
609
|
+
}
|
|
610
|
+
newMenuItem = Object.assign(Object.assign({}, newMenuItem), { children: newChildren });
|
|
611
|
+
}
|
|
612
|
+
newMenu.push(newMenuItem);
|
|
613
|
+
}
|
|
571
614
|
return {
|
|
572
615
|
user: userData,
|
|
573
616
|
resources: this.config.resources.map((res) => ({
|
|
574
617
|
resourceId: res.resourceId,
|
|
575
618
|
label: res.label,
|
|
576
619
|
})),
|
|
577
|
-
menu:
|
|
620
|
+
menu: newMenu,
|
|
578
621
|
config: {
|
|
579
622
|
brandName: this.config.customization.brandName,
|
|
580
623
|
brandLogo: this.config.customization.brandLogo,
|
|
@@ -590,10 +633,29 @@ class AdminForth {
|
|
|
590
633
|
};
|
|
591
634
|
}),
|
|
592
635
|
});
|
|
636
|
+
function interpretResource(adminUser, resource, meta) {
|
|
637
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
638
|
+
var _b;
|
|
639
|
+
yield Promise.all(Object.entries(((_b = resource.options) === null || _b === void 0 ? void 0 : _b.allowedActions) || {}).map((_c) => __awaiter(this, [_c], void 0, function* ([key, value]) {
|
|
640
|
+
// if callable then call
|
|
641
|
+
if (typeof value === 'function') {
|
|
642
|
+
resource.options.allowedActions[key] = yield value(adminUser, resource, meta);
|
|
643
|
+
}
|
|
644
|
+
})));
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
function checkAccess(action, resource) {
|
|
648
|
+
var _b;
|
|
649
|
+
const allowed = (_b = resource.options) === null || _b === void 0 ? void 0 : _b.allowedActions[action];
|
|
650
|
+
if (allowed !== true) {
|
|
651
|
+
return { error: typeof allowed === 'string' ? allowed : 'Action is not allowed', allowed: false };
|
|
652
|
+
}
|
|
653
|
+
return { allowed: true };
|
|
654
|
+
}
|
|
593
655
|
server.endpoint({
|
|
594
656
|
method: 'POST',
|
|
595
657
|
path: '/get_resource',
|
|
596
|
-
handler: (_l) => __awaiter(this, [_l], void 0, function* ({ body }) {
|
|
658
|
+
handler: (_l) => __awaiter(this, [_l], void 0, function* ({ body, adminUser }) {
|
|
597
659
|
const { resourceId } = body;
|
|
598
660
|
if (!this.statuses.dbDiscover) {
|
|
599
661
|
return { error: 'Database discovery not started' };
|
|
@@ -605,6 +667,7 @@ class AdminForth {
|
|
|
605
667
|
if (!resource) {
|
|
606
668
|
return { error: `Resource ${resourceId} not found` };
|
|
607
669
|
}
|
|
670
|
+
yield interpretResource(adminUser, resource, {});
|
|
608
671
|
// exclude "plugins" key
|
|
609
672
|
return { resource: Object.assign(Object.assign({}, resource), { plugins: undefined }) };
|
|
610
673
|
}),
|
|
@@ -628,6 +691,12 @@ class AdminForth {
|
|
|
628
691
|
if (!resource) {
|
|
629
692
|
return { error: `Resource ${resourceId} not found` };
|
|
630
693
|
}
|
|
694
|
+
yield interpretResource(adminUser, resource, { requestBody: body });
|
|
695
|
+
const { allowed, error } = checkAccess(source, resource);
|
|
696
|
+
console.log('allowed', allowed, error);
|
|
697
|
+
if (!allowed) {
|
|
698
|
+
return { error };
|
|
699
|
+
}
|
|
631
700
|
for (const hook of listify((_p = (_o = resource.hooks) === null || _o === void 0 ? void 0 : _o[source]) === null || _p === void 0 ? void 0 : _p.beforeDatasourceRequest)) {
|
|
632
701
|
const resp = yield hook({ resource, query: body, adminUser });
|
|
633
702
|
if (!resp || (!resp.ok && !resp.error)) {
|
|
@@ -647,7 +716,7 @@ class AdminForth {
|
|
|
647
716
|
}
|
|
648
717
|
if (filter.operator === AdminForthFilterOperators.IN || filter.operator === AdminForthFilterOperators.NIN) {
|
|
649
718
|
if (!Array.isArray(filter.value)) {
|
|
650
|
-
throw new Error(`Value for operator '${filter.operator}'
|
|
719
|
+
throw new Error(`Value for operator '${filter.operator}' should be an array`);
|
|
651
720
|
}
|
|
652
721
|
}
|
|
653
722
|
if (filter.operator === AdminForthFilterOperators.IN && filter.value.length === 0) {
|
|
@@ -818,11 +887,15 @@ class AdminForth {
|
|
|
818
887
|
path: '/create_record',
|
|
819
888
|
handler: (_y) => __awaiter(this, [_y], void 0, function* ({ body, adminUser }) {
|
|
820
889
|
var _z, _0, _1, _2, _3;
|
|
821
|
-
console.log('create_record', body, this.config.resources);
|
|
822
890
|
const resource = this.config.resources.find((res) => res.resourceId == body['resourceId']);
|
|
823
891
|
if (!resource) {
|
|
824
892
|
return { error: `Resource '${body['resourceId']}' not found` };
|
|
825
893
|
}
|
|
894
|
+
yield interpretResource(adminUser, resource, { requestBody: body });
|
|
895
|
+
const { allowed, error } = checkAccess(AllowedActionsEnum.create, resource);
|
|
896
|
+
if (!allowed) {
|
|
897
|
+
return { error };
|
|
898
|
+
}
|
|
826
899
|
const record = body['record'];
|
|
827
900
|
// execute hook if needed
|
|
828
901
|
for (const hook of listify((_0 = (_z = resource.hooks) === null || _z === void 0 ? void 0 : _z.create) === null || _0 === void 0 ? void 0 : _0.beforeSave)) {
|
|
@@ -890,6 +963,11 @@ class AdminForth {
|
|
|
890
963
|
if (!resource) {
|
|
891
964
|
return { error: `Resource '${body['resourceId']}' not found` };
|
|
892
965
|
}
|
|
966
|
+
yield interpretResource(adminUser, resource, { requestBody: body });
|
|
967
|
+
const { allowed, error } = checkAccess(AllowedActionsEnum.edit, resource);
|
|
968
|
+
if (!allowed) {
|
|
969
|
+
return { error };
|
|
970
|
+
}
|
|
893
971
|
const recordId = body['recordId'];
|
|
894
972
|
const connector = this.connectors[resource.dataSource];
|
|
895
973
|
const oldRecord = yield connector.getRecordByPrimaryKey(resource, recordId);
|
|
@@ -957,6 +1035,11 @@ class AdminForth {
|
|
|
957
1035
|
if (resource.options.allowedActions.delete === false) {
|
|
958
1036
|
return { error: `Resource '${resource.resourceId}' does not allow delete action` };
|
|
959
1037
|
}
|
|
1038
|
+
yield interpretResource(adminUser, resource, { requestBody: body });
|
|
1039
|
+
const { allowed, error } = checkAccess(AllowedActionsEnum.delete, resource);
|
|
1040
|
+
if (!allowed) {
|
|
1041
|
+
return { error };
|
|
1042
|
+
}
|
|
960
1043
|
// execute hook if needed
|
|
961
1044
|
for (const hook of listify((_11 = (_10 = resource.hooks) === null || _10 === void 0 ? void 0 : _10.delete) === null || _11 === void 0 ? void 0 : _11.beforeSave)) {
|
|
962
1045
|
const resp = yield hook({ resource, record, adminUser });
|
|
@@ -140,7 +140,7 @@ class CodeInjector {
|
|
|
140
140
|
routes += `{
|
|
141
141
|
path: '${item.path}',
|
|
142
142
|
name: '${item.path}',
|
|
143
|
-
component: () => import('${item.component}'),
|
|
143
|
+
component: () => import('${item.component}'),
|
|
144
144
|
meta: { title: '${(_d = item === null || item === void 0 ? void 0 : item.meta) === null || _d === void 0 ? void 0 : _d.title}'
|
|
145
145
|
},\n`;
|
|
146
146
|
}
|
|
@@ -1,15 +1,5 @@
|
|
|
1
|
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
-
});
|
|
9
|
-
};
|
|
10
|
-
import { AllowedActionsEnum } from "../../types/AdminForthConfig.js";
|
|
11
1
|
import AdminForthPlugin from "../base.js";
|
|
12
|
-
class
|
|
2
|
+
class AuditLogPlugin extends AdminForthPlugin {
|
|
13
3
|
constructor(options) {
|
|
14
4
|
super(options, import.meta.url);
|
|
15
5
|
this.options = options;
|
|
@@ -17,50 +7,7 @@ class AccessControlPlugin extends AdminForthPlugin {
|
|
|
17
7
|
modifyResourceConfig(adminforth, resourceConfig) {
|
|
18
8
|
super.modifyResourceConfig(adminforth, resourceConfig);
|
|
19
9
|
this.adminforth = adminforth;
|
|
20
|
-
if (!resourceConfig.hooks) {
|
|
21
|
-
resourceConfig.hooks = {};
|
|
22
|
-
}
|
|
23
|
-
const checkAccess = (adminUser, action, meta) => __awaiter(this, void 0, void 0, function* () {
|
|
24
|
-
const hasAccessOrError = yield this.options.hasAccess(adminUser, action, meta);
|
|
25
|
-
if (hasAccessOrError === true) {
|
|
26
|
-
return { ok: true };
|
|
27
|
-
}
|
|
28
|
-
else {
|
|
29
|
-
return { ok: false, error: hasAccessOrError || AccessControlPlugin.defaultError };
|
|
30
|
-
}
|
|
31
|
-
});
|
|
32
|
-
const bindHookCheck = (action, hookName) => {
|
|
33
|
-
if (!resourceConfig.hooks[action]) {
|
|
34
|
-
resourceConfig.hooks[action] = {};
|
|
35
|
-
}
|
|
36
|
-
if (!resourceConfig.hooks[action][hookName]) {
|
|
37
|
-
resourceConfig.hooks[action][hookName] = [];
|
|
38
|
-
}
|
|
39
|
-
if (hookName === 'beforeDatasourceRequest') {
|
|
40
|
-
resourceConfig.hooks[action][hookName].unshift((_a) => __awaiter(this, [_a], void 0, function* ({ adminUser, query }) {
|
|
41
|
-
return checkAccess(adminUser, action, { query });
|
|
42
|
-
}));
|
|
43
|
-
}
|
|
44
|
-
else {
|
|
45
|
-
resourceConfig.hooks[action][hookName].unshift((_b) => __awaiter(this, [_b], void 0, function* ({ adminUser, record }) {
|
|
46
|
-
return checkAccess(adminUser, action, { record });
|
|
47
|
-
}));
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
// List check
|
|
51
|
-
bindHookCheck(AllowedActionsEnum.list, 'beforeDatasourceRequest');
|
|
52
|
-
// Show check
|
|
53
|
-
bindHookCheck(AllowedActionsEnum.show, 'beforeDatasourceRequest');
|
|
54
|
-
// Edit check
|
|
55
|
-
bindHookCheck(AllowedActionsEnum.edit, 'beforeDatasourceRequest');
|
|
56
|
-
// create check
|
|
57
|
-
bindHookCheck(AllowedActionsEnum.create, 'beforeSave');
|
|
58
|
-
// edit check
|
|
59
|
-
bindHookCheck(AllowedActionsEnum.edit, 'beforeSave');
|
|
60
|
-
// delete check
|
|
61
|
-
bindHookCheck(AllowedActionsEnum.delete, 'beforeSave');
|
|
62
|
-
console.log('resourceConfig', resourceConfig);
|
|
63
10
|
}
|
|
64
11
|
}
|
|
65
|
-
|
|
66
|
-
export default
|
|
12
|
+
AuditLogPlugin.defaultError = 'Sorry, you do not have access to this resource.';
|
|
13
|
+
export default AuditLogPlugin;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { AllowedActionsEnum } from "../../types/AdminForthConfig.js";
|
|
11
|
+
import AdminForthPlugin from "../base.js";
|
|
12
|
+
class AccessControlPlugin extends AdminForthPlugin {
|
|
13
|
+
constructor(options) {
|
|
14
|
+
super(options, import.meta.url);
|
|
15
|
+
this.options = options;
|
|
16
|
+
}
|
|
17
|
+
modifyResourceConfig(adminforth, resourceConfig) {
|
|
18
|
+
super.modifyResourceConfig(adminforth, resourceConfig);
|
|
19
|
+
this.adminforth = adminforth;
|
|
20
|
+
if (!resourceConfig.hooks) {
|
|
21
|
+
resourceConfig.hooks = {};
|
|
22
|
+
}
|
|
23
|
+
const checkAccess = (adminUser, action, meta) => __awaiter(this, void 0, void 0, function* () {
|
|
24
|
+
const hasAccessOrError = yield this.options.hasAccess(adminUser, action, meta);
|
|
25
|
+
if (hasAccessOrError === true) {
|
|
26
|
+
return { ok: true };
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
return { ok: false, error: hasAccessOrError || AccessControlPlugin.defaultError };
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
const bindHookCheck = (action, hookName) => {
|
|
33
|
+
if (!resourceConfig.hooks[action]) {
|
|
34
|
+
resourceConfig.hooks[action] = {};
|
|
35
|
+
}
|
|
36
|
+
if (!resourceConfig.hooks[action][hookName]) {
|
|
37
|
+
resourceConfig.hooks[action][hookName] = [];
|
|
38
|
+
}
|
|
39
|
+
if (hookName === 'beforeDatasourceRequest') {
|
|
40
|
+
resourceConfig.hooks[action][hookName].unshift((_a) => __awaiter(this, [_a], void 0, function* ({ adminUser, query }) {
|
|
41
|
+
return checkAccess(adminUser, action, { query });
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
resourceConfig.hooks[action][hookName].unshift((_b) => __awaiter(this, [_b], void 0, function* ({ adminUser, record }) {
|
|
46
|
+
return checkAccess(adminUser, action, { record });
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
// List check
|
|
51
|
+
bindHookCheck(AllowedActionsEnum.list, 'beforeDatasourceRequest');
|
|
52
|
+
// Show check
|
|
53
|
+
bindHookCheck(AllowedActionsEnum.show, 'beforeDatasourceRequest');
|
|
54
|
+
// Edit check
|
|
55
|
+
bindHookCheck(AllowedActionsEnum.edit, 'beforeDatasourceRequest');
|
|
56
|
+
// create check
|
|
57
|
+
bindHookCheck(AllowedActionsEnum.create, 'beforeSave');
|
|
58
|
+
// edit check
|
|
59
|
+
bindHookCheck(AllowedActionsEnum.edit, 'beforeSave');
|
|
60
|
+
// delete check
|
|
61
|
+
bindHookCheck(AllowedActionsEnum.delete, 'beforeSave');
|
|
62
|
+
console.log('resourceConfig', resourceConfig);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
AccessControlPlugin.defaultError = 'Sorry, you do not have access to this resource.';
|
|
66
|
+
export default AccessControlPlugin;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -104,6 +104,7 @@ import {
|
|
|
104
104
|
IconFilterOutline,
|
|
105
105
|
IconPlusOutline,
|
|
106
106
|
} from '@iconify-prerendered/vue-flowbite';
|
|
107
|
+
import { showErrorTost, showWarningTost, showSuccesTost} from '@/composables/useFrontendApi';
|
|
107
108
|
|
|
108
109
|
import { getIcon } from '@/utils';
|
|
109
110
|
|
|
@@ -145,10 +146,7 @@ const endFilters = computed(() => {
|
|
|
145
146
|
const primaryKeyColumn = selfPrimaryKeyColumn.value;
|
|
146
147
|
|
|
147
148
|
if (!refColumn) {
|
|
148
|
-
|
|
149
|
-
message: `Column with foreignResource.resourceId which is equal to '${props.resource.resourceId}' not found in resource which is specified as foreighResourceId '${listResource.value.resourceId}'`,
|
|
150
|
-
variant: 'danger',
|
|
151
|
-
});
|
|
149
|
+
showErrorTost(`Column with foreignResource.resourceId which is equal to '${props.resource.resourceId}' not found in resource which is specified as foreighResourceId '${listResource.value.resourceId}'`,10000);
|
|
152
150
|
return [];
|
|
153
151
|
}
|
|
154
152
|
return [
|
|
@@ -209,11 +207,7 @@ async function getList() {
|
|
|
209
207
|
});
|
|
210
208
|
|
|
211
209
|
if (data.error) {
|
|
212
|
-
|
|
213
|
-
message: data.error,
|
|
214
|
-
variant: 'danger',
|
|
215
|
-
timeout: 'unlimited',
|
|
216
|
-
});
|
|
210
|
+
showErrorTost(data.error);
|
|
217
211
|
rows.value = [];
|
|
218
212
|
totalRows.value = 0;
|
|
219
213
|
return;
|
|
@@ -190,7 +190,7 @@
|
|
|
190
190
|
<button v-if="resource.options?.allowedActions.delete"
|
|
191
191
|
class="font-medium text-red-600 dark:text-red-500 hover:underline ms-3"
|
|
192
192
|
:data-tooltip-target="`tooltip-delete-${rowI}`"
|
|
193
|
-
@click="
|
|
193
|
+
@click="deleteRecord(row)"
|
|
194
194
|
>
|
|
195
195
|
<IconTrashBinSolid class="w-5 h-5 me-2"/>
|
|
196
196
|
</button>
|
|
@@ -272,10 +272,12 @@
|
|
|
272
272
|
import { initFlowbite } from 'flowbite';
|
|
273
273
|
import { computed, onMounted, ref, watch } from 'vue';
|
|
274
274
|
import { useRoute } from 'vue-router';
|
|
275
|
+
import { callAdminForthApi, getIcon } from '@/utils';
|
|
275
276
|
|
|
276
277
|
import ValueRenderer from '@/components/ValueRenderer.vue';
|
|
277
278
|
import { getCustomComponent } from '@/utils';
|
|
278
279
|
import { useCoreStore } from '@/stores/core';
|
|
280
|
+
import { showSuccesTost } from '@/composables/useFrontendApi';
|
|
279
281
|
|
|
280
282
|
import {
|
|
281
283
|
IconInboxOutline,
|
|
@@ -305,6 +307,7 @@ const emits = defineEmits([
|
|
|
305
307
|
'update:page',
|
|
306
308
|
'update:sort',
|
|
307
309
|
'update:checkboxes',
|
|
310
|
+
'update:records'
|
|
308
311
|
|
|
309
312
|
]);
|
|
310
313
|
|
|
@@ -358,9 +361,11 @@ async function selectAll(value) {
|
|
|
358
361
|
const totalPages = computed(() => Math.ceil(props.totalRows / props.pageSize));
|
|
359
362
|
|
|
360
363
|
|
|
364
|
+
|
|
365
|
+
|
|
361
366
|
const allFromThisPageChecked = computed(() => {
|
|
362
|
-
if (!props.rows
|
|
363
|
-
return props.rows.
|
|
367
|
+
if (!props.rows) return false;
|
|
368
|
+
return props.rows.every((r) => props.checkboxes.includes(r.id));
|
|
364
369
|
});
|
|
365
370
|
const ascArr = computed(() => sort.value.filter((s) => s.direction === 'asc').map((s) => s.field));
|
|
366
371
|
const descArr = computed(() => sort.value.filter((s) => s.direction === 'desc').map((s) => s.field));
|
|
@@ -380,4 +385,35 @@ function onSortButtonClick(field) {
|
|
|
380
385
|
}
|
|
381
386
|
}
|
|
382
387
|
|
|
388
|
+
async function deleteRecord(row) {
|
|
389
|
+
const data = await window.adminforth.confirm({
|
|
390
|
+
message: 'Are you sure you want to delete this item?',
|
|
391
|
+
yes: 'Delete',
|
|
392
|
+
no: 'Cancel',
|
|
393
|
+
});
|
|
394
|
+
if (data) {
|
|
395
|
+
try {
|
|
396
|
+
const res = await callAdminForthApi({
|
|
397
|
+
path: '/delete_record',
|
|
398
|
+
method: 'POST',
|
|
399
|
+
body: {
|
|
400
|
+
resourceId: props.resource.resourceId,
|
|
401
|
+
primaryKey: row._primaryKeyValue,
|
|
402
|
+
}});
|
|
403
|
+
if (!res.error){
|
|
404
|
+
emits('update:records', true)
|
|
405
|
+
showSuccesTost('Record deleted successfully')
|
|
406
|
+
} else {
|
|
407
|
+
console.error(res.error)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
} catch (e) {
|
|
411
|
+
console.error(e);
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
|
|
383
419
|
</script>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function showSuccesTost(message: string) {
|
|
2
|
+
window.adminforth.alert({ message, variant: 'success' });
|
|
3
|
+
return message;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function showWarningTost(message: string) {
|
|
7
|
+
window.adminforth.alert({ message, variant: 'warning' });
|
|
8
|
+
return message;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function showErrorTost(message: string, timeout?: number) {
|
|
12
|
+
window.adminforth.alert({ message, variant: 'danger', timeout: timeout || 'unlimited'});
|
|
13
|
+
return message;
|
|
14
|
+
}
|
|
@@ -78,6 +78,7 @@ import { IconFloppyDiskSolid } from '@iconify-prerendered/vue-flowbite';
|
|
|
78
78
|
import { onMounted, ref, watch } from 'vue';
|
|
79
79
|
import { useRoute, useRouter } from 'vue-router';
|
|
80
80
|
import { computed } from 'vue';
|
|
81
|
+
import { showErrorTost } from '@/composables/useFrontendApi';
|
|
81
82
|
|
|
82
83
|
|
|
83
84
|
const isValid = ref(false);
|
|
@@ -141,11 +142,8 @@ async function saveRecord() {
|
|
|
141
142
|
},
|
|
142
143
|
});
|
|
143
144
|
if (response.error) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
variant: 'danger',
|
|
147
|
-
timeout: 'unlimited',
|
|
148
|
-
})
|
|
145
|
+
showErrorTost(response.error);
|
|
146
|
+
|
|
149
147
|
}
|
|
150
148
|
saving.value = false;
|
|
151
149
|
if (route.query.returnTo) {
|
|
@@ -73,6 +73,7 @@ import { callAdminForthApi, getCustomComponent,checkAcessByAllowedActions } from
|
|
|
73
73
|
import { IconFloppyDiskSolid } from '@iconify-prerendered/vue-flowbite';
|
|
74
74
|
import { computed, onMounted, ref } from 'vue';
|
|
75
75
|
import { useRoute, useRouter } from 'vue-router';
|
|
76
|
+
import { showErrorTost } from '@/composables/useFrontendApi';
|
|
76
77
|
|
|
77
78
|
const coreStore = useCoreStore();
|
|
78
79
|
|
|
@@ -155,11 +156,7 @@ async function saveRecord() {
|
|
|
155
156
|
},
|
|
156
157
|
});
|
|
157
158
|
if (resp.error) {
|
|
158
|
-
|
|
159
|
-
message: resp.error,
|
|
160
|
-
variant: 'danger',
|
|
161
|
-
timeout: 'unlimited',
|
|
162
|
-
})
|
|
159
|
+
showErrorTost(resp.error);
|
|
163
160
|
}
|
|
164
161
|
saving.value = false;
|
|
165
162
|
router.push({ name: 'resource-show', params: { resourceId: route.params.resourceId, primaryKey: coreStore.record[coreStore.primaryKey] } });
|
|
@@ -87,6 +87,7 @@
|
|
|
87
87
|
@update:page="page = $event"
|
|
88
88
|
@update:sort="sort = $event"
|
|
89
89
|
@update:checkboxes="checkboxes = $event"
|
|
90
|
+
@update:records="getList"
|
|
90
91
|
:pageSize="pageSize"
|
|
91
92
|
:totalRows="totalRows"
|
|
92
93
|
:checkboxes="checkboxes"
|
|
@@ -113,6 +114,7 @@ import { callAdminForthApi, getIcon } from '@/utils';
|
|
|
113
114
|
import { initFlowbite } from 'flowbite';
|
|
114
115
|
import { computed, onMounted, ref, watch } from 'vue';
|
|
115
116
|
import { useRoute } from 'vue-router';
|
|
117
|
+
import { showErrorTost } from '@/composables/useFrontendApi'
|
|
116
118
|
|
|
117
119
|
import ValueRenderer from '@/components/ValueRenderer.vue';
|
|
118
120
|
import { getCustomComponent } from '@/utils';
|
|
@@ -184,11 +186,7 @@ async function getList() {
|
|
|
184
186
|
}
|
|
185
187
|
});
|
|
186
188
|
if (data.error) {
|
|
187
|
-
|
|
188
|
-
message: data.error,
|
|
189
|
-
variant: 'danger',
|
|
190
|
-
timeout: 'unlimited',
|
|
191
|
-
});
|
|
189
|
+
showErrorTost(data.error);
|
|
192
190
|
rows.value = [];
|
|
193
191
|
totalRows.value = 0;
|
|
194
192
|
return;
|
|
@@ -201,36 +199,9 @@ async function getList() {
|
|
|
201
199
|
}
|
|
202
200
|
|
|
203
201
|
|
|
204
|
-
function showDeleteModal(row) {
|
|
205
|
-
if (!coreStore.config?.deleteConfirmation) {
|
|
206
|
-
return deleteRecord(row);
|
|
207
|
-
}
|
|
208
|
-
modalStore.setModalContent({
|
|
209
|
-
content: 'Are you sure you want to delete this item?',
|
|
210
|
-
acceptText: 'Delete',
|
|
211
|
-
cancelText: 'Cancel',
|
|
212
|
-
});
|
|
213
|
-
modalStore.setOnAcceptFunction(() => {
|
|
214
|
-
return deleteRecord(row)
|
|
215
|
-
})
|
|
216
|
-
console.log('row', row);
|
|
217
|
-
modalStore.togleModal();
|
|
218
202
|
|
|
219
|
-
}
|
|
220
203
|
|
|
221
|
-
|
|
222
|
-
await callAdminForthApi({
|
|
223
|
-
path: '/delete_record',
|
|
224
|
-
method: 'POST',
|
|
225
|
-
body: {
|
|
226
|
-
resourceId: route.params.resourceId,
|
|
227
|
-
primaryKey: row._primaryKeyValue,
|
|
228
|
-
recordId: row.id
|
|
229
|
-
}
|
|
230
|
-
});
|
|
231
|
-
await getList();
|
|
232
|
-
modalStore.resetmodalState()
|
|
233
|
-
}
|
|
204
|
+
|
|
234
205
|
|
|
235
206
|
async function startBulkAction(actionId) {
|
|
236
207
|
const data = await callAdminForthApi({
|
package/index.ts
CHANGED
|
@@ -14,14 +14,17 @@ import { AdminForthConfig, AdminForthClass, AdminForthComponentDeclaration, Admi
|
|
|
14
14
|
BeforeSaveFunction,
|
|
15
15
|
AfterSaveFunction,
|
|
16
16
|
BeforeDataSourceRequestFunction,
|
|
17
|
-
AfterDataSourceResponseFunction
|
|
17
|
+
AfterDataSourceResponseFunction, AllowedActionValue, AdminUser,
|
|
18
|
+
AdminForthResource,
|
|
19
|
+
AllowedActionsEnum
|
|
20
|
+
} from './types/AdminForthConfig.js';
|
|
18
21
|
import path from 'path';
|
|
19
22
|
import AdminForthPlugin from './plugins/base.js';
|
|
23
|
+
import { tr } from '@faker-js/faker';
|
|
20
24
|
|
|
21
25
|
|
|
22
26
|
//get array from enum AdminForthResourcePages
|
|
23
27
|
|
|
24
|
-
const DEFAULT_ALLOWED_ACTIONS = {create: true, edit: true, show: true, delete: true};
|
|
25
28
|
|
|
26
29
|
|
|
27
30
|
class AdminForth implements AdminForthClass {
|
|
@@ -250,6 +253,32 @@ class AdminForth implements AdminForthClass {
|
|
|
250
253
|
res.options = {bulkActions: [], allowedActions: {}};
|
|
251
254
|
}
|
|
252
255
|
|
|
256
|
+
if (!res.options.allowedActions) {
|
|
257
|
+
res.options.allowedActions = {
|
|
258
|
+
all: true,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
if (Object.keys(res.options.allowedActions).includes('all')) {
|
|
264
|
+
if (Object.keys(res.options.allowedActions).length > 1) {
|
|
265
|
+
errors.push(`Resource "${res.resourceId}" allowedActions cannot have "all" and other keys at same time: ${Object.keys(res.options.allowedActions).join(', ')}`);
|
|
266
|
+
}
|
|
267
|
+
for (const key of Object.keys(AllowedActionsEnum)) {
|
|
268
|
+
if (key !== 'all') {
|
|
269
|
+
res.options.allowedActions[key] = res.options.allowedActions.all;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
delete res.options.allowedActions.all;
|
|
273
|
+
} else {
|
|
274
|
+
// by default allow all actions
|
|
275
|
+
for (const key of Object.keys(AllowedActionsEnum)) {
|
|
276
|
+
if (!Object.keys(res.options.allowedActions).includes(key)) {
|
|
277
|
+
res.options.allowedActions[key] = true;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
253
282
|
|
|
254
283
|
//check if resource has bulkActions
|
|
255
284
|
if(res.options?.bulkActions){
|
|
@@ -301,17 +330,6 @@ class AdminForth implements AdminForthClass {
|
|
|
301
330
|
|
|
302
331
|
}
|
|
303
332
|
|
|
304
|
-
//add default allowedActions to resources
|
|
305
|
-
if(res.options.allowedActions){
|
|
306
|
-
//check if allowedActions is an object
|
|
307
|
-
if(typeof res.options.allowedActions !== 'object'){
|
|
308
|
-
errors.push(`Resource "${res.resourceId}" allowedActions must be an object`);
|
|
309
|
-
}
|
|
310
|
-
const userAllowedActions = res.options.allowedActions
|
|
311
|
-
res.options.allowedActions = Object.assign({}, DEFAULT_ALLOWED_ACTIONS, userAllowedActions);
|
|
312
|
-
} else {
|
|
313
|
-
res.options.allowedActions = DEFAULT_ALLOWED_ACTIONS;
|
|
314
|
-
}
|
|
315
333
|
|
|
316
334
|
// transform all hooks Functions to array of functions
|
|
317
335
|
if (res.hooks) {
|
|
@@ -648,13 +666,49 @@ class AdminForth implements AdminForthClass {
|
|
|
648
666
|
[this.config.auth.usernameField]: username,
|
|
649
667
|
[this.config.auth.userFullNameField]: userFullName
|
|
650
668
|
};
|
|
669
|
+
const checkIsMenuItemVisible = (menuItem) => {
|
|
670
|
+
if (typeof menuItem.visible === 'function') {
|
|
671
|
+
const toReturn = menuItem.visible( adminUser );
|
|
672
|
+
if (typeof toReturn !== 'boolean') {
|
|
673
|
+
throw new Error(`'visible' function of ${menuItem.label || menuItem.type } must return boolean value`);
|
|
674
|
+
}
|
|
675
|
+
return toReturn;
|
|
676
|
+
}}
|
|
677
|
+
let newMenu = []
|
|
678
|
+
for (let menuItem of this.config.menu) {
|
|
679
|
+
let newMenuItem = {...menuItem,}
|
|
680
|
+
if (menuItem.visible){
|
|
681
|
+
if (!checkIsMenuItemVisible(menuItem)){
|
|
682
|
+
continue
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
if (menuItem.children){
|
|
686
|
+
let newChildren = []
|
|
687
|
+
for (let child of menuItem.children){
|
|
688
|
+
let newChild = {...child,}
|
|
689
|
+
if (child.visible){
|
|
690
|
+
if (!checkIsMenuItemVisible(child)){
|
|
691
|
+
continue
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
newChildren.push(newChild)
|
|
695
|
+
}
|
|
696
|
+
newMenuItem = {...newMenuItem, children: newChildren}
|
|
697
|
+
}
|
|
698
|
+
newMenu.push(newMenuItem)
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
|
|
651
705
|
return {
|
|
652
706
|
user: userData,
|
|
653
707
|
resources: this.config.resources.map((res) => ({
|
|
654
708
|
resourceId: res.resourceId,
|
|
655
709
|
label: res.label,
|
|
656
710
|
})),
|
|
657
|
-
menu:
|
|
711
|
+
menu: newMenu,
|
|
658
712
|
config: {
|
|
659
713
|
brandName: this.config.customization.brandName,
|
|
660
714
|
brandLogo: this.config.customization.brandLogo,
|
|
@@ -671,12 +725,31 @@ class AdminForth implements AdminForthClass {
|
|
|
671
725
|
},
|
|
672
726
|
});
|
|
673
727
|
|
|
728
|
+
async function interpretResource(adminUser: AdminUser, resource: AdminForthResource, meta: any) {
|
|
729
|
+
await Promise.all(
|
|
730
|
+
Object.entries(resource.options?.allowedActions || {}).map(
|
|
731
|
+
async ([key, value]: [string, AllowedActionValue]) => {
|
|
732
|
+
// if callable then call
|
|
733
|
+
if (typeof value === 'function') {
|
|
734
|
+
resource.options.allowedActions[key] = await value( adminUser, resource, meta );
|
|
735
|
+
}
|
|
736
|
+
})
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function checkAccess(action: AllowedActionsEnum, resource: AdminForthResource): { allowed: boolean, error?: string } {
|
|
741
|
+
const allowed = (resource.options?.allowedActions[action] as boolean | string | undefined);
|
|
742
|
+
if (allowed !== true) {
|
|
743
|
+
return { error: typeof allowed === 'string' ? allowed : 'Action is not allowed', allowed: false };
|
|
744
|
+
}
|
|
745
|
+
return { allowed: true };
|
|
746
|
+
}
|
|
674
747
|
|
|
675
748
|
|
|
676
749
|
server.endpoint({
|
|
677
750
|
method: 'POST',
|
|
678
751
|
path: '/get_resource',
|
|
679
|
-
handler: async ({ body }) => {
|
|
752
|
+
handler: async ({ body, adminUser }) => {
|
|
680
753
|
const { resourceId } = body;
|
|
681
754
|
if (!this.statuses.dbDiscover) {
|
|
682
755
|
return { error: 'Database discovery not started' };
|
|
@@ -688,6 +761,9 @@ class AdminForth implements AdminForthClass {
|
|
|
688
761
|
if (!resource) {
|
|
689
762
|
return { error: `Resource ${resourceId} not found` };
|
|
690
763
|
}
|
|
764
|
+
|
|
765
|
+
await interpretResource(adminUser, resource, {});
|
|
766
|
+
|
|
691
767
|
// exclude "plugins" key
|
|
692
768
|
return { resource: { ...resource, plugins: undefined } };
|
|
693
769
|
},
|
|
@@ -711,6 +787,14 @@ class AdminForth implements AdminForthClass {
|
|
|
711
787
|
return { error: `Resource ${resourceId} not found` };
|
|
712
788
|
}
|
|
713
789
|
|
|
790
|
+
await interpretResource(adminUser, resource, { requestBody: body });
|
|
791
|
+
|
|
792
|
+
const { allowed, error } = checkAccess(source as AllowedActionsEnum, resource);
|
|
793
|
+
console.log('allowed', allowed, error);
|
|
794
|
+
if (!allowed) {
|
|
795
|
+
return { error };
|
|
796
|
+
}
|
|
797
|
+
|
|
714
798
|
for (const hook of listify(resource.hooks?.[source]?.beforeDatasourceRequest)) {
|
|
715
799
|
const resp = await hook({ resource, query: body, adminUser });
|
|
716
800
|
if (!resp || (!resp.ok && !resp.error)) {
|
|
@@ -723,6 +807,8 @@ class AdminForth implements AdminForthClass {
|
|
|
723
807
|
}
|
|
724
808
|
const { limit, offset, filters, sort } = body;
|
|
725
809
|
|
|
810
|
+
|
|
811
|
+
|
|
726
812
|
for (const filter of (filters || [])) {
|
|
727
813
|
if (!Object.values(AdminForthFilterOperators).includes(filter.operator)) {
|
|
728
814
|
throw new Error(`Operator '${filter.operator}' is not allowed`);
|
|
@@ -734,7 +820,7 @@ class AdminForth implements AdminForthClass {
|
|
|
734
820
|
|
|
735
821
|
if (filter.operator === AdminForthFilterOperators.IN || filter.operator === AdminForthFilterOperators.NIN) {
|
|
736
822
|
if (!Array.isArray(filter.value)) {
|
|
737
|
-
throw new Error(`Value for operator '${filter.operator}'
|
|
823
|
+
throw new Error(`Value for operator '${filter.operator}' should be an array`);
|
|
738
824
|
}
|
|
739
825
|
}
|
|
740
826
|
|
|
@@ -922,12 +1008,17 @@ class AdminForth implements AdminForthClass {
|
|
|
922
1008
|
method: 'POST',
|
|
923
1009
|
path: '/create_record',
|
|
924
1010
|
handler: async ({ body, adminUser }) => {
|
|
925
|
-
console.log('create_record', body, this.config.resources);
|
|
926
1011
|
const resource = this.config.resources.find((res) => res.resourceId == body['resourceId']);
|
|
927
1012
|
if (!resource) {
|
|
928
1013
|
return { error: `Resource '${body['resourceId']}' not found` };
|
|
929
1014
|
}
|
|
930
|
-
|
|
1015
|
+
await interpretResource(adminUser, resource, { requestBody: body });
|
|
1016
|
+
|
|
1017
|
+
const { allowed, error } = checkAccess(AllowedActionsEnum.create, resource);
|
|
1018
|
+
if (!allowed) {
|
|
1019
|
+
return { error };
|
|
1020
|
+
}
|
|
1021
|
+
|
|
931
1022
|
const record = body['record'];
|
|
932
1023
|
// execute hook if needed
|
|
933
1024
|
for (const hook of listify(resource.hooks?.create?.beforeSave as BeforeSaveFunction[])) {
|
|
@@ -941,6 +1032,8 @@ class AdminForth implements AdminForthClass {
|
|
|
941
1032
|
}
|
|
942
1033
|
}
|
|
943
1034
|
|
|
1035
|
+
|
|
1036
|
+
|
|
944
1037
|
for (const column of resource.columns) {
|
|
945
1038
|
if (column.fillOnCreate) {
|
|
946
1039
|
if (body['record'][column.name] === undefined) {
|
|
@@ -1000,6 +1093,12 @@ class AdminForth implements AdminForthClass {
|
|
|
1000
1093
|
if (!resource) {
|
|
1001
1094
|
return { error: `Resource '${body['resourceId']}' not found` };
|
|
1002
1095
|
}
|
|
1096
|
+
await interpretResource(adminUser, resource, { requestBody: body });
|
|
1097
|
+
|
|
1098
|
+
const { allowed, error } = checkAccess(AllowedActionsEnum.edit, resource);
|
|
1099
|
+
if (!allowed) {
|
|
1100
|
+
return { error };
|
|
1101
|
+
}
|
|
1003
1102
|
|
|
1004
1103
|
const recordId = body['recordId'];
|
|
1005
1104
|
const connector = this.connectors[resource.dataSource];
|
|
@@ -1074,6 +1173,13 @@ class AdminForth implements AdminForthClass {
|
|
|
1074
1173
|
return { error: `Resource '${resource.resourceId}' does not allow delete action` };
|
|
1075
1174
|
}
|
|
1076
1175
|
|
|
1176
|
+
await interpretResource(adminUser, resource, { requestBody: body });
|
|
1177
|
+
|
|
1178
|
+
const { allowed, error } = checkAccess(AllowedActionsEnum.delete, resource);
|
|
1179
|
+
if (!allowed) {
|
|
1180
|
+
return { error };
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1077
1183
|
// execute hook if needed
|
|
1078
1184
|
for (const hook of listify(resource.hooks?.delete?.beforeSave as BeforeSaveFunction[])) {
|
|
1079
1185
|
const resp = await hook({ resource, record, adminUser });
|
package/modules/codeInjector.ts
CHANGED
|
@@ -149,7 +149,7 @@ class CodeInjector implements CodeInjectorType {
|
|
|
149
149
|
routes += `{
|
|
150
150
|
path: '${item.path}',
|
|
151
151
|
name: '${item.path}',
|
|
152
|
-
component: () => import('${item.component}'),
|
|
152
|
+
component: () => import('${item.component}'),
|
|
153
153
|
meta: { title: '${item?.meta?.title}'
|
|
154
154
|
},\n`
|
|
155
155
|
|
package/package.json
CHANGED
|
@@ -8,7 +8,7 @@ import { PluginOptions } from "./types.js";
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
export default class
|
|
11
|
+
export default class AuditLogPlugin extends AdminForthPlugin {
|
|
12
12
|
|
|
13
13
|
options: PluginOptions;
|
|
14
14
|
adminforth: AdminForthClass;
|
|
@@ -25,59 +25,5 @@ export default class AccessControlPlugin extends AdminForthPlugin {
|
|
|
25
25
|
super.modifyResourceConfig(adminforth, resourceConfig);
|
|
26
26
|
this.adminforth = adminforth;
|
|
27
27
|
|
|
28
|
-
if (!resourceConfig.hooks) {
|
|
29
|
-
resourceConfig.hooks = {};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const checkAccess = async (adminUser: any, action: AllowedActionsEnum, meta: any) => {
|
|
33
|
-
const hasAccessOrError = await this.options.hasAccess(adminUser, action, meta);
|
|
34
|
-
if (hasAccessOrError === true) {
|
|
35
|
-
return { ok: true };
|
|
36
|
-
} else {
|
|
37
|
-
return { ok: false, error: hasAccessOrError || AccessControlPlugin.defaultError };
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const bindHookCheck = (action: AllowedActionsEnum, hookName: string) => {
|
|
42
|
-
if (!resourceConfig.hooks[action]) {
|
|
43
|
-
resourceConfig.hooks[action] = {};
|
|
44
|
-
}
|
|
45
|
-
if (!resourceConfig.hooks[action][hookName]) {
|
|
46
|
-
resourceConfig.hooks[action][hookName] = [];
|
|
47
|
-
}
|
|
48
|
-
if (hookName === 'beforeDatasourceRequest') {
|
|
49
|
-
(resourceConfig.hooks[action][hookName] as Array<BeforeDataSourceRequestFunction>).unshift(
|
|
50
|
-
async ({adminUser, query}: {resource: AdminForthResource, adminUser: any, query: any}) => {
|
|
51
|
-
return checkAccess(adminUser, action, { query });
|
|
52
|
-
}
|
|
53
|
-
);
|
|
54
|
-
} else {
|
|
55
|
-
(resourceConfig.hooks[action][hookName] as Array<BeforeSaveFunction>).unshift(
|
|
56
|
-
async ({adminUser, record}: {resource: AdminForthResource, adminUser: any, record: any}) => {
|
|
57
|
-
return checkAccess(adminUser, action, { record });
|
|
58
|
-
}
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// List check
|
|
64
|
-
bindHookCheck(AllowedActionsEnum.list, 'beforeDatasourceRequest');
|
|
65
|
-
|
|
66
|
-
// Show check
|
|
67
|
-
bindHookCheck(AllowedActionsEnum.show, 'beforeDatasourceRequest');
|
|
68
|
-
|
|
69
|
-
// Edit check
|
|
70
|
-
bindHookCheck(AllowedActionsEnum.edit, 'beforeDatasourceRequest');
|
|
71
|
-
|
|
72
|
-
// create check
|
|
73
|
-
bindHookCheck(AllowedActionsEnum.create, 'beforeSave');
|
|
74
|
-
|
|
75
|
-
// edit check
|
|
76
|
-
bindHookCheck(AllowedActionsEnum.edit, 'beforeSave');
|
|
77
|
-
|
|
78
|
-
// delete check
|
|
79
|
-
bindHookCheck(AllowedActionsEnum.delete, 'beforeSave');
|
|
80
|
-
|
|
81
|
-
console.log('resourceConfig', resourceConfig);
|
|
82
28
|
}
|
|
83
29
|
}
|
|
@@ -3,13 +3,5 @@ import { AllowedActionsEnum, AdminUser } from '../../types/AdminForthConfig.js';
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
export type PluginOptions = {
|
|
6
|
-
|
|
7
|
-
* Called to check if user has access to a page
|
|
8
|
-
* Should return `true` if user has access. If user has no access, return `false` or a string with an error message
|
|
9
|
-
* @param user - The user object
|
|
10
|
-
* @param action - The {@link AllowedActionsEnum} action
|
|
11
|
-
* @param meta - The meta object containing query for list/show or record for save/edit
|
|
12
|
-
* @returns {boolean | string} -
|
|
13
|
-
*/
|
|
14
|
-
hasAccess: (user: AdminUser, action: AllowedActionsEnum, meta: any) => Promise<boolean | string>;
|
|
6
|
+
|
|
15
7
|
}
|
|
@@ -104,6 +104,7 @@ import {
|
|
|
104
104
|
IconFilterOutline,
|
|
105
105
|
IconPlusOutline,
|
|
106
106
|
} from '@iconify-prerendered/vue-flowbite';
|
|
107
|
+
import { showErrorTost, showWarningTost, showSuccesTost} from '@/composables/useFrontendApi';
|
|
107
108
|
|
|
108
109
|
import { getIcon } from '@/utils';
|
|
109
110
|
|
|
@@ -145,10 +146,7 @@ const endFilters = computed(() => {
|
|
|
145
146
|
const primaryKeyColumn = selfPrimaryKeyColumn.value;
|
|
146
147
|
|
|
147
148
|
if (!refColumn) {
|
|
148
|
-
|
|
149
|
-
message: `Column with foreignResource.resourceId which is equal to '${props.resource.resourceId}' not found in resource which is specified as foreighResourceId '${listResource.value.resourceId}'`,
|
|
150
|
-
variant: 'danger',
|
|
151
|
-
});
|
|
149
|
+
showErrorTost(`Column with foreignResource.resourceId which is equal to '${props.resource.resourceId}' not found in resource which is specified as foreighResourceId '${listResource.value.resourceId}'`,10000);
|
|
152
150
|
return [];
|
|
153
151
|
}
|
|
154
152
|
return [
|
|
@@ -209,11 +207,7 @@ async function getList() {
|
|
|
209
207
|
});
|
|
210
208
|
|
|
211
209
|
if (data.error) {
|
|
212
|
-
|
|
213
|
-
message: data.error,
|
|
214
|
-
variant: 'danger',
|
|
215
|
-
timeout: 'unlimited',
|
|
216
|
-
});
|
|
210
|
+
showErrorTost(data.error);
|
|
217
211
|
rows.value = [];
|
|
218
212
|
totalRows.value = 0;
|
|
219
213
|
return;
|
|
@@ -190,7 +190,7 @@
|
|
|
190
190
|
<button v-if="resource.options?.allowedActions.delete"
|
|
191
191
|
class="font-medium text-red-600 dark:text-red-500 hover:underline ms-3"
|
|
192
192
|
:data-tooltip-target="`tooltip-delete-${rowI}`"
|
|
193
|
-
@click="
|
|
193
|
+
@click="deleteRecord(row)"
|
|
194
194
|
>
|
|
195
195
|
<IconTrashBinSolid class="w-5 h-5 me-2"/>
|
|
196
196
|
</button>
|
|
@@ -272,10 +272,12 @@
|
|
|
272
272
|
import { initFlowbite } from 'flowbite';
|
|
273
273
|
import { computed, onMounted, ref, watch } from 'vue';
|
|
274
274
|
import { useRoute } from 'vue-router';
|
|
275
|
+
import { callAdminForthApi, getIcon } from '@/utils';
|
|
275
276
|
|
|
276
277
|
import ValueRenderer from '@/components/ValueRenderer.vue';
|
|
277
278
|
import { getCustomComponent } from '@/utils';
|
|
278
279
|
import { useCoreStore } from '@/stores/core';
|
|
280
|
+
import { showSuccesTost } from '@/composables/useFrontendApi';
|
|
279
281
|
|
|
280
282
|
import {
|
|
281
283
|
IconInboxOutline,
|
|
@@ -305,6 +307,7 @@ const emits = defineEmits([
|
|
|
305
307
|
'update:page',
|
|
306
308
|
'update:sort',
|
|
307
309
|
'update:checkboxes',
|
|
310
|
+
'update:records'
|
|
308
311
|
|
|
309
312
|
]);
|
|
310
313
|
|
|
@@ -358,9 +361,11 @@ async function selectAll(value) {
|
|
|
358
361
|
const totalPages = computed(() => Math.ceil(props.totalRows / props.pageSize));
|
|
359
362
|
|
|
360
363
|
|
|
364
|
+
|
|
365
|
+
|
|
361
366
|
const allFromThisPageChecked = computed(() => {
|
|
362
|
-
if (!props.rows
|
|
363
|
-
return props.rows.
|
|
367
|
+
if (!props.rows) return false;
|
|
368
|
+
return props.rows.every((r) => props.checkboxes.includes(r.id));
|
|
364
369
|
});
|
|
365
370
|
const ascArr = computed(() => sort.value.filter((s) => s.direction === 'asc').map((s) => s.field));
|
|
366
371
|
const descArr = computed(() => sort.value.filter((s) => s.direction === 'desc').map((s) => s.field));
|
|
@@ -380,4 +385,35 @@ function onSortButtonClick(field) {
|
|
|
380
385
|
}
|
|
381
386
|
}
|
|
382
387
|
|
|
388
|
+
async function deleteRecord(row) {
|
|
389
|
+
const data = await window.adminforth.confirm({
|
|
390
|
+
message: 'Are you sure you want to delete this item?',
|
|
391
|
+
yes: 'Delete',
|
|
392
|
+
no: 'Cancel',
|
|
393
|
+
});
|
|
394
|
+
if (data) {
|
|
395
|
+
try {
|
|
396
|
+
const res = await callAdminForthApi({
|
|
397
|
+
path: '/delete_record',
|
|
398
|
+
method: 'POST',
|
|
399
|
+
body: {
|
|
400
|
+
resourceId: props.resource.resourceId,
|
|
401
|
+
primaryKey: row._primaryKeyValue,
|
|
402
|
+
}});
|
|
403
|
+
if (!res.error){
|
|
404
|
+
emits('update:records', true)
|
|
405
|
+
showSuccesTost('Record deleted successfully')
|
|
406
|
+
} else {
|
|
407
|
+
console.error(res.error)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
} catch (e) {
|
|
411
|
+
console.error(e);
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
|
|
383
419
|
</script>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function showSuccesTost(message: string) {
|
|
2
|
+
window.adminforth.alert({ message, variant: 'success' });
|
|
3
|
+
return message;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function showWarningTost(message: string) {
|
|
7
|
+
window.adminforth.alert({ message, variant: 'warning' });
|
|
8
|
+
return message;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function showErrorTost(message: string, timeout?: number) {
|
|
12
|
+
window.adminforth.alert({ message, variant: 'danger', timeout: timeout || 'unlimited'});
|
|
13
|
+
return message;
|
|
14
|
+
}
|
package/spa/src/utils.ts
CHANGED
|
@@ -78,6 +78,7 @@ import { IconFloppyDiskSolid } from '@iconify-prerendered/vue-flowbite';
|
|
|
78
78
|
import { onMounted, ref, watch } from 'vue';
|
|
79
79
|
import { useRoute, useRouter } from 'vue-router';
|
|
80
80
|
import { computed } from 'vue';
|
|
81
|
+
import { showErrorTost } from '@/composables/useFrontendApi';
|
|
81
82
|
|
|
82
83
|
|
|
83
84
|
const isValid = ref(false);
|
|
@@ -141,11 +142,8 @@ async function saveRecord() {
|
|
|
141
142
|
},
|
|
142
143
|
});
|
|
143
144
|
if (response.error) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
variant: 'danger',
|
|
147
|
-
timeout: 'unlimited',
|
|
148
|
-
})
|
|
145
|
+
showErrorTost(response.error);
|
|
146
|
+
|
|
149
147
|
}
|
|
150
148
|
saving.value = false;
|
|
151
149
|
if (route.query.returnTo) {
|
|
@@ -73,6 +73,7 @@ import { callAdminForthApi, getCustomComponent,checkAcessByAllowedActions } from
|
|
|
73
73
|
import { IconFloppyDiskSolid } from '@iconify-prerendered/vue-flowbite';
|
|
74
74
|
import { computed, onMounted, ref } from 'vue';
|
|
75
75
|
import { useRoute, useRouter } from 'vue-router';
|
|
76
|
+
import { showErrorTost } from '@/composables/useFrontendApi';
|
|
76
77
|
|
|
77
78
|
const coreStore = useCoreStore();
|
|
78
79
|
|
|
@@ -155,11 +156,7 @@ async function saveRecord() {
|
|
|
155
156
|
},
|
|
156
157
|
});
|
|
157
158
|
if (resp.error) {
|
|
158
|
-
|
|
159
|
-
message: resp.error,
|
|
160
|
-
variant: 'danger',
|
|
161
|
-
timeout: 'unlimited',
|
|
162
|
-
})
|
|
159
|
+
showErrorTost(resp.error);
|
|
163
160
|
}
|
|
164
161
|
saving.value = false;
|
|
165
162
|
router.push({ name: 'resource-show', params: { resourceId: route.params.resourceId, primaryKey: coreStore.record[coreStore.primaryKey] } });
|
|
@@ -87,6 +87,7 @@
|
|
|
87
87
|
@update:page="page = $event"
|
|
88
88
|
@update:sort="sort = $event"
|
|
89
89
|
@update:checkboxes="checkboxes = $event"
|
|
90
|
+
@update:records="getList"
|
|
90
91
|
:pageSize="pageSize"
|
|
91
92
|
:totalRows="totalRows"
|
|
92
93
|
:checkboxes="checkboxes"
|
|
@@ -113,6 +114,7 @@ import { callAdminForthApi, getIcon } from '@/utils';
|
|
|
113
114
|
import { initFlowbite } from 'flowbite';
|
|
114
115
|
import { computed, onMounted, ref, watch } from 'vue';
|
|
115
116
|
import { useRoute } from 'vue-router';
|
|
117
|
+
import { showErrorTost } from '@/composables/useFrontendApi'
|
|
116
118
|
|
|
117
119
|
import ValueRenderer from '@/components/ValueRenderer.vue';
|
|
118
120
|
import { getCustomComponent } from '@/utils';
|
|
@@ -184,11 +186,7 @@ async function getList() {
|
|
|
184
186
|
}
|
|
185
187
|
});
|
|
186
188
|
if (data.error) {
|
|
187
|
-
|
|
188
|
-
message: data.error,
|
|
189
|
-
variant: 'danger',
|
|
190
|
-
timeout: 'unlimited',
|
|
191
|
-
});
|
|
189
|
+
showErrorTost(data.error);
|
|
192
190
|
rows.value = [];
|
|
193
191
|
totalRows.value = 0;
|
|
194
192
|
return;
|
|
@@ -201,36 +199,9 @@ async function getList() {
|
|
|
201
199
|
}
|
|
202
200
|
|
|
203
201
|
|
|
204
|
-
function showDeleteModal(row) {
|
|
205
|
-
if (!coreStore.config?.deleteConfirmation) {
|
|
206
|
-
return deleteRecord(row);
|
|
207
|
-
}
|
|
208
|
-
modalStore.setModalContent({
|
|
209
|
-
content: 'Are you sure you want to delete this item?',
|
|
210
|
-
acceptText: 'Delete',
|
|
211
|
-
cancelText: 'Cancel',
|
|
212
|
-
});
|
|
213
|
-
modalStore.setOnAcceptFunction(() => {
|
|
214
|
-
return deleteRecord(row)
|
|
215
|
-
})
|
|
216
|
-
console.log('row', row);
|
|
217
|
-
modalStore.togleModal();
|
|
218
202
|
|
|
219
|
-
}
|
|
220
203
|
|
|
221
|
-
|
|
222
|
-
await callAdminForthApi({
|
|
223
|
-
path: '/delete_record',
|
|
224
|
-
method: 'POST',
|
|
225
|
-
body: {
|
|
226
|
-
resourceId: route.params.resourceId,
|
|
227
|
-
primaryKey: row._primaryKeyValue,
|
|
228
|
-
recordId: row.id
|
|
229
|
-
}
|
|
230
|
-
});
|
|
231
|
-
await getList();
|
|
232
|
-
modalStore.resetmodalState()
|
|
233
|
-
}
|
|
204
|
+
|
|
234
205
|
|
|
235
206
|
async function startBulkAction(actionId) {
|
|
236
207
|
const data = await callAdminForthApi({
|
|
@@ -833,9 +833,20 @@ export enum AllowedActionsEnum {
|
|
|
833
833
|
filter = 'filter',
|
|
834
834
|
}
|
|
835
835
|
|
|
836
|
+
/**
|
|
837
|
+
* Defines whether user has access to an action, can statically be Boolean
|
|
838
|
+
* or function which returns Boolean or string with error message
|
|
839
|
+
*
|
|
840
|
+
*/
|
|
841
|
+
export type AllowedActionValue = boolean | ((adminUser: AdminUser, resource: AdminForthResource, meta: any) => Promise<boolean | string>);
|
|
836
842
|
|
|
843
|
+
/**
|
|
844
|
+
* Object which describes allowed actions for user.
|
|
845
|
+
*/
|
|
837
846
|
export type AllowedActions = {
|
|
838
|
-
[key in AllowedActionsEnum]?:
|
|
847
|
+
[key in AllowedActionsEnum]?: AllowedActionValue
|
|
848
|
+
} & {
|
|
849
|
+
all?: AllowedActionValue;
|
|
839
850
|
}
|
|
840
851
|
|
|
841
852
|
export type ValidationObject = {
|