adminforth 1.1.12 → 1.1.14
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/auth.ts +17 -3
- package/dist/auth.js +32 -18
- package/dist/index.js +107 -63
- package/dist/modules/codeInjector.js +9 -3
- package/dist/modules/utils.js +2 -12
- package/dist/plugins/AccessControl/index.js +66 -0
- package/dist/plugins/AccessControl/types.js +1 -0
- package/dist/plugins/ForeignInlineListPlugin/index.js +1 -1
- package/dist/servers/express.js +2 -2
- package/dist/spa/spa/src/App.vue +7 -3
- package/dist/spa/spa/src/components/Toast.vue +5 -4
- package/dist/spa/spa/src/components/ValueRenderer.vue +1 -1
- package/dist/spa/spa/src/composables/useStores.ts +2 -1
- package/dist/spa/spa/src/router/index.ts +13 -1
- package/dist/spa/spa/src/stores/core.ts +13 -11
- package/dist/spa/spa/src/stores/user.ts +50 -0
- package/dist/spa/spa/src/utils.ts +8 -3
- package/dist/spa/spa/src/views/CreateView.vue +7 -0
- package/dist/spa/spa/src/views/EditView.vue +8 -1
- package/dist/spa/spa/src/views/ListView.vue +10 -1
- package/dist/types/AdminForthConfig.js +9 -0
- package/dist/types/FrontendAPI.js +4 -4
- package/index.ts +103 -51
- package/modules/codeInjector.ts +9 -3
- package/modules/utils.ts +2 -10
- package/package.json +1 -1
- package/plugins/AccessControl/index.ts +83 -0
- package/plugins/AccessControl/types.ts +14 -0
- package/plugins/ForeignInlineListPlugin/custom/InlineList.vue +13 -1
- package/plugins/ForeignInlineListPlugin/index.ts +2 -1
- package/servers/express.ts +2 -2
- package/spa/src/App.vue +7 -3
- package/spa/src/components/Toast.vue +5 -4
- package/spa/src/components/ValueRenderer.vue +1 -1
- package/spa/src/composables/useStores.ts +2 -1
- package/spa/src/router/index.ts +13 -1
- package/spa/src/stores/core.ts +13 -11
- package/spa/src/stores/user.ts +50 -0
- package/spa/src/utils.ts +8 -3
- package/spa/src/views/CreateView.vue +7 -0
- package/spa/src/views/EditView.vue +8 -1
- package/spa/src/views/ListView.vue +10 -1
- package/types/AdminForthConfig.ts +73 -21
- package/types/FrontendAPI.ts +11 -5
package/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
import
|
|
2
|
+
import AdminForthAuth from './auth.js';
|
|
3
3
|
import MongoConnector from './dataConnectors/mongo.js';
|
|
4
4
|
import PostgresConnector from './dataConnectors/postgres.js';
|
|
5
5
|
import SQLiteConnector from './dataConnectors/sqlite.js';
|
|
@@ -8,10 +8,13 @@ import { guessLabelFromName } from './modules/utils.js';
|
|
|
8
8
|
import ExpressServer from './servers/express.js';
|
|
9
9
|
import {v1 as uuid} from 'uuid';
|
|
10
10
|
import fs from 'fs';
|
|
11
|
-
import { ADMINFORTH_VERSION } from './modules/utils.js';
|
|
11
|
+
import { ADMINFORTH_VERSION, listify } from './modules/utils.js';
|
|
12
12
|
import { AdminForthConfig, AdminForthClass, AdminForthComponentDeclaration, AdminForthComponentDeclarationFull,
|
|
13
|
-
AdminForthFilterOperators, AdminForthDataTypes, AdminForthResourcePages
|
|
14
|
-
|
|
13
|
+
AdminForthFilterOperators, AdminForthDataTypes, AdminForthResourcePages, GenericHttpServer,
|
|
14
|
+
BeforeSaveFunction,
|
|
15
|
+
AfterSaveFunction,
|
|
16
|
+
BeforeDataSourceRequestFunction,
|
|
17
|
+
AfterDataSourceResponseFunction} from './types/AdminForthConfig.js';
|
|
15
18
|
import path from 'path';
|
|
16
19
|
import AdminForthPlugin from './plugins/base.js';
|
|
17
20
|
|
|
@@ -26,7 +29,7 @@ class AdminForth implements AdminForthClass {
|
|
|
26
29
|
|
|
27
30
|
static Utils = {
|
|
28
31
|
generatePasswordHash: async (password) => {
|
|
29
|
-
return await
|
|
32
|
+
return await AdminForthAuth.generatePasswordHash(password);
|
|
30
33
|
}
|
|
31
34
|
}
|
|
32
35
|
|
|
@@ -36,7 +39,7 @@ class AdminForth implements AdminForthClass {
|
|
|
36
39
|
|
|
37
40
|
config: AdminForthConfig;
|
|
38
41
|
express: ExpressServer;
|
|
39
|
-
auth:
|
|
42
|
+
auth: AdminForthAuth;
|
|
40
43
|
codeInjector: CodeInjector;
|
|
41
44
|
connectors: any;
|
|
42
45
|
connectorClasses: any;
|
|
@@ -57,7 +60,7 @@ class AdminForth implements AdminForthClass {
|
|
|
57
60
|
this.validateConfig(); // revalidate after plugins
|
|
58
61
|
|
|
59
62
|
this.express = new ExpressServer(this);
|
|
60
|
-
this.auth = new
|
|
63
|
+
this.auth = new AdminForthAuth(this);
|
|
61
64
|
this.connectors = {};
|
|
62
65
|
this.statuses = {};
|
|
63
66
|
|
|
@@ -83,7 +86,7 @@ class AdminForth implements AdminForthClass {
|
|
|
83
86
|
return [];
|
|
84
87
|
}
|
|
85
88
|
|
|
86
|
-
validateComponent(component: AdminForthComponentDeclaration, errors: Array<string
|
|
89
|
+
validateComponent(component: AdminForthComponentDeclaration, errors: Array<string>, ignoreExistsCheck: boolean = false): AdminForthComponentDeclaration {
|
|
87
90
|
if (!component) {
|
|
88
91
|
return component;
|
|
89
92
|
}
|
|
@@ -93,7 +96,9 @@ class AdminForth implements AdminForthClass {
|
|
|
93
96
|
} else {
|
|
94
97
|
obj = component;
|
|
95
98
|
}
|
|
96
|
-
|
|
99
|
+
if (!ignoreExistsCheck) {
|
|
100
|
+
errors.push(...this.checkCustomFileExists(obj.file));
|
|
101
|
+
}
|
|
97
102
|
|
|
98
103
|
return obj;
|
|
99
104
|
}
|
|
@@ -235,7 +240,7 @@ class AdminForth implements AdminForthClass {
|
|
|
235
240
|
if(res.options?.bulkActions){
|
|
236
241
|
let bulkActions = res.options.bulkActions;
|
|
237
242
|
|
|
238
|
-
if(!Array.isArray(bulkActions)){
|
|
243
|
+
if (!Array.isArray(bulkActions)) {
|
|
239
244
|
errors.push(`Resource "${res.resourceId}" bulkActions must be an array`);
|
|
240
245
|
bulkActions = [];
|
|
241
246
|
}
|
|
@@ -292,7 +297,40 @@ class AdminForth implements AdminForthClass {
|
|
|
292
297
|
} else {
|
|
293
298
|
res.options.allowedActions = DEFAULT_ALLOWED_ACTIONS;
|
|
294
299
|
}
|
|
295
|
-
|
|
300
|
+
|
|
301
|
+
// transform all hooks Functions to array of functions
|
|
302
|
+
if (res.hooks) {
|
|
303
|
+
for (const value of [res.hooks.show, res.hooks.list]) {
|
|
304
|
+
if (value) {
|
|
305
|
+
if (value.beforeDatasourceRequest) {
|
|
306
|
+
if (!Array.isArray(value.beforeDatasourceRequest)) {
|
|
307
|
+
value.beforeDatasourceRequest = [value.beforeDatasourceRequest];
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (value.afterDatasourceResponse) {
|
|
311
|
+
if (!Array.isArray(value.afterDatasourceResponse)) {
|
|
312
|
+
value.afterDatasourceResponse = [value.afterDatasourceResponse];
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
for (const value of [res.hooks.create, res.hooks.edit, res.hooks.delete]) {
|
|
318
|
+
if (value) {
|
|
319
|
+
|
|
320
|
+
if (value.beforeSave) {
|
|
321
|
+
if (!Array.isArray(value.beforeSave)) {
|
|
322
|
+
value.beforeSave = [value.beforeSave];
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (value.afterSave) {
|
|
326
|
+
if (!Array.isArray(value.afterSave)) {
|
|
327
|
+
value.afterSave = [value.afterSave];
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
});
|
|
296
334
|
|
|
297
335
|
|
|
298
336
|
|
|
@@ -371,15 +409,15 @@ class AdminForth implements AdminForthClass {
|
|
|
371
409
|
for (const resource of this.config.resources) {
|
|
372
410
|
for (const column of resource.columns) {
|
|
373
411
|
if (column.components) {
|
|
374
|
-
console.log('🔧🔧🔧 Validating components for resource', column.components);
|
|
375
412
|
|
|
376
|
-
for (const comp of Object.
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
413
|
+
for (const [key, comp] of Object.entries(column.components as Record<string, AdminForthComponentDeclarationFull>)) {
|
|
414
|
+
let ignoreExistsCheck = false;
|
|
415
|
+
if (this.codeInjector.allComponentNames[comp.file]) {
|
|
416
|
+
// not obvious, but if we are in this if, it means that this is plugin component
|
|
417
|
+
// and there is no sense to check if it exists in users folder
|
|
418
|
+
ignoreExistsCheck = true;
|
|
419
|
+
}
|
|
420
|
+
column.components[key] = this.validateComponent(comp, errors, ignoreExistsCheck);
|
|
383
421
|
}
|
|
384
422
|
}
|
|
385
423
|
}
|
|
@@ -458,7 +496,24 @@ class AdminForth implements AdminForthClass {
|
|
|
458
496
|
this.codeInjector.bundleNow({ hotReload, verbose });
|
|
459
497
|
}
|
|
460
498
|
|
|
461
|
-
|
|
499
|
+
async getUserByPk(pk: string) {
|
|
500
|
+
const resource = this.config.resources.find((res) => res.resourceId === this.config.auth.resourceId);
|
|
501
|
+
if (!resource) {
|
|
502
|
+
throw new Error('No auth resource found');
|
|
503
|
+
}
|
|
504
|
+
const users = await this.connectors[resource.dataSource].getData({
|
|
505
|
+
resource,
|
|
506
|
+
filters: [
|
|
507
|
+
{ field: resource.columns.find((col) => col.primaryKey).name, operator: AdminForthFilterOperators.EQ, value: pk },
|
|
508
|
+
],
|
|
509
|
+
limit: 1,
|
|
510
|
+
offset: 0,
|
|
511
|
+
sort: [],
|
|
512
|
+
});
|
|
513
|
+
return users.data[0] || null;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
setupEndpoints(server: GenericHttpServer) {
|
|
462
517
|
server.endpoint({
|
|
463
518
|
noAuth: true,
|
|
464
519
|
method: 'POST',
|
|
@@ -504,7 +559,7 @@ class AdminForth implements AdminForthClass {
|
|
|
504
559
|
|
|
505
560
|
const passwordHash = userRecord[this.config.auth.passwordHashField];
|
|
506
561
|
console.log('User record', userRecord, passwordHash) // why does it has no hash?
|
|
507
|
-
const valid = await
|
|
562
|
+
const valid = await AdminForthAuth.verifyPassword(password, passwordHash);
|
|
508
563
|
if (valid) {
|
|
509
564
|
token = this.auth.issueJWT({
|
|
510
565
|
username, pk: userRecord[userResource.columns.find((col) => col.primaryKey).name]
|
|
@@ -519,6 +574,14 @@ class AdminForth implements AdminForthClass {
|
|
|
519
574
|
},
|
|
520
575
|
});
|
|
521
576
|
|
|
577
|
+
server.endpoint({
|
|
578
|
+
method: 'POST',
|
|
579
|
+
path: '/check_auth',
|
|
580
|
+
handler: async ({ adminUser }) => {
|
|
581
|
+
return { ok: true };
|
|
582
|
+
},
|
|
583
|
+
});
|
|
584
|
+
|
|
522
585
|
server.endpoint({
|
|
523
586
|
noAuth: true,
|
|
524
587
|
method: 'POST',
|
|
@@ -556,27 +619,14 @@ class AdminForth implements AdminForthClass {
|
|
|
556
619
|
method: 'GET',
|
|
557
620
|
path: '/get_base_config',
|
|
558
621
|
handler: async ({input, adminUser, cookies}) => {
|
|
559
|
-
const cookieParsed = this.auth.verify(cookies['adminforth_jwt']);
|
|
560
622
|
let username = ''
|
|
561
623
|
let userFullName = ''
|
|
562
|
-
if (
|
|
624
|
+
if (adminUser.isRoot) {
|
|
563
625
|
username = this.config.rootUser.username;
|
|
564
626
|
} else {
|
|
565
|
-
const
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
filters: [
|
|
569
|
-
{ field: userResource.columns.find((col) => col.primaryKey).name, operator: AdminForthFilterOperators.EQ, value: cookieParsed['pk'] },
|
|
570
|
-
],
|
|
571
|
-
limit: 1,
|
|
572
|
-
offset: 0,
|
|
573
|
-
sort: [],
|
|
574
|
-
});
|
|
575
|
-
if (!user.data.length) {
|
|
576
|
-
return { error: 'Unauthorized' };
|
|
577
|
-
}
|
|
578
|
-
username = user.data[0][this.config.auth.usernameField];
|
|
579
|
-
userFullName = user.data[0][this.config.auth.userFullNameField];
|
|
627
|
+
const dbUser = adminUser.dbUser;
|
|
628
|
+
username = dbUser[this.config.auth.usernameField];
|
|
629
|
+
userFullName =dbUser[this.config.auth.userFullNameField];
|
|
580
630
|
}
|
|
581
631
|
|
|
582
632
|
const userData = {
|
|
@@ -606,6 +656,8 @@ class AdminForth implements AdminForthClass {
|
|
|
606
656
|
},
|
|
607
657
|
});
|
|
608
658
|
|
|
659
|
+
|
|
660
|
+
|
|
609
661
|
server.endpoint({
|
|
610
662
|
method: 'POST',
|
|
611
663
|
path: '/get_resource',
|
|
@@ -644,7 +696,7 @@ class AdminForth implements AdminForthClass {
|
|
|
644
696
|
return { error: `Resource ${resourceId} not found` };
|
|
645
697
|
}
|
|
646
698
|
|
|
647
|
-
for (const hook of
|
|
699
|
+
for (const hook of listify(resource.hooks?.[source]?.beforeDatasourceRequest)) {
|
|
648
700
|
const resp = await hook({ resource, query: body, adminUser });
|
|
649
701
|
if (!resp || (!resp.ok && !resp.error)) {
|
|
650
702
|
throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `);
|
|
@@ -675,7 +727,7 @@ class AdminForth implements AdminForthClass {
|
|
|
675
727
|
// nonsense
|
|
676
728
|
return { data: [], total: 0 };
|
|
677
729
|
}
|
|
678
|
-
|
|
730
|
+
}
|
|
679
731
|
|
|
680
732
|
const data = await this.connectors[resource.dataSource].getData({
|
|
681
733
|
resource,
|
|
@@ -720,7 +772,7 @@ class AdminForth implements AdminForthClass {
|
|
|
720
772
|
})
|
|
721
773
|
);
|
|
722
774
|
|
|
723
|
-
for (const hook of
|
|
775
|
+
for (const hook of listify(resource.hooks?.[source]?.afterDatasourceResponse)) {
|
|
724
776
|
const resp = await hook({ resource, response: data.data, adminUser });
|
|
725
777
|
if (!resp || (!resp.ok && !resp.error)) {
|
|
726
778
|
throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `);
|
|
@@ -775,8 +827,8 @@ class AdminForth implements AdminForthClass {
|
|
|
775
827
|
const targetResourceId = columnConfig.foreignResource.resourceId;
|
|
776
828
|
const targetResource = this.config.resources.find((res) => res.resourceId == targetResourceId);
|
|
777
829
|
|
|
778
|
-
for (const hook of
|
|
779
|
-
const resp = await hook({ query: body, adminUser });
|
|
830
|
+
for (const hook of listify(columnConfig.foreignResource.hooks?.dropdownList?.beforeDatasourceRequest as BeforeDataSourceRequestFunction[])) {
|
|
831
|
+
const resp = await hook({ query: body, adminUser, resource: targetResource });
|
|
780
832
|
if (!resp || (!resp.ok && !resp.error)) {
|
|
781
833
|
throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `);
|
|
782
834
|
}
|
|
@@ -806,8 +858,8 @@ class AdminForth implements AdminForthClass {
|
|
|
806
858
|
items
|
|
807
859
|
};
|
|
808
860
|
|
|
809
|
-
for (const hook of
|
|
810
|
-
const resp = await hook({ response, adminUser });
|
|
861
|
+
for (const hook of listify(columnConfig.foreignResource.hooks?.dropdownList?.afterDatasourceResponse as AfterDataSourceResponseFunction[])) {
|
|
862
|
+
const resp = await hook({ response, adminUser, resource: targetResource });
|
|
811
863
|
if (!resp || (!resp.ok && !resp.error)) {
|
|
812
864
|
throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `);
|
|
813
865
|
}
|
|
@@ -863,7 +915,7 @@ class AdminForth implements AdminForthClass {
|
|
|
863
915
|
|
|
864
916
|
const record = body['record'];
|
|
865
917
|
// execute hook if needed
|
|
866
|
-
for (const hook of
|
|
918
|
+
for (const hook of listify(resource.hooks?.create?.beforeSave as BeforeSaveFunction[])) {
|
|
867
919
|
const resp = await hook({ resource, record, adminUser });
|
|
868
920
|
if (!resp || (!resp.ok && !resp.error)) {
|
|
869
921
|
throw new Error(`Hook beforeSave must return object with {ok: true} or { error: 'Error' } `);
|
|
@@ -909,7 +961,7 @@ class AdminForth implements AdminForthClass {
|
|
|
909
961
|
const connector = this.connectors[resource.dataSource];
|
|
910
962
|
await connector.createRecord({ resource, record });
|
|
911
963
|
// execute hook if needed
|
|
912
|
-
for (const hook of
|
|
964
|
+
for (const hook of listify(resource.hooks?.create?.afterSave as AfterSaveFunction[])) {
|
|
913
965
|
const resp = await hook({ resource, record, adminUser });
|
|
914
966
|
if (!resp || (!resp.ok && !resp.error)) {
|
|
915
967
|
throw new Error(`Hook afterSave must return object with {ok: true} or { error: 'Error' } `);
|
|
@@ -944,7 +996,7 @@ class AdminForth implements AdminForthClass {
|
|
|
944
996
|
const record = body['record'];
|
|
945
997
|
|
|
946
998
|
// execute hook if needed
|
|
947
|
-
for (const hook of
|
|
999
|
+
for (const hook of listify(resource.hooks?.edit?.beforeSave as BeforeSaveFunction[])) {
|
|
948
1000
|
const resp = await hook({ resource, record, adminUser });
|
|
949
1001
|
if (!resp || (!resp.ok && !resp.error)) {
|
|
950
1002
|
throw new Error(`Hook beforeSave must return object with {ok: true} or { error: 'Error' } `);
|
|
@@ -975,7 +1027,7 @@ class AdminForth implements AdminForthClass {
|
|
|
975
1027
|
}
|
|
976
1028
|
|
|
977
1029
|
// execute hook if needed
|
|
978
|
-
for (const hook of
|
|
1030
|
+
for (const hook of listify(resource.hooks?.edit?.afterSave as AfterSaveFunction[])) {
|
|
979
1031
|
const resp = await hook({ resource, record, adminUser });
|
|
980
1032
|
if (!resp || (!resp.ok && !resp.error)) {
|
|
981
1033
|
throw new Error(`Hook afterSave must return object with {ok: true} or { error: 'Error' } `);
|
|
@@ -1008,7 +1060,7 @@ class AdminForth implements AdminForthClass {
|
|
|
1008
1060
|
}
|
|
1009
1061
|
|
|
1010
1062
|
// execute hook if needed
|
|
1011
|
-
for (const hook of
|
|
1063
|
+
for (const hook of listify(resource.hooks?.delete?.beforeSave as BeforeSaveFunction[])) {
|
|
1012
1064
|
const resp = await hook({ resource, record, adminUser });
|
|
1013
1065
|
if (!resp || (!resp.ok && !resp.error)) {
|
|
1014
1066
|
throw new Error(`Hook beforeSave must return object with {ok: true} or { error: 'Error' } `);
|
|
@@ -1023,7 +1075,7 @@ class AdminForth implements AdminForthClass {
|
|
|
1023
1075
|
await connector.deleteRecord({ resource, recordId: body['primaryKey']});
|
|
1024
1076
|
|
|
1025
1077
|
// execute hook if needed
|
|
1026
|
-
for (const hook of
|
|
1078
|
+
for (const hook of listify(resource.hooks?.delete?.afterSave as BeforeSaveFunction[])) {
|
|
1027
1079
|
const resp = await hook({ resource, record, adminUser });
|
|
1028
1080
|
if (!resp || (!resp.ok && !resp.error)) {
|
|
1029
1081
|
throw new Error(`Hook afterSave must return object with {ok: true} or { error: 'Error' } `);
|
package/modules/codeInjector.ts
CHANGED
|
@@ -254,10 +254,13 @@ class CodeInjector implements CodeInjectorType {
|
|
|
254
254
|
// for each custom component generate import statement
|
|
255
255
|
const customResourceComponents = [];
|
|
256
256
|
this.adminforth.config.resources.forEach((resource) => {
|
|
257
|
-
resource.columns.forEach((
|
|
258
|
-
if (
|
|
259
|
-
Object.values(
|
|
257
|
+
resource.columns.forEach((column) => {
|
|
258
|
+
if (column.components) {
|
|
259
|
+
Object.values(column.components).forEach(({ file }: {file: string}) => {
|
|
260
260
|
if (!customResourceComponents.includes(file)) {
|
|
261
|
+
if (file === undefined) {
|
|
262
|
+
throw new Error('file is undefined from field.components, field:' + JSON.stringify(column));
|
|
263
|
+
}
|
|
261
264
|
customResourceComponents.push(file);
|
|
262
265
|
}
|
|
263
266
|
});
|
|
@@ -267,6 +270,9 @@ class CodeInjector implements CodeInjectorType {
|
|
|
267
270
|
Object.values(injection).forEach((filePathes: {file: string}[]) => {
|
|
268
271
|
filePathes.forEach(({ file }) => {
|
|
269
272
|
if (!customResourceComponents.includes(file)) {
|
|
273
|
+
if (file === undefined) {
|
|
274
|
+
throw new Error('file is undefined');
|
|
275
|
+
}
|
|
270
276
|
customResourceComponents.push(file);
|
|
271
277
|
}
|
|
272
278
|
});
|
package/modules/utils.ts
CHANGED
|
@@ -34,14 +34,6 @@ export function getComponentNameFromPath(filePath) {
|
|
|
34
34
|
return filePath.replace(/@/g, '').replace(/\./g, '').replace(/\//g, '');
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
export function
|
|
38
|
-
|
|
39
|
-
if (Array.isArray(param)) {
|
|
40
|
-
return param;
|
|
41
|
-
} else {
|
|
42
|
-
return [param];
|
|
43
|
-
}
|
|
44
|
-
} else {
|
|
45
|
-
return [];
|
|
46
|
-
}
|
|
37
|
+
export function listify(param?: Array<Function>) {
|
|
38
|
+
return param || [];
|
|
47
39
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AdminForthResource, AdminForthClass, BeforeDataSourceRequestFunction,
|
|
3
|
+
AllowedActionsEnum,
|
|
4
|
+
BeforeSaveFunction
|
|
5
|
+
} from "../../types/AdminForthConfig.js";
|
|
6
|
+
import AdminForthPlugin from "../base.js";
|
|
7
|
+
import { PluginOptions } from "./types.js";
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
export default class AccessControlPlugin extends AdminForthPlugin {
|
|
12
|
+
|
|
13
|
+
options: PluginOptions;
|
|
14
|
+
adminforth: AdminForthClass;
|
|
15
|
+
|
|
16
|
+
constructor(options: PluginOptions) {
|
|
17
|
+
super(options, import.meta.url);
|
|
18
|
+
this.options = options;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static defaultError = 'Sorry, you do not have access to this resource.'
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
modifyResourceConfig(adminforth: AdminForthClass, resourceConfig: AdminForthResource) {
|
|
25
|
+
super.modifyResourceConfig(adminforth, resourceConfig);
|
|
26
|
+
this.adminforth = adminforth;
|
|
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
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
|
|
2
|
+
import { AllowedActionsEnum, AdminUser } from '../../types/AdminForthConfig.js';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
export type PluginOptions = {
|
|
6
|
+
/**
|
|
7
|
+
* Called to check if user has access to a page
|
|
8
|
+
* @param user - The user object
|
|
9
|
+
* @param page - The page to check access for
|
|
10
|
+
* @param meta - The meta object containing query for list/show or record for save/edit
|
|
11
|
+
* @returns true if user has access, false or a string with an error message
|
|
12
|
+
*/
|
|
13
|
+
hasAccess: (user: AdminUser, page: AllowedActionsEnum, meta: any) => Promise<boolean | string>;
|
|
14
|
+
}
|
|
@@ -90,6 +90,7 @@
|
|
|
90
90
|
:totalRows="totalRows"
|
|
91
91
|
:checkboxes="checkboxes"
|
|
92
92
|
/>
|
|
93
|
+
|
|
93
94
|
</td>
|
|
94
95
|
</template>
|
|
95
96
|
|
|
@@ -114,7 +115,7 @@ const loading = ref(true);
|
|
|
114
115
|
const page = ref(1);
|
|
115
116
|
const sort = ref([]);
|
|
116
117
|
const checkboxes = ref([]);
|
|
117
|
-
const pageSize = computed(() => listResource.value?.options?.
|
|
118
|
+
const pageSize = computed(() => listResource.value?.options?.listPageSize || 10);
|
|
118
119
|
|
|
119
120
|
const rows = ref(null);
|
|
120
121
|
const totalRows = ref(0);
|
|
@@ -207,6 +208,17 @@ async function getList() {
|
|
|
207
208
|
}
|
|
208
209
|
});
|
|
209
210
|
|
|
211
|
+
if (data.error) {
|
|
212
|
+
window.adminforth.alert({
|
|
213
|
+
message: data.error,
|
|
214
|
+
variant: 'danger',
|
|
215
|
+
timeout: 'unlimited',
|
|
216
|
+
});
|
|
217
|
+
rows.value = [];
|
|
218
|
+
totalRows.value = 0;
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
210
222
|
rows.value = data.data?.map(row => {
|
|
211
223
|
row._primaryKeyValue = row[listResource.value.columns.find(c => c.primaryKey).name];
|
|
212
224
|
return row;
|
|
@@ -23,7 +23,8 @@ export default class ForeignInlineListPlugin extends AdminForthPlugin {
|
|
|
23
23
|
return { error: `Resource ${this.options.foreignResourceId} not found` };
|
|
24
24
|
}
|
|
25
25
|
// exclude "plugins" key
|
|
26
|
-
const resourceCopy = { ...resource, plugins: undefined };
|
|
26
|
+
const resourceCopy = JSON.parse(JSON.stringify({ ...resource, plugins: undefined }));
|
|
27
|
+
|
|
27
28
|
if (this.options.modifyTableResourceConfig) {
|
|
28
29
|
this.options.modifyTableResourceConfig(resourceCopy);
|
|
29
30
|
}
|
package/servers/express.ts
CHANGED
|
@@ -157,7 +157,7 @@ class ExpressServer implements ExpressHttpServer {
|
|
|
157
157
|
res.status(401).send('Unauthorized by AdminForth');
|
|
158
158
|
return
|
|
159
159
|
}
|
|
160
|
-
const adminforthUser = this.adminforth.auth.verify(jwt);
|
|
160
|
+
const adminforthUser = await this.adminforth.auth.verify(jwt);
|
|
161
161
|
if (!adminforthUser) {
|
|
162
162
|
res.status(401).send('Unauthorized by AdminForth');
|
|
163
163
|
} else {
|
|
@@ -201,7 +201,7 @@ class ExpressServer implements ExpressHttpServer {
|
|
|
201
201
|
this.message = message;
|
|
202
202
|
}
|
|
203
203
|
};
|
|
204
|
-
const input = { body, query, headers, cookies, response, _raw_express_req: req, _raw_express_res: res};
|
|
204
|
+
const input = { body, query, headers, cookies, adminUser, response, _raw_express_req: req, _raw_express_res: res};
|
|
205
205
|
|
|
206
206
|
let output;
|
|
207
207
|
try {
|
package/spa/src/App.vue
CHANGED
|
@@ -64,7 +64,6 @@
|
|
|
64
64
|
aria-label="Sidebar"
|
|
65
65
|
>
|
|
66
66
|
<div class="h-full px-3 pb-4 overflow-y-auto bg-nav-menu-bg dark:bg-gray-800 border-r">
|
|
67
|
-
|
|
68
67
|
<div class="flex ms-2 md:me-24 m-4 ">
|
|
69
68
|
<img :src="loadFile(coreStore.config?.brandLogo || '@/assets/logo.svg')" :alt="`${ coreStore.config?.brandName } Logo`" class="h-8 me-3" />
|
|
70
69
|
<span class="self-center text-header-text-size font-semibold sm:text-header-text-size whitespace-nowrap dark:text-header-text text-header-logo-color">
|
|
@@ -168,6 +167,7 @@ import { RouterLink, RouterView } from 'vue-router';
|
|
|
168
167
|
import { initFlowbite } from 'flowbite'
|
|
169
168
|
import './index.scss'
|
|
170
169
|
import { useCoreStore } from '@/stores/core';
|
|
170
|
+
import { useUserStore } from '@/stores/user';
|
|
171
171
|
import {useModalStore} from '@/stores/modal';
|
|
172
172
|
import { IconMoonSolid, IconSunSolid } from '@iconify-prerendered/vue-flowbite';
|
|
173
173
|
import AcceptModal from './components/AcceptModal.vue';
|
|
@@ -184,6 +184,7 @@ import { FrontendAPI } from '@/composables/useStores'
|
|
|
184
184
|
const coreStore = useCoreStore();
|
|
185
185
|
const modalStore = useModalStore();
|
|
186
186
|
const toastStore = useToastStore();
|
|
187
|
+
const userStore = useUserStore();
|
|
187
188
|
const frontendApi = new FrontendAPI();
|
|
188
189
|
frontendApi.init();
|
|
189
190
|
const splitAtLast = (str: string, separator: string) => {
|
|
@@ -221,8 +222,9 @@ if (opened.value.includes(label)) {
|
|
|
221
222
|
}
|
|
222
223
|
|
|
223
224
|
async function logout() {
|
|
224
|
-
|
|
225
|
-
|
|
225
|
+
userStore.unauthorize();
|
|
226
|
+
await userStore.logout();
|
|
227
|
+
router.push({ name: 'login' })
|
|
226
228
|
}
|
|
227
229
|
|
|
228
230
|
|
|
@@ -244,6 +246,8 @@ async function loadMenu() {
|
|
|
244
246
|
});
|
|
245
247
|
}
|
|
246
248
|
|
|
249
|
+
|
|
250
|
+
|
|
247
251
|
watch(route, () => {
|
|
248
252
|
title.value = `${coreStore.config?.title || coreStore.config?.brandName || 'Adminforth'} | ${ Object.values(route.params)[0] || route.meta.title || ' '}`;
|
|
249
253
|
useHead({
|
|
@@ -46,16 +46,17 @@ const props = defineProps<{
|
|
|
46
46
|
toast:{message: string;
|
|
47
47
|
variant: string;
|
|
48
48
|
id: string;
|
|
49
|
-
|
|
49
|
+
timeout?: number|'unlimited';}
|
|
50
50
|
}>();
|
|
51
51
|
function closeToast() {
|
|
52
52
|
emit('close');
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
onMounted(() => {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
if (props.toast.timeout === 'unlimited') return;
|
|
57
|
+
else {
|
|
58
|
+
setTimeout(() => {emit('close');}, props.toast.timeout || 10000 );
|
|
59
|
+
}
|
|
59
60
|
});
|
|
60
61
|
|
|
61
62
|
</script>
|
package/spa/src/router/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createRouter, createWebHistory } from 'vue-router'
|
|
2
2
|
import ResourceParent from '@/views/ResourceParent.vue'
|
|
3
|
+
import { useUserStore } from '@/stores/user'
|
|
3
4
|
/* IMPORTANT:ADMINFORTH ROUTES IMPORTS */
|
|
4
5
|
|
|
5
6
|
const router = createRouter({
|
|
@@ -9,7 +10,15 @@ const router = createRouter({
|
|
|
9
10
|
path: '/login',
|
|
10
11
|
name: 'login',
|
|
11
12
|
component: () => import('@/views/LoginView.vue'),
|
|
12
|
-
meta: { title: 'login' }
|
|
13
|
+
meta: { title: 'login' },
|
|
14
|
+
beforeEnter: async (to, from, next) => {
|
|
15
|
+
const userStore = useUserStore()
|
|
16
|
+
if (await userStore.checkAuth(true)) {
|
|
17
|
+
next({ name: 'home' })
|
|
18
|
+
} else {
|
|
19
|
+
next()
|
|
20
|
+
}
|
|
21
|
+
}
|
|
13
22
|
},
|
|
14
23
|
{
|
|
15
24
|
path: '/resource/:resourceId',
|
|
@@ -49,4 +58,7 @@ const router = createRouter({
|
|
|
49
58
|
})
|
|
50
59
|
|
|
51
60
|
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
52
64
|
export default router
|