adminforth 1.1.13 → 1.1.15
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 +114 -58
- package/dist/modules/codeInjector.js +0 -1
- 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/plugins/plugins/ForeignInlineListPlugin/custom/InlineList.vue +248 -0
- 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 +54 -0
- package/dist/spa/spa/src/utils.ts +9 -4
- 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/spa/spa/src/views/LoginView.vue +4 -0
- package/dist/types/AdminForthConfig.js +9 -0
- package/dist/types/FrontendAPI.js +4 -4
- package/index.ts +106 -42
- package/modules/codeInjector.ts +0 -1
- package/modules/utils.ts +2 -10
- package/package.json +3 -2
- package/plugins/AccessControl/index.ts +83 -0
- package/plugins/AccessControl/types.ts +14 -0
- package/plugins/ForeignInlineListPlugin/custom/InlineList.vue +11 -0
- 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 +54 -0
- package/spa/src/utils.ts +9 -4
- 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/spa/src/views/LoginView.vue +4 -0
- 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
|
|
|
@@ -226,6 +229,21 @@ class AdminForth implements AdminForthClass {
|
|
|
226
229
|
errors.push(`Resource "${res.resourceId}" column "${col.name}" has invalid showIn value "${wrongShowIn}", allowed values are ${Object.keys(AdminForthResourcePages).join(', ')}`);
|
|
227
230
|
}
|
|
228
231
|
col.showIn = col.showIn || Object.values(AdminForthResourcePages);
|
|
232
|
+
|
|
233
|
+
if (col.foreignResource) {
|
|
234
|
+
const befHook = col.foreignResource.hooks?.dropdownList?.beforeDatasourceRequest;
|
|
235
|
+
if (befHook) {
|
|
236
|
+
if (!Array.isArray(befHook)) {
|
|
237
|
+
col.foreignResource.hooks.dropdownList.beforeDatasourceRequest = [befHook];
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const aftHook = col.foreignResource.hooks?.dropdownList?.afterDatasourceResponse;
|
|
241
|
+
if (aftHook) {
|
|
242
|
+
if (!Array.isArray(aftHook)) {
|
|
243
|
+
col.foreignResource.hooks.dropdownList.afterDatasourceResponse = [aftHook];
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
229
247
|
})
|
|
230
248
|
|
|
231
249
|
if (!res.options) {
|
|
@@ -237,7 +255,7 @@ class AdminForth implements AdminForthClass {
|
|
|
237
255
|
if(res.options?.bulkActions){
|
|
238
256
|
let bulkActions = res.options.bulkActions;
|
|
239
257
|
|
|
240
|
-
if(!Array.isArray(bulkActions)){
|
|
258
|
+
if (!Array.isArray(bulkActions)) {
|
|
241
259
|
errors.push(`Resource "${res.resourceId}" bulkActions must be an array`);
|
|
242
260
|
bulkActions = [];
|
|
243
261
|
}
|
|
@@ -294,7 +312,40 @@ class AdminForth implements AdminForthClass {
|
|
|
294
312
|
} else {
|
|
295
313
|
res.options.allowedActions = DEFAULT_ALLOWED_ACTIONS;
|
|
296
314
|
}
|
|
297
|
-
|
|
315
|
+
|
|
316
|
+
// transform all hooks Functions to array of functions
|
|
317
|
+
if (res.hooks) {
|
|
318
|
+
for (const value of [res.hooks.show, res.hooks.list]) {
|
|
319
|
+
if (value) {
|
|
320
|
+
if (value.beforeDatasourceRequest) {
|
|
321
|
+
if (!Array.isArray(value.beforeDatasourceRequest)) {
|
|
322
|
+
value.beforeDatasourceRequest = [value.beforeDatasourceRequest];
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (value.afterDatasourceResponse) {
|
|
326
|
+
if (!Array.isArray(value.afterDatasourceResponse)) {
|
|
327
|
+
value.afterDatasourceResponse = [value.afterDatasourceResponse];
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
for (const value of [res.hooks.create, res.hooks.edit, res.hooks.delete]) {
|
|
333
|
+
if (value) {
|
|
334
|
+
|
|
335
|
+
if (value.beforeSave) {
|
|
336
|
+
if (!Array.isArray(value.beforeSave)) {
|
|
337
|
+
value.beforeSave = [value.beforeSave];
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (value.afterSave) {
|
|
341
|
+
if (!Array.isArray(value.afterSave)) {
|
|
342
|
+
value.afterSave = [value.afterSave];
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
});
|
|
298
349
|
|
|
299
350
|
|
|
300
351
|
|
|
@@ -373,7 +424,6 @@ class AdminForth implements AdminForthClass {
|
|
|
373
424
|
for (const resource of this.config.resources) {
|
|
374
425
|
for (const column of resource.columns) {
|
|
375
426
|
if (column.components) {
|
|
376
|
-
console.log('🔧🔧🔧 Validating components for resource', column.components);
|
|
377
427
|
|
|
378
428
|
for (const [key, comp] of Object.entries(column.components as Record<string, AdminForthComponentDeclarationFull>)) {
|
|
379
429
|
let ignoreExistsCheck = false;
|
|
@@ -461,7 +511,24 @@ class AdminForth implements AdminForthClass {
|
|
|
461
511
|
this.codeInjector.bundleNow({ hotReload, verbose });
|
|
462
512
|
}
|
|
463
513
|
|
|
464
|
-
|
|
514
|
+
async getUserByPk(pk: string) {
|
|
515
|
+
const resource = this.config.resources.find((res) => res.resourceId === this.config.auth.resourceId);
|
|
516
|
+
if (!resource) {
|
|
517
|
+
throw new Error('No auth resource found');
|
|
518
|
+
}
|
|
519
|
+
const users = await this.connectors[resource.dataSource].getData({
|
|
520
|
+
resource,
|
|
521
|
+
filters: [
|
|
522
|
+
{ field: resource.columns.find((col) => col.primaryKey).name, operator: AdminForthFilterOperators.EQ, value: pk },
|
|
523
|
+
],
|
|
524
|
+
limit: 1,
|
|
525
|
+
offset: 0,
|
|
526
|
+
sort: [],
|
|
527
|
+
});
|
|
528
|
+
return users.data[0] || null;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
setupEndpoints(server: GenericHttpServer) {
|
|
465
532
|
server.endpoint({
|
|
466
533
|
noAuth: true,
|
|
467
534
|
method: 'POST',
|
|
@@ -507,7 +574,7 @@ class AdminForth implements AdminForthClass {
|
|
|
507
574
|
|
|
508
575
|
const passwordHash = userRecord[this.config.auth.passwordHashField];
|
|
509
576
|
console.log('User record', userRecord, passwordHash) // why does it has no hash?
|
|
510
|
-
const valid = await
|
|
577
|
+
const valid = await AdminForthAuth.verifyPassword(password, passwordHash);
|
|
511
578
|
if (valid) {
|
|
512
579
|
token = this.auth.issueJWT({
|
|
513
580
|
username, pk: userRecord[userResource.columns.find((col) => col.primaryKey).name]
|
|
@@ -522,6 +589,14 @@ class AdminForth implements AdminForthClass {
|
|
|
522
589
|
},
|
|
523
590
|
});
|
|
524
591
|
|
|
592
|
+
server.endpoint({
|
|
593
|
+
method: 'POST',
|
|
594
|
+
path: '/check_auth',
|
|
595
|
+
handler: async ({ adminUser }) => {
|
|
596
|
+
return { ok: true };
|
|
597
|
+
},
|
|
598
|
+
});
|
|
599
|
+
|
|
525
600
|
server.endpoint({
|
|
526
601
|
noAuth: true,
|
|
527
602
|
method: 'POST',
|
|
@@ -559,27 +634,14 @@ class AdminForth implements AdminForthClass {
|
|
|
559
634
|
method: 'GET',
|
|
560
635
|
path: '/get_base_config',
|
|
561
636
|
handler: async ({input, adminUser, cookies}) => {
|
|
562
|
-
const cookieParsed = this.auth.verify(cookies['adminforth_jwt']);
|
|
563
637
|
let username = ''
|
|
564
638
|
let userFullName = ''
|
|
565
|
-
if (
|
|
639
|
+
if (adminUser.isRoot) {
|
|
566
640
|
username = this.config.rootUser.username;
|
|
567
641
|
} else {
|
|
568
|
-
const
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
filters: [
|
|
572
|
-
{ field: userResource.columns.find((col) => col.primaryKey).name, operator: AdminForthFilterOperators.EQ, value: cookieParsed['pk'] },
|
|
573
|
-
],
|
|
574
|
-
limit: 1,
|
|
575
|
-
offset: 0,
|
|
576
|
-
sort: [],
|
|
577
|
-
});
|
|
578
|
-
if (!user.data.length) {
|
|
579
|
-
return { error: 'Unauthorized' };
|
|
580
|
-
}
|
|
581
|
-
username = user.data[0][this.config.auth.usernameField];
|
|
582
|
-
userFullName = user.data[0][this.config.auth.userFullNameField];
|
|
642
|
+
const dbUser = adminUser.dbUser;
|
|
643
|
+
username = dbUser[this.config.auth.usernameField];
|
|
644
|
+
userFullName =dbUser[this.config.auth.userFullNameField];
|
|
583
645
|
}
|
|
584
646
|
|
|
585
647
|
const userData = {
|
|
@@ -609,6 +671,8 @@ class AdminForth implements AdminForthClass {
|
|
|
609
671
|
},
|
|
610
672
|
});
|
|
611
673
|
|
|
674
|
+
|
|
675
|
+
|
|
612
676
|
server.endpoint({
|
|
613
677
|
method: 'POST',
|
|
614
678
|
path: '/get_resource',
|
|
@@ -647,7 +711,7 @@ class AdminForth implements AdminForthClass {
|
|
|
647
711
|
return { error: `Resource ${resourceId} not found` };
|
|
648
712
|
}
|
|
649
713
|
|
|
650
|
-
for (const hook of
|
|
714
|
+
for (const hook of listify(resource.hooks?.[source]?.beforeDatasourceRequest)) {
|
|
651
715
|
const resp = await hook({ resource, query: body, adminUser });
|
|
652
716
|
if (!resp || (!resp.ok && !resp.error)) {
|
|
653
717
|
throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `);
|
|
@@ -678,7 +742,7 @@ class AdminForth implements AdminForthClass {
|
|
|
678
742
|
// nonsense
|
|
679
743
|
return { data: [], total: 0 };
|
|
680
744
|
}
|
|
681
|
-
|
|
745
|
+
}
|
|
682
746
|
|
|
683
747
|
const data = await this.connectors[resource.dataSource].getData({
|
|
684
748
|
resource,
|
|
@@ -723,7 +787,7 @@ class AdminForth implements AdminForthClass {
|
|
|
723
787
|
})
|
|
724
788
|
);
|
|
725
789
|
|
|
726
|
-
for (const hook of
|
|
790
|
+
for (const hook of listify(resource.hooks?.[source]?.afterDatasourceResponse)) {
|
|
727
791
|
const resp = await hook({ resource, response: data.data, adminUser });
|
|
728
792
|
if (!resp || (!resp.ok && !resp.error)) {
|
|
729
793
|
throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `);
|
|
@@ -778,8 +842,8 @@ class AdminForth implements AdminForthClass {
|
|
|
778
842
|
const targetResourceId = columnConfig.foreignResource.resourceId;
|
|
779
843
|
const targetResource = this.config.resources.find((res) => res.resourceId == targetResourceId);
|
|
780
844
|
|
|
781
|
-
for (const hook of
|
|
782
|
-
const resp = await hook({ query: body, adminUser });
|
|
845
|
+
for (const hook of listify(columnConfig.foreignResource.hooks?.dropdownList?.beforeDatasourceRequest as BeforeDataSourceRequestFunction[])) {
|
|
846
|
+
const resp = await hook({ query: body, adminUser, resource: targetResource });
|
|
783
847
|
if (!resp || (!resp.ok && !resp.error)) {
|
|
784
848
|
throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `);
|
|
785
849
|
}
|
|
@@ -809,8 +873,8 @@ class AdminForth implements AdminForthClass {
|
|
|
809
873
|
items
|
|
810
874
|
};
|
|
811
875
|
|
|
812
|
-
for (const hook of
|
|
813
|
-
const resp = await hook({ response, adminUser });
|
|
876
|
+
for (const hook of listify(columnConfig.foreignResource.hooks?.dropdownList?.afterDatasourceResponse as AfterDataSourceResponseFunction[])) {
|
|
877
|
+
const resp = await hook({ response, adminUser, resource: targetResource });
|
|
814
878
|
if (!resp || (!resp.ok && !resp.error)) {
|
|
815
879
|
throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `);
|
|
816
880
|
}
|
|
@@ -866,7 +930,7 @@ class AdminForth implements AdminForthClass {
|
|
|
866
930
|
|
|
867
931
|
const record = body['record'];
|
|
868
932
|
// execute hook if needed
|
|
869
|
-
for (const hook of
|
|
933
|
+
for (const hook of listify(resource.hooks?.create?.beforeSave as BeforeSaveFunction[])) {
|
|
870
934
|
const resp = await hook({ resource, record, adminUser });
|
|
871
935
|
if (!resp || (!resp.ok && !resp.error)) {
|
|
872
936
|
throw new Error(`Hook beforeSave must return object with {ok: true} or { error: 'Error' } `);
|
|
@@ -912,7 +976,7 @@ class AdminForth implements AdminForthClass {
|
|
|
912
976
|
const connector = this.connectors[resource.dataSource];
|
|
913
977
|
await connector.createRecord({ resource, record });
|
|
914
978
|
// execute hook if needed
|
|
915
|
-
for (const hook of
|
|
979
|
+
for (const hook of listify(resource.hooks?.create?.afterSave as AfterSaveFunction[])) {
|
|
916
980
|
const resp = await hook({ resource, record, adminUser });
|
|
917
981
|
if (!resp || (!resp.ok && !resp.error)) {
|
|
918
982
|
throw new Error(`Hook afterSave must return object with {ok: true} or { error: 'Error' } `);
|
|
@@ -947,7 +1011,7 @@ class AdminForth implements AdminForthClass {
|
|
|
947
1011
|
const record = body['record'];
|
|
948
1012
|
|
|
949
1013
|
// execute hook if needed
|
|
950
|
-
for (const hook of
|
|
1014
|
+
for (const hook of listify(resource.hooks?.edit?.beforeSave as BeforeSaveFunction[])) {
|
|
951
1015
|
const resp = await hook({ resource, record, adminUser });
|
|
952
1016
|
if (!resp || (!resp.ok && !resp.error)) {
|
|
953
1017
|
throw new Error(`Hook beforeSave must return object with {ok: true} or { error: 'Error' } `);
|
|
@@ -978,7 +1042,7 @@ class AdminForth implements AdminForthClass {
|
|
|
978
1042
|
}
|
|
979
1043
|
|
|
980
1044
|
// execute hook if needed
|
|
981
|
-
for (const hook of
|
|
1045
|
+
for (const hook of listify(resource.hooks?.edit?.afterSave as AfterSaveFunction[])) {
|
|
982
1046
|
const resp = await hook({ resource, record, adminUser });
|
|
983
1047
|
if (!resp || (!resp.ok && !resp.error)) {
|
|
984
1048
|
throw new Error(`Hook afterSave must return object with {ok: true} or { error: 'Error' } `);
|
|
@@ -1011,7 +1075,7 @@ class AdminForth implements AdminForthClass {
|
|
|
1011
1075
|
}
|
|
1012
1076
|
|
|
1013
1077
|
// execute hook if needed
|
|
1014
|
-
for (const hook of
|
|
1078
|
+
for (const hook of listify(resource.hooks?.delete?.beforeSave as BeforeSaveFunction[])) {
|
|
1015
1079
|
const resp = await hook({ resource, record, adminUser });
|
|
1016
1080
|
if (!resp || (!resp.ok && !resp.error)) {
|
|
1017
1081
|
throw new Error(`Hook beforeSave must return object with {ok: true} or { error: 'Error' } `);
|
|
@@ -1026,7 +1090,7 @@ class AdminForth implements AdminForthClass {
|
|
|
1026
1090
|
await connector.deleteRecord({ resource, recordId: body['primaryKey']});
|
|
1027
1091
|
|
|
1028
1092
|
// execute hook if needed
|
|
1029
|
-
for (const hook of
|
|
1093
|
+
for (const hook of listify(resource.hooks?.delete?.afterSave as BeforeSaveFunction[])) {
|
|
1030
1094
|
const resp = await hook({ resource, record, adminUser });
|
|
1031
1095
|
if (!resp || (!resp.ok && !resp.error)) {
|
|
1032
1096
|
throw new Error(`Hook afterSave must return object with {ok: true} or { error: 'Error' } `);
|
package/modules/codeInjector.ts
CHANGED
|
@@ -282,7 +282,6 @@ class CodeInjector implements CodeInjectorType {
|
|
|
282
282
|
|
|
283
283
|
});
|
|
284
284
|
|
|
285
|
-
console.log('🔧 🔧 Injecting code into Vue sources...', customResourceComponents);
|
|
286
285
|
customResourceComponents.forEach((filePath) => {
|
|
287
286
|
const componentName = getComponentNameFromPath(filePath);
|
|
288
287
|
this.allComponentNames[filePath] = componentName;
|
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
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adminforth",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.15",
|
|
4
4
|
"description": "OpenSource Vue3 powered forth-generation admin panel",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"test": "echo \"Error: no test specified\" && exit 1",
|
|
9
9
|
"build": "tsc",
|
|
10
|
-
"
|
|
10
|
+
"prepareDist": "cp -r spa dist/spa && find plugins -type f ! -name '*.ts' -exec cp --parents {} dist/plugins \\;",
|
|
11
|
+
"rollout": "tsc && npm run prepareDist && npm version patch && npm publish && cd documentation && npm run build && npm run deploy",
|
|
11
12
|
"docs": "typedoc"
|
|
12
13
|
},
|
|
13
14
|
"author": "devforth.io",
|
|
@@ -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
|
+
}
|
|
@@ -208,6 +208,17 @@ async function getList() {
|
|
|
208
208
|
}
|
|
209
209
|
});
|
|
210
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
|
+
|
|
211
222
|
rows.value = data.data?.map(row => {
|
|
212
223
|
row._primaryKeyValue = row[listResource.value.columns.find(c => c.primaryKey).name];
|
|
213
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(localStorage.getItem('isAuthorized') === '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
|