adminforth 1.1.20 → 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 +68 -17
- 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/plugins/plugins/ForeignInlineListPlugin/custom/InlineList.vue +248 -0
- package/dist/spa/index.html +2 -2
- package/dist/spa/package-lock.json +1 -189
- package/dist/spa/package.json +1 -6
- package/dist/spa/spa/src/components/ResourceListTable.vue +35 -1
- package/dist/spa/spa/src/components/ShowTableItem.vue +14 -0
- 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/HomeView.vue +8 -0
- package/dist/spa/spa/src/views/ListView.vue +4 -33
- package/dist/spa/src/App.vue +57 -76
- package/dist/spa/src/components/AcceptModal.vue +1 -1
- package/dist/spa/src/components/CustomDatePicker.vue +3 -3
- package/dist/spa/src/components/CustomDateRangePicker.vue +3 -3
- package/dist/spa/src/components/Dropdown.vue +4 -13
- package/dist/spa/src/components/Filters.vue +22 -55
- package/dist/spa/src/components/MenuLink.vue +2 -2
- package/dist/spa/src/components/ResourceForm.vue +99 -157
- package/dist/spa/src/components/ValueRenderer.vue +1 -11
- package/dist/spa/src/router/index.ts +2 -3
- package/dist/spa/src/stores/core.ts +33 -22
- package/dist/spa/src/utils.ts +3 -5
- package/dist/spa/src/views/CreateView.vue +7 -15
- package/dist/spa/src/views/EditView.vue +8 -36
- package/dist/spa/src/views/HomeView.vue +8 -0
- package/dist/spa/src/views/ListView.vue +23 -35
- package/dist/spa/src/views/LoginView.vue +1 -1
- package/dist/spa/src/views/ResourceParent.vue +1 -1
- package/dist/spa/src/views/ShowView.vue +9 -20
- package/dist/spa/tailwind.config.js +2 -5
- package/index.ts +87 -17
- 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 +35 -1
- 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/spa/src/components/CustomRangePicker.vue +0 -148
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
</BreadcrumbsWithButtons>
|
|
66
66
|
|
|
67
67
|
<!-- table -->
|
|
68
|
-
<div class="relative overflow-x-auto shadow-
|
|
68
|
+
<div class="relative overflow-x-auto shadow-md sm:rounded-lg dark:shadow-2xl">
|
|
69
69
|
|
|
70
70
|
|
|
71
71
|
<!-- skelet loader -->
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
</div>
|
|
91
91
|
|
|
92
92
|
<table v-else class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
|
|
93
|
-
<thead class="text-xs text-
|
|
93
|
+
<thead class="text-xs text-gray-700 bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
|
|
94
94
|
<tr>
|
|
95
95
|
<th scope="col" class="p-4">
|
|
96
96
|
<div v-if="rows && rows.length" class="flex items-center">
|
|
@@ -127,7 +127,7 @@
|
|
|
127
127
|
</tr>
|
|
128
128
|
</thead>
|
|
129
129
|
<tbody>
|
|
130
|
-
<tr v-if="!rows" class="bg-
|
|
130
|
+
<tr v-if="!rows" class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
|
|
131
131
|
<td :colspan="coreStore.resourceColumns.length + 2">
|
|
132
132
|
|
|
133
133
|
<div role="status"
|
|
@@ -171,7 +171,7 @@
|
|
|
171
171
|
</div>
|
|
172
172
|
</td>
|
|
173
173
|
</tr>
|
|
174
|
-
<tr v-else-if="rows.length === 0" class="bg-
|
|
174
|
+
<tr v-else-if="rows.length === 0" class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
|
|
175
175
|
<td :colspan="coreStore.resourceColumns.length + 2">
|
|
176
176
|
|
|
177
177
|
<div id="toast-simple"
|
|
@@ -185,7 +185,7 @@
|
|
|
185
185
|
</tr>
|
|
186
186
|
|
|
187
187
|
<tr v-else v-for="(row, rowI) in rows" :key="row.id"
|
|
188
|
-
class="bg-
|
|
188
|
+
class="bg-white border-b dark:bg-gray-800 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600">
|
|
189
189
|
<td class="w-4 p-4">
|
|
190
190
|
<div class="flex items center">
|
|
191
191
|
<input
|
|
@@ -198,12 +198,7 @@
|
|
|
198
198
|
</div>
|
|
199
199
|
</td>
|
|
200
200
|
<td v-for="c in columnsListed" class="px-6 py-4">
|
|
201
|
-
|
|
202
|
-
<component
|
|
203
|
-
:is="listComponentsPerColumn[c.name] || ValueRenderer"
|
|
204
|
-
:column="c"
|
|
205
|
-
:row="row"
|
|
206
|
-
/>
|
|
201
|
+
<ValueRenderer :column="c" :row="row"/>
|
|
207
202
|
</td>
|
|
208
203
|
<td class="flex items-center px-6 py-4">
|
|
209
204
|
|
|
@@ -315,28 +310,29 @@
|
|
|
315
310
|
</template>
|
|
316
311
|
|
|
317
312
|
<script setup>
|
|
313
|
+
import {ref, onMounted, watch, computed} from 'vue';
|
|
314
|
+
import {callAdminForthApi, getIcon} from '@/utils';
|
|
315
|
+
import {useRoute} from 'vue-router';
|
|
316
|
+
import {useCoreStore} from '@/stores/core';
|
|
317
|
+
import {useModalStore} from '@/stores/modal';
|
|
318
318
|
import BreadcrumbsWithButtons from '@/components/BreadcrumbsWithButtons.vue';
|
|
319
|
-
import {
|
|
320
|
-
import { useModalStore } from '@/stores/modal';
|
|
321
|
-
import { callAdminForthApi, getIcon } from '@/utils';
|
|
322
|
-
import { initFlowbite } from 'flowbite';
|
|
323
|
-
import { computed, onMounted, ref, watch } from 'vue';
|
|
324
|
-
import { useRoute } from 'vue-router';
|
|
319
|
+
import {initFlowbite} from 'flowbite'
|
|
325
320
|
|
|
326
321
|
import ValueRenderer from '@/components/ValueRenderer.vue';
|
|
327
|
-
import { getCustomComponent } from '@/utils';
|
|
328
322
|
|
|
329
323
|
import {
|
|
330
|
-
|
|
331
|
-
|
|
324
|
+
IconChevronDoubleLeftOutline,
|
|
325
|
+
IconChevronDoubleRightOutline,
|
|
326
|
+
IconInboxFullSolid,
|
|
327
|
+
IconInboxOutline,
|
|
328
|
+
IconPlusOutline
|
|
332
329
|
} from '@iconify-prerendered/vue-flowbite';
|
|
333
330
|
|
|
334
331
|
import {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
IconTrashBinSolid
|
|
332
|
+
IconEyeSolid,
|
|
333
|
+
IconTrashBinSolid,
|
|
334
|
+
IconPenSolid,
|
|
335
|
+
IconFilterOutline, IconBanOutline
|
|
340
336
|
} from '@iconify-prerendered/vue-flowbite';
|
|
341
337
|
|
|
342
338
|
import Filters from '@/components/Filters.vue';
|
|
@@ -361,6 +357,7 @@ const totalRows = ref(0);
|
|
|
361
357
|
const allCheckedActions = ref([]);
|
|
362
358
|
const allowedActions = ref({});
|
|
363
359
|
|
|
360
|
+
|
|
364
361
|
const DEFAULT_PAGE_SIZE = 10;
|
|
365
362
|
|
|
366
363
|
const columnsListed = computed(() => coreStore.resourceColumns?.filter(c => c.showIn.includes('list')));
|
|
@@ -379,7 +376,6 @@ async function selectAll(value) {
|
|
|
379
376
|
|
|
380
377
|
const pageSize = computed(() => coreStore.resourceById[route.params.resourceId]?.pageSize || DEFAULT_PAGE_SIZE);
|
|
381
378
|
const totalPages = computed(() => Math.ceil(totalRows.value / pageSize.value));
|
|
382
|
-
let listComponentsPerColumn = {};
|
|
383
379
|
const allFromThisPageChecked = computed(() => {
|
|
384
380
|
if (!rows.value) return false;
|
|
385
381
|
return rows.value.every((r) => checkboxes.value.includes(r.id));
|
|
@@ -423,7 +419,6 @@ async function getList() {
|
|
|
423
419
|
path: '/get_resource_data',
|
|
424
420
|
method: 'POST',
|
|
425
421
|
body: {
|
|
426
|
-
source: 'list',
|
|
427
422
|
resourceId: route.params.resourceId,
|
|
428
423
|
limit: pageSize.value,
|
|
429
424
|
offset: (page.value - 1) * pageSize.value,
|
|
@@ -431,14 +426,7 @@ async function getList() {
|
|
|
431
426
|
sort: sort.value,
|
|
432
427
|
}
|
|
433
428
|
});
|
|
434
|
-
|
|
435
|
-
if (column.component?.list) {
|
|
436
|
-
acc[column.name] = getCustomComponent(column.component.list);
|
|
437
|
-
}
|
|
438
|
-
return acc;
|
|
439
|
-
}, {});
|
|
440
|
-
|
|
441
|
-
fetchStatus.value.pending = false;
|
|
429
|
+
fetchStatus.value.pending = false;
|
|
442
430
|
rows.value = data.data?.map(row => {
|
|
443
431
|
row._primaryKeyValue = row[coreStore.resourceColumns.find(c => c.primaryKey).name];
|
|
444
432
|
return row;
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<div id="authentication-modal" tabindex="-1" class=" overflow-y-auto overflow-x-hidden z-50 min-w-[400px] justify-center items-center md:inset-0 h-[calc(100%-1rem)] max-h-full">
|
|
12
12
|
<div class="relative p-4 w-full max-w-md max-h-full">
|
|
13
13
|
<!-- Modal content -->
|
|
14
|
-
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700
|
|
14
|
+
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
|
|
15
15
|
<!-- Modal header -->
|
|
16
16
|
<div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600">
|
|
17
17
|
<h3 class="text-xl font-semibold text-gray-900 dark:text-white">
|
|
@@ -27,10 +27,10 @@
|
|
|
27
27
|
</div>
|
|
28
28
|
<div
|
|
29
29
|
v-else
|
|
30
|
-
class="relative overflow-x-auto shadow-
|
|
30
|
+
class="relative overflow-x-auto shadow-md sm:rounded-lg"
|
|
31
31
|
>
|
|
32
32
|
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
|
|
33
|
-
<thead class="text-xs text-gray-700 uppercase bg-
|
|
33
|
+
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
|
|
34
34
|
<tr>
|
|
35
35
|
<th scope="col" class="px-6 py-3">
|
|
36
36
|
Field
|
|
@@ -42,17 +42,13 @@
|
|
|
42
42
|
</thead>
|
|
43
43
|
<tbody>
|
|
44
44
|
<tr v-for="column in coreStore.resourceColumns?.filter(c => c.showIn.includes('show'))" :key="column.name"
|
|
45
|
-
class="bg-
|
|
45
|
+
class="odd:bg-white odd:dark:bg-gray-900 even:bg-gray-50 even:dark:bg-gray-800 border-b dark:border-gray-700"
|
|
46
46
|
>
|
|
47
47
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
48
48
|
{{ column.label }}
|
|
49
49
|
</td>
|
|
50
50
|
<td class="px-6 py-4 whitespace-nowrap whitespace-pre-wrap">
|
|
51
|
-
<
|
|
52
|
-
:is="showComponentsPerColumn[column.name] || ValueRenderer"
|
|
53
|
-
:column="column"
|
|
54
|
-
:row="coreStore.record"
|
|
55
|
-
/>
|
|
51
|
+
<ValueRenderer :column="column" :row="coreStore.record" />
|
|
56
52
|
</td>
|
|
57
53
|
</tr>
|
|
58
54
|
|
|
@@ -67,19 +63,18 @@
|
|
|
67
63
|
|
|
68
64
|
<script setup>
|
|
69
65
|
|
|
66
|
+
import { ref, computed, onMounted } from 'vue';
|
|
67
|
+
import { useRoute } from 'vue-router';
|
|
68
|
+
import { callAdminForthApi } from '@/utils';
|
|
70
69
|
import BreadcrumbsWithButtons from '@/components/BreadcrumbsWithButtons.vue';
|
|
71
|
-
import ValueRenderer from '@/components/ValueRenderer.vue';
|
|
72
|
-
import { useCoreStore } from '@/stores/core';
|
|
73
|
-
import { getCustomComponent } from '@/utils';
|
|
74
70
|
import { IconPenSolid, IconTrashBinSolid } from '@iconify-prerendered/vue-flowbite';
|
|
75
|
-
import {
|
|
76
|
-
import
|
|
71
|
+
import { useCoreStore } from '@/stores/core';
|
|
72
|
+
import ValueRenderer from '@/components/ValueRenderer.vue';
|
|
77
73
|
|
|
78
74
|
|
|
79
75
|
const item = ref(null);
|
|
80
76
|
const route = useRoute();
|
|
81
77
|
const loading = ref(false);
|
|
82
|
-
let showComponentsPerColumn = {};
|
|
83
78
|
|
|
84
79
|
const coreStore = useCoreStore();
|
|
85
80
|
|
|
@@ -92,12 +87,6 @@ onMounted(async () => {
|
|
|
92
87
|
resourceId: route.params.resourceId,
|
|
93
88
|
primaryKey: route.params.primaryKey,
|
|
94
89
|
});
|
|
95
|
-
showComponentsPerColumn = coreStore.resourceColumns.reduce((acc, column) => {
|
|
96
|
-
if (column.component?.show) {
|
|
97
|
-
acc[column.name] = getCustomComponent(column.component.show);
|
|
98
|
-
}
|
|
99
|
-
return acc;
|
|
100
|
-
}, {});
|
|
101
90
|
loading.value = false;
|
|
102
91
|
});
|
|
103
92
|
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
/** @type {import('tailwindcss').Config} */
|
|
2
2
|
export default {
|
|
3
|
-
content: ["./src/**/*.{vue, js}",
|
|
3
|
+
content: ["./src/**/*.{vue, js}", "./node_modules/flowbite/**/*.js"],
|
|
4
4
|
theme: {
|
|
5
|
-
extend: {
|
|
6
|
-
/* IMPORTANT:ADMINFORTH TAILWIND STYLES */
|
|
7
|
-
}
|
|
5
|
+
extend: {},
|
|
8
6
|
},
|
|
9
|
-
|
|
10
7
|
darkMode: 'class',
|
|
11
8
|
plugins: [
|
|
12
9
|
require('flowbite/plugin'),
|
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) {
|
|
@@ -707,12 +725,31 @@ class AdminForth implements AdminForthClass {
|
|
|
707
725
|
},
|
|
708
726
|
});
|
|
709
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
|
+
}
|
|
710
747
|
|
|
711
748
|
|
|
712
749
|
server.endpoint({
|
|
713
750
|
method: 'POST',
|
|
714
751
|
path: '/get_resource',
|
|
715
|
-
handler: async ({ body }) => {
|
|
752
|
+
handler: async ({ body, adminUser }) => {
|
|
716
753
|
const { resourceId } = body;
|
|
717
754
|
if (!this.statuses.dbDiscover) {
|
|
718
755
|
return { error: 'Database discovery not started' };
|
|
@@ -724,6 +761,9 @@ class AdminForth implements AdminForthClass {
|
|
|
724
761
|
if (!resource) {
|
|
725
762
|
return { error: `Resource ${resourceId} not found` };
|
|
726
763
|
}
|
|
764
|
+
|
|
765
|
+
await interpretResource(adminUser, resource, {});
|
|
766
|
+
|
|
727
767
|
// exclude "plugins" key
|
|
728
768
|
return { resource: { ...resource, plugins: undefined } };
|
|
729
769
|
},
|
|
@@ -747,6 +787,14 @@ class AdminForth implements AdminForthClass {
|
|
|
747
787
|
return { error: `Resource ${resourceId} not found` };
|
|
748
788
|
}
|
|
749
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
|
+
|
|
750
798
|
for (const hook of listify(resource.hooks?.[source]?.beforeDatasourceRequest)) {
|
|
751
799
|
const resp = await hook({ resource, query: body, adminUser });
|
|
752
800
|
if (!resp || (!resp.ok && !resp.error)) {
|
|
@@ -759,6 +807,8 @@ class AdminForth implements AdminForthClass {
|
|
|
759
807
|
}
|
|
760
808
|
const { limit, offset, filters, sort } = body;
|
|
761
809
|
|
|
810
|
+
|
|
811
|
+
|
|
762
812
|
for (const filter of (filters || [])) {
|
|
763
813
|
if (!Object.values(AdminForthFilterOperators).includes(filter.operator)) {
|
|
764
814
|
throw new Error(`Operator '${filter.operator}' is not allowed`);
|
|
@@ -770,7 +820,7 @@ class AdminForth implements AdminForthClass {
|
|
|
770
820
|
|
|
771
821
|
if (filter.operator === AdminForthFilterOperators.IN || filter.operator === AdminForthFilterOperators.NIN) {
|
|
772
822
|
if (!Array.isArray(filter.value)) {
|
|
773
|
-
throw new Error(`Value for operator '${filter.operator}'
|
|
823
|
+
throw new Error(`Value for operator '${filter.operator}' should be an array`);
|
|
774
824
|
}
|
|
775
825
|
}
|
|
776
826
|
|
|
@@ -958,12 +1008,17 @@ class AdminForth implements AdminForthClass {
|
|
|
958
1008
|
method: 'POST',
|
|
959
1009
|
path: '/create_record',
|
|
960
1010
|
handler: async ({ body, adminUser }) => {
|
|
961
|
-
console.log('create_record', body, this.config.resources);
|
|
962
1011
|
const resource = this.config.resources.find((res) => res.resourceId == body['resourceId']);
|
|
963
1012
|
if (!resource) {
|
|
964
1013
|
return { error: `Resource '${body['resourceId']}' not found` };
|
|
965
1014
|
}
|
|
966
|
-
|
|
1015
|
+
await interpretResource(adminUser, resource, { requestBody: body });
|
|
1016
|
+
|
|
1017
|
+
const { allowed, error } = checkAccess(AllowedActionsEnum.create, resource);
|
|
1018
|
+
if (!allowed) {
|
|
1019
|
+
return { error };
|
|
1020
|
+
}
|
|
1021
|
+
|
|
967
1022
|
const record = body['record'];
|
|
968
1023
|
// execute hook if needed
|
|
969
1024
|
for (const hook of listify(resource.hooks?.create?.beforeSave as BeforeSaveFunction[])) {
|
|
@@ -977,6 +1032,8 @@ class AdminForth implements AdminForthClass {
|
|
|
977
1032
|
}
|
|
978
1033
|
}
|
|
979
1034
|
|
|
1035
|
+
|
|
1036
|
+
|
|
980
1037
|
for (const column of resource.columns) {
|
|
981
1038
|
if (column.fillOnCreate) {
|
|
982
1039
|
if (body['record'][column.name] === undefined) {
|
|
@@ -1036,6 +1093,12 @@ class AdminForth implements AdminForthClass {
|
|
|
1036
1093
|
if (!resource) {
|
|
1037
1094
|
return { error: `Resource '${body['resourceId']}' not found` };
|
|
1038
1095
|
}
|
|
1096
|
+
await interpretResource(adminUser, resource, { requestBody: body });
|
|
1097
|
+
|
|
1098
|
+
const { allowed, error } = checkAccess(AllowedActionsEnum.edit, resource);
|
|
1099
|
+
if (!allowed) {
|
|
1100
|
+
return { error };
|
|
1101
|
+
}
|
|
1039
1102
|
|
|
1040
1103
|
const recordId = body['recordId'];
|
|
1041
1104
|
const connector = this.connectors[resource.dataSource];
|
|
@@ -1110,6 +1173,13 @@ class AdminForth implements AdminForthClass {
|
|
|
1110
1173
|
return { error: `Resource '${resource.resourceId}' does not allow delete action` };
|
|
1111
1174
|
}
|
|
1112
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
|
+
|
|
1113
1183
|
// execute hook if needed
|
|
1114
1184
|
for (const hook of listify(resource.hooks?.delete?.beforeSave as BeforeSaveFunction[])) {
|
|
1115
1185
|
const resp = await hook({ resource, record, adminUser });
|
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 page - The page to check access for
|
|
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, page: 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
|
|
|
@@ -382,4 +385,35 @@ function onSortButtonClick(field) {
|
|
|
382
385
|
}
|
|
383
386
|
}
|
|
384
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
|
+
|
|
385
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
|
+
}
|