cabloy 5.1.153 → 5.1.155
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/.cabloy-version +1 -1
- package/.claude/scheduled_tasks.lock +1 -0
- package/.claude/skills/cabloy-spec-generation/scripts/generate-implementation-charts.mjs +46 -11
- package/.claude/skills/cabloy-spec-generation/scripts/generate-implementation-charts.test.mjs +13 -0
- package/CHANGELOG.md +19 -0
- package/package.json +1 -1
- package/repo-docs/backend/menu-guide.md +3 -2
- package/test-results/.last-run.json +2 -4
- package/vona/env/.env +8 -0
- package/vona/packages-vona/vona/package.json +1 -1
- package/vona/src/suite/a-commerce/modules/commerce-siteadmin/package.json +1 -2
- package/vona/src/suite/a-commerce/modules/commerce-siteadmin/src/.metadata/index.ts +40 -65
- package/vona/src/suite/a-home/modules/home-base/src/.metadata/index.ts +78 -72
- package/vona/src/suite/a-home/modules/home-base/src/.metadata/locales.ts +18 -0
- package/vona/src/suite/a-home/modules/home-base/src/config/locale/en-us.ts +3 -0
- package/vona/src/suite/a-home/modules/home-base/src/config/locale/zh-cn.ts +3 -0
- package/vona/src/suite/a-home/modules/home-base/src/index.ts +1 -0
- package/vona/src/suite/a-home/modules/home-base/src/service/siteCatalog.ts +17 -10
- package/vona/src/suite/a-home/modules/home-user/src/bean/meta.version.ts +5 -0
- package/vona/src/suite/a-home/modules/home-user/src/config/config.ts +36 -5
- package/vona/src/suite/a-home/modules/home-user/src/config/roles.ts +7 -2
- package/vona/src/suite/a-home/modules/home-user/src/entity/role.ts +3 -0
- package/vona/src/suite/a-home/modules/home-user/src/types/env.ts +11 -0
- package/vona/src/suite/a-home/modules/home-user/src/types/index.ts +1 -0
- package/vona/src/suite/a-home/modules/home-user/test/config.test.ts +65 -0
- package/vona/src/suite/a-home/modules/home-user/test/role.test.ts +14 -6
- package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/package.json +1 -1
- package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/package.json +1 -1
- package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/.metadata/index.ts +17 -2
- package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/bean/event.resolveMenuVisibility.ts +19 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/lib/beanSsrSiteBase.ts +32 -16
- package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/lib/index.ts +2 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/lib/ssrMenuEligibility.ts +107 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/lib/ssrMenuVisibility.ts +91 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/service/ssr.ts +30 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/types/ssrMenu.ts +67 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/test/ssrMenuEligibility.test.ts +135 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/test/ssrMenuVisibility.test.ts +64 -0
- package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
- package/vona/src/suite-vendor/a-vona/modules/a-openapiutils/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-openapiutils/src/lib/const/index.ts +1 -0
- package/vona/src/suite-vendor/a-vona/modules/a-openapiutils/src/lib/const/site.ts +1 -0
- package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-user/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-user/src/types/role.ts +1 -0
- package/vona/src/suite-vendor/a-vona/package.json +1 -1
- package/zova/pnpm-lock.yaml +1 -1
- package/zova/src/suite/a-home/modules/home-api/mock/passport.fake.ts +6 -6
- package/zova/src/suite/a-home/modules/home-base/src/lib/index.ts +1 -0
- package/zova/src/suite/a-home/modules/home-base/src/lib/roleSiteAdmission.ts +15 -0
- package/zova/src/suite/a-home/modules/home-base/src/service/routerGuards.ts +3 -3
- package/zova/src/suite/a-home/modules/home-layoutadmin/src/model/menu.ts +4 -3
- package/zova/src/suite/a-home/modules/home-layoutweb/src/model/menu.ts +4 -3
- package/zova/src/suite/a-home/modules/home-passport/package.json +2 -1
- package/zova/src/suite/a-home/modules/home-passport/src/model/passport.ts +2 -2
- package/zova/src/suite/a-home/modules/home-passport/test/lib/roleSiteAdmission.test.ts +23 -0
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/lib/resourceRouteMeta.ts +9 -0
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/routes.ts +4 -10
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/test/lib/routes.test.ts +37 -0
- package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
- package/test-results/cabloy-basic-ATP-BASIC-SUM-58d8a-ry-states-and-Markdown-HTML/error-context.md +0 -195
- package/vona/src/suite/a-commerce/modules/commerce-siteadmin/src/bean/meta.version.ts +0 -32
- package/vona/src/suite/a-commerce/modules/commerce-siteadmin/test/meta.version.test.ts +0 -44
|
@@ -18,11 +18,78 @@ export interface ISsrMenuItem<Pages extends {} = {}, Icons extends {} = {}> exte
|
|
|
18
18
|
|
|
19
19
|
export type ISsrMenuItemPrepared = IMenuItem<any, any> & Pick<ISsrMenuItem<any, any>, 'roles'>;
|
|
20
20
|
|
|
21
|
+
export interface ISsrMenuEligibility {
|
|
22
|
+
ssrSiteName: string;
|
|
23
|
+
ssrMenuName: string;
|
|
24
|
+
rolesDefined: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ISsrMenuCatalogSiteInfo {
|
|
28
|
+
ssrSiteName: string;
|
|
29
|
+
title: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ISsrMenuCatalogMenu {
|
|
33
|
+
ssrSiteName: string;
|
|
34
|
+
ssrMenuName: string;
|
|
35
|
+
onionName: string;
|
|
36
|
+
roles?: (keyof IRoleNameRecord)[];
|
|
37
|
+
title?: IMenuItem['title'];
|
|
38
|
+
description?: IMenuItem['description'];
|
|
39
|
+
icon?: IMenuItem['icon'];
|
|
40
|
+
order?: IMenuItem['order'];
|
|
41
|
+
group?: IMenuItem['group'];
|
|
42
|
+
separator?: IMenuItem['separator'];
|
|
43
|
+
link?: IMenuItem['link'];
|
|
44
|
+
external?: IMenuItem['external'];
|
|
45
|
+
target?: IMenuItem['target'];
|
|
46
|
+
meta?: IMenuItem['meta'];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ISsrMenuCatalogGroup {
|
|
50
|
+
ssrSiteName: string;
|
|
51
|
+
ssrMenuGroupName: string;
|
|
52
|
+
onionName: string;
|
|
53
|
+
title?: IMenuGroup['title'];
|
|
54
|
+
description?: IMenuGroup['description'];
|
|
55
|
+
icon?: IMenuGroup['icon'];
|
|
56
|
+
order?: IMenuGroup['order'];
|
|
57
|
+
group?: IMenuGroup['group'];
|
|
58
|
+
collapsed?: IMenuGroup['collapsed'];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface ISsrMenuCatalog {
|
|
62
|
+
sites: ISsrMenuCatalogSiteInfo[];
|
|
63
|
+
menus: ISsrMenuCatalogMenu[];
|
|
64
|
+
groups: ISsrMenuCatalogGroup[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface ISsrMenuCatalogPresentationMenu extends IMenuItem<any, any> {
|
|
68
|
+
ssrSiteName: string;
|
|
69
|
+
ssrMenuName: string;
|
|
70
|
+
configurable: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface ISsrMenuCatalogPresentationGroup extends IMenuGroup<any> {
|
|
74
|
+
ssrSiteName: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface ISsrMenuCatalogPresentation {
|
|
78
|
+
menus: ISsrMenuCatalogPresentationMenu[];
|
|
79
|
+
groups: ISsrMenuCatalogPresentationGroup[];
|
|
80
|
+
}
|
|
81
|
+
|
|
21
82
|
export interface ISsrMenusPrepared {
|
|
22
83
|
menus?: ISsrMenuItemPrepared[];
|
|
23
84
|
groups?: IMenuGroup[];
|
|
24
85
|
}
|
|
25
86
|
|
|
87
|
+
export interface ISsrMenuCatalogSite {
|
|
88
|
+
ssrSiteName: string;
|
|
89
|
+
menus: ISsrMenuItemPrepared[];
|
|
90
|
+
groups: IMenuGroup[];
|
|
91
|
+
}
|
|
92
|
+
|
|
26
93
|
// should not set default generic = IDecoratorSsrSiteOptions
|
|
27
94
|
export interface IDecoratorSsrMenuOptions<
|
|
28
95
|
SsrSiteOptions extends IDecoratorSsrSiteOptions,
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
checkSsrBinding,
|
|
6
|
+
resolveSsrMenuCatalog,
|
|
7
|
+
resolveSsrMenuEligibility,
|
|
8
|
+
} from '../src/lib/ssrMenuEligibility.ts';
|
|
9
|
+
|
|
10
|
+
function createMenus() {
|
|
11
|
+
return [
|
|
12
|
+
{
|
|
13
|
+
name: 'test:omitted',
|
|
14
|
+
beanOptions: {
|
|
15
|
+
options: {
|
|
16
|
+
item: { roles: [] },
|
|
17
|
+
locale: ['en-us'],
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: 'test:keyed',
|
|
23
|
+
beanOptions: {
|
|
24
|
+
options: {
|
|
25
|
+
site: ['test:admin', 'test:web'],
|
|
26
|
+
items: {
|
|
27
|
+
static: { roles: ['systemAdmin'] },
|
|
28
|
+
public: {},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'test:malformed',
|
|
35
|
+
beanOptions: {},
|
|
36
|
+
},
|
|
37
|
+
] as any;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe('ssrMenuEligibility.test.ts', () => {
|
|
41
|
+
it('matches unset, scalar, or array bindings', () => {
|
|
42
|
+
assert.equal(checkSsrBinding('test:admin', undefined), true);
|
|
43
|
+
assert.equal(checkSsrBinding('test:admin', null), true);
|
|
44
|
+
assert.equal(checkSsrBinding('test:admin', ''), true);
|
|
45
|
+
assert.equal(checkSsrBinding('test:admin', 'test:admin'), true);
|
|
46
|
+
assert.equal(checkSsrBinding('test:web', 'test:admin'), false);
|
|
47
|
+
assert.equal(checkSsrBinding('test:web', ['test:admin', 'test:web']), true);
|
|
48
|
+
assert.equal(checkSsrBinding('en-us', ['zh-cn', 'en-us']), true);
|
|
49
|
+
assert.equal(checkSsrBinding('en-us', []), false);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('derives every enabled-site partition with site and onion identities', () => {
|
|
53
|
+
const catalog = resolveSsrMenuCatalog(
|
|
54
|
+
[
|
|
55
|
+
{ ssrSiteName: 'test:admin', title: 'Admin' },
|
|
56
|
+
{ ssrSiteName: 'test:web', title: 'Web' },
|
|
57
|
+
],
|
|
58
|
+
createMenus(),
|
|
59
|
+
);
|
|
60
|
+
assert.deepEqual(catalog.sites, [
|
|
61
|
+
{ ssrSiteName: 'test:admin', title: 'Admin' },
|
|
62
|
+
{ ssrSiteName: 'test:web', title: 'Web' },
|
|
63
|
+
]);
|
|
64
|
+
assert.deepEqual(catalog.menus, [
|
|
65
|
+
{
|
|
66
|
+
ssrSiteName: 'test:admin',
|
|
67
|
+
ssrMenuName: 'test:omitted',
|
|
68
|
+
onionName: 'test:omitted',
|
|
69
|
+
roles: [],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
ssrSiteName: 'test:admin',
|
|
73
|
+
ssrMenuName: 'test:keyed#static',
|
|
74
|
+
onionName: 'test:keyed',
|
|
75
|
+
roles: ['systemAdmin'],
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
ssrSiteName: 'test:admin',
|
|
79
|
+
ssrMenuName: 'test:keyed#public',
|
|
80
|
+
onionName: 'test:keyed',
|
|
81
|
+
roles: undefined,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
ssrSiteName: 'test:web',
|
|
85
|
+
ssrMenuName: 'test:omitted',
|
|
86
|
+
onionName: 'test:omitted',
|
|
87
|
+
roles: [],
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
ssrSiteName: 'test:web',
|
|
91
|
+
ssrMenuName: 'test:keyed#static',
|
|
92
|
+
onionName: 'test:keyed',
|
|
93
|
+
roles: ['systemAdmin'],
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
ssrSiteName: 'test:web',
|
|
97
|
+
ssrMenuName: 'test:keyed#public',
|
|
98
|
+
onionName: 'test:keyed',
|
|
99
|
+
roles: undefined,
|
|
100
|
+
},
|
|
101
|
+
]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('resolves final leaves independently of locale', () => {
|
|
105
|
+
const menus = createMenus();
|
|
106
|
+
assert.deepEqual(resolveSsrMenuEligibility('test:admin', 'test:omitted', menus), {
|
|
107
|
+
ssrSiteName: 'test:admin',
|
|
108
|
+
ssrMenuName: 'test:omitted',
|
|
109
|
+
rolesDefined: true,
|
|
110
|
+
});
|
|
111
|
+
assert.deepEqual(resolveSsrMenuEligibility('test:web', 'test:omitted', menus), {
|
|
112
|
+
ssrSiteName: 'test:web',
|
|
113
|
+
ssrMenuName: 'test:omitted',
|
|
114
|
+
rolesDefined: true,
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('distinguishes dynamic/static leaves from public leaves and exact keyed names', () => {
|
|
119
|
+
const menus = createMenus();
|
|
120
|
+
assert.deepEqual(resolveSsrMenuEligibility('test:admin', 'test:keyed#static', menus), {
|
|
121
|
+
ssrSiteName: 'test:admin',
|
|
122
|
+
ssrMenuName: 'test:keyed#static',
|
|
123
|
+
rolesDefined: true,
|
|
124
|
+
});
|
|
125
|
+
assert.deepEqual(resolveSsrMenuEligibility('test:web', 'test:keyed#public', menus), {
|
|
126
|
+
ssrSiteName: 'test:web',
|
|
127
|
+
ssrMenuName: 'test:keyed#public',
|
|
128
|
+
rolesDefined: false,
|
|
129
|
+
});
|
|
130
|
+
assert.equal(resolveSsrMenuEligibility('test:admin', 'test:keyed', menus), undefined);
|
|
131
|
+
assert.equal(resolveSsrMenuEligibility('test:other', 'test:keyed#static', menus), undefined);
|
|
132
|
+
assert.equal(resolveSsrMenuEligibility('test:admin', 'test:missing', menus), undefined);
|
|
133
|
+
assert.equal(resolveSsrMenuEligibility('test:admin', 'test:malformed', menus), undefined);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
projectPublicSsrMenuGroups,
|
|
6
|
+
projectPublicSsrMenus,
|
|
7
|
+
resolveSsrMenuVisibilityDefault,
|
|
8
|
+
resolveVisibleSsrMenuGroups,
|
|
9
|
+
} from '../src/lib/ssrMenuVisibility.ts';
|
|
10
|
+
|
|
11
|
+
function createMenus() {
|
|
12
|
+
return [
|
|
13
|
+
{ name: 'test:public', title: 'Public', group: 'root' },
|
|
14
|
+
{ name: 'test:dynamic', title: 'Dynamic', roles: [], group: 'root' },
|
|
15
|
+
{ name: 'test:static', title: 'Static', roles: ['member'] },
|
|
16
|
+
] as any;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('ssrMenuVisibility.test.ts', () => {
|
|
20
|
+
it('distinguishes public, dynamic-only, and static leaves', () => {
|
|
21
|
+
const menus = createMenus();
|
|
22
|
+
assert.deepEqual(
|
|
23
|
+
resolveSsrMenuVisibilityDefault(menus, roles => roles.includes('member')),
|
|
24
|
+
[menus[0], menus[2]],
|
|
25
|
+
);
|
|
26
|
+
assert.deepEqual(
|
|
27
|
+
resolveSsrMenuVisibilityDefault(menus, () => false),
|
|
28
|
+
[menus[0]],
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('retains only groups that contain a visible descendant', () => {
|
|
33
|
+
const groups = [
|
|
34
|
+
{ name: 'root', title: 'Root' },
|
|
35
|
+
{ name: 'nested', title: 'Nested', group: 'root' },
|
|
36
|
+
{ name: 'empty', title: 'Empty' },
|
|
37
|
+
];
|
|
38
|
+
const menus = [{ name: 'test:visible', group: 'nested' }] as any;
|
|
39
|
+
assert.deepEqual(resolveVisibleSsrMenuGroups(menus, groups), [groups[0], groups[1]]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('projects only public menu and group fields', () => {
|
|
43
|
+
const menus = [
|
|
44
|
+
{
|
|
45
|
+
name: 'test:private',
|
|
46
|
+
title: 'Private',
|
|
47
|
+
roles: ['member'],
|
|
48
|
+
policyRevision: 'private',
|
|
49
|
+
associationState: 'private',
|
|
50
|
+
},
|
|
51
|
+
] as any;
|
|
52
|
+
const groups = [{ name: 'root', title: 'Root', catalogState: 'private' }] as any;
|
|
53
|
+
const result = {
|
|
54
|
+
menus: projectPublicSsrMenus(menus),
|
|
55
|
+
groups: projectPublicSsrMenuGroups(groups),
|
|
56
|
+
};
|
|
57
|
+
assert.deepEqual(result.menus, [{ name: 'test:private', title: 'Private' }]);
|
|
58
|
+
assert.deepEqual(result.groups, [{ name: 'root', title: 'Root' }]);
|
|
59
|
+
const serialized = JSON.stringify(result);
|
|
60
|
+
for (const forbidden of ['roles', 'policyRevision', 'associationState', 'catalogState']) {
|
|
61
|
+
assert.equal(serialized.includes(forbidden), false, forbidden);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vona-suite-a-cabloy",
|
|
3
|
-
"version": "5.1.
|
|
3
|
+
"version": "5.1.48",
|
|
4
4
|
"gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
|
|
5
5
|
"description": "",
|
|
6
6
|
"author": "",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"dependencies": {
|
|
10
10
|
"vona-module-a-cabloy": "workspace:^",
|
|
11
11
|
"vona-module-a-markdown": "^5.0.3",
|
|
12
|
-
"vona-module-a-rbac": "^5.0.
|
|
12
|
+
"vona-module-a-rbac": "^5.0.2",
|
|
13
13
|
"vona-module-a-socket": "workspace:^",
|
|
14
14
|
"vona-module-a-ssr": "workspace:^",
|
|
15
15
|
"vona-module-a-ssrhmr": "workspace:^"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const roleSiteIdAll = '*';
|
package/zova/pnpm-lock.yaml
CHANGED
|
@@ -416,14 +416,14 @@ export default defineFakeRoute([
|
|
|
416
416
|
name: 'registeredUser',
|
|
417
417
|
title: 'Registered User',
|
|
418
418
|
titleLocales: { 'zh-cn': '注册用户' },
|
|
419
|
-
siteIds: ['
|
|
419
|
+
siteIds: ['*'],
|
|
420
420
|
},
|
|
421
421
|
{
|
|
422
422
|
id: 2,
|
|
423
423
|
name: 'systemAdmin',
|
|
424
424
|
title: 'System Administrator',
|
|
425
425
|
titleLocales: { 'zh-cn': '系统管理员' },
|
|
426
|
-
siteIds: ['
|
|
426
|
+
siteIds: ['*'],
|
|
427
427
|
},
|
|
428
428
|
],
|
|
429
429
|
},
|
|
@@ -461,15 +461,15 @@ export default defineFakeRoute([
|
|
|
461
461
|
id: 1,
|
|
462
462
|
name: 'registeredUser',
|
|
463
463
|
title: 'Registered User',
|
|
464
|
-
|
|
465
|
-
siteIds: ['
|
|
464
|
+
titleLocales: { 'zh-cn': '注册用户' },
|
|
465
|
+
siteIds: ['*'],
|
|
466
466
|
},
|
|
467
467
|
{
|
|
468
468
|
id: 2,
|
|
469
469
|
name: 'systemAdmin',
|
|
470
470
|
title: 'System Administrator',
|
|
471
|
-
|
|
472
|
-
siteIds: ['
|
|
471
|
+
titleLocales: { 'zh-cn': '系统管理员' },
|
|
472
|
+
siteIds: ['*'],
|
|
473
473
|
},
|
|
474
474
|
],
|
|
475
475
|
},
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const roleSiteIdAll = '*';
|
|
2
|
+
|
|
3
|
+
export interface RoleSiteAdmission {
|
|
4
|
+
siteIds: string[];
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function isRoleSiteAdmitted(
|
|
8
|
+
siteId: string | undefined,
|
|
9
|
+
roles: RoleSiteAdmission[] | undefined,
|
|
10
|
+
): boolean {
|
|
11
|
+
if (!siteId) return false;
|
|
12
|
+
return !!roles?.some(
|
|
13
|
+
role => role.siteIds.includes(siteId) || role.siteIds.includes(roleSiteIdAll),
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -4,6 +4,8 @@ import { catchError } from '@cabloy/utils';
|
|
|
4
4
|
import { Service } from 'zova-module-a-bean';
|
|
5
5
|
import { BeanRouterGuardsBase } from 'zova-module-a-router';
|
|
6
6
|
|
|
7
|
+
import { isRoleSiteAdmitted } from '../lib/roleSiteAdmission.js';
|
|
8
|
+
|
|
7
9
|
@Service()
|
|
8
10
|
export class ServiceRouterGuards extends BeanRouterGuardsBase {
|
|
9
11
|
protected onRouterGuards(router: BeanRouter) {
|
|
@@ -31,9 +33,7 @@ export class ServiceRouterGuards extends BeanRouterGuardsBase {
|
|
|
31
33
|
}
|
|
32
34
|
return pagePath;
|
|
33
35
|
}
|
|
34
|
-
const
|
|
35
|
-
const siteAdmitted =
|
|
36
|
-
!!siteId && !!this.$passport.roles?.some(role => role.siteIds.includes(siteId));
|
|
36
|
+
const siteAdmitted = isRoleSiteAdmitted(this.sys.env.SITE_ID, this.$passport.roles);
|
|
37
37
|
if (!siteAdmitted) {
|
|
38
38
|
const pagePath = this.app.$getPagePathAccessDenied();
|
|
39
39
|
if (process.env.SERVER) {
|
|
@@ -28,7 +28,7 @@ export class ModelMenu extends BeanModelBase {
|
|
|
28
28
|
// event
|
|
29
29
|
if (process.env.CLIENT && this.sys.config.ssr.hmr) {
|
|
30
30
|
this._eventSsrHmrReload = this.sys.meta.event.on('a-ssrhmr:reload', async (_data, next) => {
|
|
31
|
-
await this.
|
|
31
|
+
await this.refreshMenus();
|
|
32
32
|
return next();
|
|
33
33
|
});
|
|
34
34
|
}
|
|
@@ -78,8 +78,9 @@ export class ModelMenu extends BeanModelBase {
|
|
|
78
78
|
});
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
async refreshMenus(): Promise<void> {
|
|
82
|
+
const queryMenus = this.retrieveMenus();
|
|
83
|
+
await queryMenus.refetch({ bypassPersister: true });
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
private _getQueryKeyRetrieveMenus(): [string, string | undefined, string | undefined] {
|
|
@@ -28,7 +28,7 @@ export class ModelMenu extends BeanModelBase {
|
|
|
28
28
|
// event
|
|
29
29
|
if (process.env.CLIENT && this.sys.config.ssr.hmr) {
|
|
30
30
|
this._eventSsrHmrReload = this.sys.meta.event.on('a-ssrhmr:reload', async (_data, next) => {
|
|
31
|
-
await this.
|
|
31
|
+
await this.refreshMenus();
|
|
32
32
|
return next();
|
|
33
33
|
});
|
|
34
34
|
}
|
|
@@ -78,8 +78,9 @@ export class ModelMenu extends BeanModelBase {
|
|
|
78
78
|
});
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
async refreshMenus(): Promise<void> {
|
|
82
|
+
const queryMenus = this.retrieveMenus();
|
|
83
|
+
await queryMenus.refetch({ bypassPersister: true });
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
private _getQueryKeyRetrieveMenus(): [string, string | undefined, string | undefined] {
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
TypeOpenapiPermissions,
|
|
18
18
|
} from 'zova-module-a-openapi';
|
|
19
19
|
import { ApiApiHomeUserPassportloginOauthPath, OpenApiBaseURL } from 'zova-module-home-api';
|
|
20
|
+
import { isRoleSiteAdmitted } from 'zova-module-home-base';
|
|
20
21
|
|
|
21
22
|
import { matchPermissionAction } from '../lib/permissionActionMatcher.js';
|
|
22
23
|
|
|
@@ -143,8 +144,7 @@ export class ModelPassport extends BeanModelBase {
|
|
|
143
144
|
}
|
|
144
145
|
|
|
145
146
|
isPassportSiteAdmitted(passport?: ApiApiHomeUserPassportloginResponseBody['passport']) {
|
|
146
|
-
|
|
147
|
-
return !!siteId && !!passport?.roles.some(role => role.siteIds.includes(siteId));
|
|
147
|
+
return isRoleSiteAdmitted(this.sys.env.SITE_ID, passport?.roles);
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
logout() {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { isRoleSiteAdmitted } from 'zova-module-home-base';
|
|
4
|
+
|
|
5
|
+
test('admits an exact site match', () => {
|
|
6
|
+
assert.equal(isRoleSiteAdmitted('web', [{ siteIds: ['web'] }]), true);
|
|
7
|
+
assert.equal(isRoleSiteAdmitted('admin', [{ siteIds: ['web'] }]), false);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test('admits every non-empty site through the wildcard', () => {
|
|
11
|
+
assert.equal(isRoleSiteAdmitted('web', [{ siteIds: ['*'] }]), true);
|
|
12
|
+
assert.equal(isRoleSiteAdmitted('admin', [{ siteIds: ['*'] }]), true);
|
|
13
|
+
assert.equal(isRoleSiteAdmitted(undefined, [{ siteIds: ['*'] }]), false);
|
|
14
|
+
assert.equal(isRoleSiteAdmitted('', [{ siteIds: ['*'] }]), false);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('combines site admission across roles', () => {
|
|
18
|
+
assert.equal(isRoleSiteAdmitted('admin', [{ siteIds: ['web'] }, { siteIds: ['admin'] }]), true);
|
|
19
|
+
assert.equal(
|
|
20
|
+
isRoleSiteAdmitted('web', [{ siteIds: ['storefront'] }, { siteIds: ['admin'] }]),
|
|
21
|
+
false,
|
|
22
|
+
);
|
|
23
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const resourceRouteMeta = {
|
|
2
|
+
tabKey: resourceTabKey,
|
|
3
|
+
componentKey: resourceTabKey,
|
|
4
|
+
ssrProfile: 'session' as const,
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export function resourceTabKey(route: { params: Record<string, string | string[]> }) {
|
|
8
|
+
return `/rest/resource/${encodeURIComponent(route.params.resource as string)}`;
|
|
9
|
+
}
|
|
@@ -3,23 +3,21 @@ import type { IModuleRoute } from 'zova-module-a-router';
|
|
|
3
3
|
import { ZPageEntry } from './.metadata/page/entry.js';
|
|
4
4
|
import { ZPageEntryCreate } from './.metadata/page/entryCreate.js';
|
|
5
5
|
import { ZPageResource } from './.metadata/page/resource.js';
|
|
6
|
+
import { resourceRouteMeta, resourceTabKey } from './lib/resourceRouteMeta.js';
|
|
6
7
|
|
|
7
8
|
export const routes: IModuleRoute[] = [
|
|
8
9
|
{
|
|
9
10
|
name: 'resource',
|
|
10
11
|
path: ':resource',
|
|
11
12
|
component: ZPageResource,
|
|
12
|
-
meta:
|
|
13
|
-
tabKey,
|
|
14
|
-
ssrProfile: 'session',
|
|
15
|
-
},
|
|
13
|
+
meta: resourceRouteMeta,
|
|
16
14
|
},
|
|
17
15
|
{
|
|
18
16
|
name: 'entryCreate',
|
|
19
17
|
path: ':resource/create',
|
|
20
18
|
component: ZPageEntryCreate,
|
|
21
19
|
meta: {
|
|
22
|
-
tabKey,
|
|
20
|
+
tabKey: resourceTabKey,
|
|
23
21
|
ssrProfile: 'session',
|
|
24
22
|
},
|
|
25
23
|
},
|
|
@@ -28,12 +26,8 @@ export const routes: IModuleRoute[] = [
|
|
|
28
26
|
path: ':resource/:id/:formScene?',
|
|
29
27
|
component: ZPageEntry,
|
|
30
28
|
meta: {
|
|
31
|
-
tabKey,
|
|
29
|
+
tabKey: resourceTabKey,
|
|
32
30
|
ssrProfile: 'session',
|
|
33
31
|
},
|
|
34
32
|
},
|
|
35
33
|
];
|
|
36
|
-
|
|
37
|
-
function tabKey(route) {
|
|
38
|
-
return `/rest/resource/${encodeURIComponent(route.params.resource)}`;
|
|
39
|
-
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
|
|
4
|
+
import { resourceRouteMeta } from '../../src/lib/resourceRouteMeta.ts';
|
|
5
|
+
|
|
6
|
+
function getResourceKey(url: string) {
|
|
7
|
+
const resource = decodeURIComponent(url.substring('/rest/resource/'.length));
|
|
8
|
+
const route = { params: { resource } };
|
|
9
|
+
return {
|
|
10
|
+
resource,
|
|
11
|
+
componentKey: resourceRouteMeta.componentKey(route),
|
|
12
|
+
tabKey: resourceRouteMeta.tabKey(route),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
test('resource root canonicalizes colon URL spellings to one component key', () => {
|
|
17
|
+
const expectedKey = '/rest/resource/admin-user%3Auser';
|
|
18
|
+
|
|
19
|
+
for (const url of [
|
|
20
|
+
'/rest/resource/admin-user:user',
|
|
21
|
+
'/rest/resource/admin-user%3Auser',
|
|
22
|
+
'/rest/resource/admin-user%3auser',
|
|
23
|
+
]) {
|
|
24
|
+
const actual = getResourceKey(url);
|
|
25
|
+
assert.equal(actual.resource, 'admin-user:user');
|
|
26
|
+
assert.equal(actual.componentKey, expectedKey);
|
|
27
|
+
assert.equal(actual.tabKey, expectedKey);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('resource root keeps distinct resource workspaces separate', () => {
|
|
32
|
+
const user = getResourceKey('/rest/resource/admin-user:user');
|
|
33
|
+
const role = getResourceKey('/rest/resource/admin-user:role');
|
|
34
|
+
|
|
35
|
+
assert.notEqual(user.componentKey, role.componentKey);
|
|
36
|
+
assert.notEqual(user.tabKey, role.tabKey);
|
|
37
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zova-suite-a-cabloy",
|
|
3
|
-
"version": "5.1.
|
|
3
|
+
"version": "5.1.50",
|
|
4
4
|
"gitHead": "4dece229b6d3f4735930e833afcb4b95ec51bf86",
|
|
5
5
|
"description": "",
|
|
6
6
|
"author": "",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"type": "module",
|
|
9
9
|
"dependencies": {
|
|
10
10
|
"zova-module-a-rbac": "^5.0.1",
|
|
11
|
-
"zova-module-rest-resource": "^5.1.
|
|
11
|
+
"zova-module-rest-resource": "^5.1.49"
|
|
12
12
|
},
|
|
13
13
|
"title": "a-cabloy"
|
|
14
14
|
}
|