cabloy 5.1.103 → 5.1.105

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.
Files changed (78) hide show
  1. package/.cabloy-version +1 -1
  2. package/.claude/skills/cabloy-backend-scaffold/references/follow-up-checklist.md +1 -1
  3. package/CHANGELOG.md +27 -0
  4. package/CLAUDE.md +1 -0
  5. package/cabloy-docs/backend/controller-aop-guide.md +2 -2
  6. package/cabloy-docs/backend/field-indexes.md +10 -1
  7. package/cabloy-docs/backend/menu-guide.md +30 -0
  8. package/cabloy-docs/frontend/a-model-under-the-hood.md +26 -4
  9. package/cabloy-docs/frontend/model-architecture.md +1 -1
  10. package/cabloy-docs/frontend/model-state-guide.md +122 -16
  11. package/cabloy-docs/frontend/ssr-init-data.md +12 -1
  12. package/cabloy-docs/frontend/use-state-data-best-practices.md +23 -0
  13. package/package.json +1 -1
  14. package/vona/packages-vona/vona/package.json +1 -1
  15. package/vona/pnpm-lock.yaml +31 -31
  16. package/vona/src/suite/a-home/modules/home-user/src/bean/eventListener.activate.ts +5 -4
  17. package/vona/src/suite/a-home/modules/home-user/src/bean/meta.version.ts +10 -6
  18. package/vona/src/suite/a-home/modules/home-user/src/config/config.ts +1 -1
  19. package/vona/src/suite/a-home/modules/home-user/src/controller/passport.ts +7 -7
  20. package/vona/src/suite/a-home/modules/home-user/src/entity/role.ts +4 -0
  21. package/vona/src/suite/a-home/modules/home-user/src/service/passportAdapter.ts +2 -2
  22. package/vona/src/suite/a-home/modules/home-user/src/service/roleAdapter.ts +5 -5
  23. package/vona/src/suite/a-training/modules/training-record/src/bean/ssrMenu.record.ts +1 -0
  24. package/vona/src/suite/a-training/modules/training-student/src/bean/ssrMenu.student.ts +1 -0
  25. package/vona/src/suite/cabloy-basic/modules/basic-siteadmin/src/bean/ssrMenu.home.ts +1 -0
  26. package/vona/src/suite/cabloy-basic/modules/basic-siteadmin/src/bean/ssrSite.admin.ts +5 -1
  27. package/vona/src/suite/cabloy-basic/modules/basic-siteweb/src/bean/ssrSite.web.ts +5 -1
  28. package/vona/src/suite-vendor/a-auth/modules/a-auth/package.json +1 -1
  29. package/vona/src/suite-vendor/a-auth/modules/a-auth/src/service/auth.ts +5 -1
  30. package/vona/src/suite-vendor/a-auth/package.json +1 -1
  31. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/cli/ssrSite/boilerplate/{{sceneName}}.{{beanName}}.ts_ +5 -3
  32. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/package.json +1 -1
  33. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/bean/event.retrieveMenusSite.ts +2 -2
  34. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/lib/beanSsrSiteBase.ts +33 -8
  35. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/service/ssr.ts +46 -16
  36. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/service/ssrHandler.ts +2 -4
  37. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/types/env.ts +1 -0
  38. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/types/ssrMenu.ts +10 -1
  39. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/types/ssrSite.ts +3 -1
  40. package/vona/src/suite-vendor/a-cabloy/package.json +1 -1
  41. package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
  42. package/vona/src/suite-vendor/a-vona/modules/a-permission/src/.metadata/index.ts +18 -2
  43. package/vona/src/suite-vendor/a-vona/modules/a-permission/src/bean/bean.permission.ts +6 -1
  44. package/vona/src/suite-vendor/a-vona/modules/a-permission/src/bean/event.retrievePermissionAction.ts +16 -0
  45. package/vona/src/suite-vendor/a-vona/modules/a-user/package.json +1 -1
  46. package/vona/src/suite-vendor/a-vona/modules/a-user/src/bean/bean.passport.ts +2 -2
  47. package/vona/src/suite-vendor/a-vona/modules/a-user/src/lib/passport.ts +7 -4
  48. package/vona/src/suite-vendor/a-vona/modules/a-user/src/types/passport.ts +1 -1
  49. package/vona/src/suite-vendor/a-vona/modules/a-user/src/types/role.ts +3 -1
  50. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  51. package/zova/env/.env.cabloyBasicAdmin +2 -0
  52. package/zova/env/.env.cabloyBasicWeb +2 -0
  53. package/zova/packages-utils/zova-jsx/package.json +2 -2
  54. package/zova/packages-zova/zova/package.json +3 -3
  55. package/zova/packages-zova/zova-core/package.json +1 -1
  56. package/zova/packages-zova/zova-core/src/core/sys/sys.ts +3 -0
  57. package/zova/packages-zova/zova-core/src/types/utils/env.ts +1 -0
  58. package/zova/src/suite/a-home/modules/home-api/src/api/homeUserPassport.ts +19 -19
  59. package/zova/src/suite/a-home/modules/home-api/src/api/openapi/types.ts +11 -10
  60. package/zova/src/suite/a-home/modules/home-api/src/apiSchema/homeUserPassport.ts +5 -5
  61. package/zova/src/suite/a-home/modules/home-base/src/.metadata/page/errorAccessDenied.ts +9 -0
  62. package/zova/src/suite/a-home/modules/home-base/src/page/errorAccessDenied/controller.tsx +40 -0
  63. package/zova/src/suite/a-home/modules/home-base/src/routes.ts +9 -0
  64. package/zova/src/suite/a-home/modules/home-base/src/service/routerGuards.ts +21 -12
  65. package/zova/src/suite/a-home/modules/home-passport/src/model/passport.ts +7 -1
  66. package/zova/src/suite/a-home/modules/home-passport/src/monkey.ts +17 -3
  67. package/zova/src/suite-vendor/a-zova/modules/a-model/package.json +1 -1
  68. package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.useState.ts +6 -6
  69. package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.useStateGeneral.ts +5 -5
  70. package/zova/src/suite-vendor/a-zova/modules/a-model/src/monkey.ts +11 -2
  71. package/zova/src/suite-vendor/a-zova/modules/a-model/src/types/query.ts +1 -1
  72. package/zova/src/suite-vendor/a-zova/modules/a-router/package.json +1 -1
  73. package/zova/src/suite-vendor/a-zova/modules/a-router/src/monkeySys.ts +10 -1
  74. package/zova/src/suite-vendor/a-zova/modules/a-router/src/types/router.ts +1 -0
  75. package/zova/src/suite-vendor/a-zova/modules/a-routertabs/package.json +1 -1
  76. package/zova/src/suite-vendor/a-zova/modules/a-routertabs/src/model/tabs.ts +4 -4
  77. package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +2 -2
  78. package/zova/src/suite-vendor/a-zova/package.json +5 -5
@@ -17,10 +17,11 @@ export class EventListenerActivate
17
17
  next: NextEvent<TypeEventData, TypeEventResult>,
18
18
  ): Promise<TypeEventResult> {
19
19
  const user = data as IUser;
20
- if (user.name === 'admin' && !this.scope.config.disableRoleAdmin) {
21
- // role: admin
22
- const roleAdmin = await this.scope.model.role.get({ name: 'admin' });
23
- await this.bean.role.addUserId(roleAdmin!.id, user.id);
20
+ const roleRegisteredUser = await this.scope.model.role.get({ name: 'registeredUser' });
21
+ await this.bean.role.addUserId(roleRegisteredUser!.id, user.id);
22
+ if (user.name === 'admin' && !this.scope.config.disableBootstrapSystemAdmin) {
23
+ const roleSystemAdmin = await this.scope.model.role.get({ name: 'systemAdmin' });
24
+ await this.bean.role.addUserId(roleSystemAdmin!.id, user.id);
24
25
  }
25
26
  // next
26
27
  return next();
@@ -18,6 +18,7 @@ export class MetaVersion extends BeanBase implements IMetaVersionUpdate, IMetaVe
18
18
  table.comment(entityRole.$comment.$table);
19
19
  table.basicFields();
20
20
  table.string(entityRole.name, 255).comment(entityRole.$comment.name);
21
+ table.json(entityRole.siteIds).comment(entityRole.$comment.siteIds);
21
22
  });
22
23
  // homeUser
23
24
  const entityUser = this.scope.entity.user;
@@ -47,12 +48,15 @@ export class MetaVersion extends BeanBase implements IMetaVersionUpdate, IMetaVe
47
48
 
48
49
  async init(options: IMetaVersionInitOptions) {
49
50
  if (options.version === 1) {
50
- // role: admin
51
- if (!this.scope.config.disableRoleAdmin) {
52
- await this.scope.model.role.insert({
53
- name: 'admin',
54
- });
55
- }
51
+ // roles
52
+ await this.scope.model.role.insert({
53
+ name: 'registeredUser',
54
+ siteIds: ['web'],
55
+ });
56
+ await this.scope.model.role.insert({
57
+ name: 'systemAdmin',
58
+ siteIds: ['web', 'admin'],
59
+ });
56
60
  // user: admin
57
61
  if (!this.scope.config.disableUserAdmin) {
58
62
  await this.bean.auth.authenticate('auth-simple:simple', {
@@ -5,7 +5,7 @@ export function config(_app: VonaApplication) {
5
5
  passwordDefault: {
6
6
  admin: '123456',
7
7
  },
8
- disableRoleAdmin: false,
8
+ disableBootstrapSystemAdmin: false,
9
9
  disableUserAdmin: false,
10
10
  };
11
11
  }
@@ -111,6 +111,13 @@ export class ControllerPassport extends BeanBase {
111
111
  return this._combineDtoPassportJwt(jwt);
112
112
  }
113
113
 
114
+ @Web.post('refreshAuthToken')
115
+ @Passport.public()
116
+ @Api.body(v.object(DtoJwtToken))
117
+ async refreshAuthToken(@Arg.body('refreshToken') refreshToken: string): Promise<DtoJwtToken> {
118
+ return await this.bean.passport.refreshAuthToken(refreshToken);
119
+ }
120
+
114
121
  @Web.post('createPassportJwtFromOauthCode')
115
122
  @Passport.public()
116
123
  @Api.body(v.object(DtoPassportJwt))
@@ -119,13 +126,6 @@ export class ControllerPassport extends BeanBase {
119
126
  return this._combineDtoPassportJwt(jwt);
120
127
  }
121
128
 
122
- @Web.post('refreshAuthToken')
123
- @Passport.public()
124
- @Api.body(v.object(DtoJwtToken))
125
- async refreshAuthToken(@Arg.body('refreshToken') refreshToken: string): Promise<DtoJwtToken> {
126
- return await this.bean.passport.refreshAuthToken(refreshToken);
127
- }
128
-
129
129
  @Web.post('createTempAuthToken')
130
130
  @Api.body(z.string())
131
131
  async createTempAuthToken(@Arg.query('path', v.optional()) path?: string): Promise<string> {
@@ -3,6 +3,7 @@ import type { IRole } from 'vona-module-a-user';
3
3
 
4
4
  import { Api, v } from 'vona-module-a-openapiutils';
5
5
  import { Entity, EntityBase } from 'vona-module-a-orm';
6
+ import z from 'zod';
6
7
 
7
8
  import { $locale } from '../.metadata/locales.ts';
8
9
 
@@ -12,4 +13,7 @@ export interface IEntityOptionsRole extends IDecoratorEntityOptions {}
12
13
  export class EntityRole extends EntityBase implements IRole {
13
14
  @Api.field(v.title($locale('RoleName')))
14
15
  name: string;
16
+
17
+ @Api.field(v.array(z.string()))
18
+ siteIds: string[];
15
19
  }
@@ -7,9 +7,9 @@ import type { IPayloadDataOfPassport } from '../types/jwt.ts';
7
7
 
8
8
  @Service()
9
9
  export class ServicePassportAdapter extends BeanBase implements IPassportAdapter {
10
- async isAdmin(passport: IPassport | undefined): Promise<boolean> {
10
+ async isSystemAdmin(passport: IPassport | undefined): Promise<boolean> {
11
11
  if (!passport) return false;
12
- if (passport.roles?.some(role => role.name === 'admin')) return true;
12
+ if (passport.roles?.some(role => role.name === 'systemAdmin')) return true;
13
13
  return false;
14
14
  }
15
15
 
@@ -20,10 +20,10 @@ export class ServiceRoleAdapter extends BeanBase implements IRoleAdapter {
20
20
  }
21
21
 
22
22
  async addUserId(id: TableIdentity, userId: TableIdentity): Promise<TableIdentity> {
23
- const res = await this.scope.model.roleUser.insert({
24
- userId,
25
- roleId: id,
26
- });
27
- return res.id;
23
+ let roleUser = await this.scope.model.roleUser.get({ userId, roleId: id });
24
+ if (!roleUser) {
25
+ roleUser = await this.scope.model.roleUser.insert({ userId, roleId: id });
26
+ }
27
+ return roleUser.id;
28
28
  }
29
29
  }
@@ -22,6 +22,7 @@ export interface ISsrMenuOptionsRecord extends IDecoratorSsrMenuOptions<ISsrSite
22
22
  },
23
23
  },
24
24
  group: 'basic-siteadmin:management',
25
+ roles: ['systemAdmin'],
25
26
  },
26
27
  },
27
28
  site: ['basic-siteadmin:admin'],
@@ -22,6 +22,7 @@ export interface ISsrMenuOptionsStudent extends IDecoratorSsrMenuOptions<ISsrSit
22
22
  },
23
23
  },
24
24
  group: 'basic-siteadmin:management',
25
+ roles: ['systemAdmin'],
25
26
  },
26
27
  },
27
28
  site: ['basic-siteadmin:admin'],
@@ -16,6 +16,7 @@ export interface ISsrMenuOptionsHome extends IDecoratorSsrMenuOptions<ISsrSiteOp
16
16
  order: $order(1, 'core'),
17
17
  icon: '::home',
18
18
  link: '/',
19
+ roles: ['systemAdmin'],
19
20
  },
20
21
  site: ['basic-siteadmin:admin'],
21
22
  })
@@ -4,6 +4,10 @@ import type { IIconRecord, IPagePathRecord } from 'zova-rest-cabloy-basic-admin'
4
4
  import { BeanSsrSiteBase, SsrSite } from 'vona-module-a-ssr';
5
5
 
6
6
  declare module 'vona-module-a-ssr' {
7
+ export interface ISsrSiteIdRecord {
8
+ admin: never;
9
+ }
10
+
7
11
  export interface ISsrSitePublicPathRecord {
8
12
  admin: never;
9
13
  }
@@ -20,10 +24,10 @@ export interface ISsrSiteOptionsAdmin extends IDecoratorSsrSiteOptions<
20
24
  > {}
21
25
 
22
26
  @SsrSite<ISsrSiteOptionsAdmin>({
27
+ siteId: 'admin',
23
28
  publicPath: 'admin',
24
29
  bundlePath: 'ssr-cabloyBasicAdmin-5.0.0',
25
30
  diagnostics: {
26
- siteName: 'admin',
27
31
  buildCommand: 'npm run build:zova:admin',
28
32
  },
29
33
  })
@@ -4,6 +4,10 @@ import type { IIconRecord, IPagePathRecord } from 'zova-rest-cabloy-basic-web';
4
4
  import { BeanSsrSiteBase, SsrSite } from 'vona-module-a-ssr';
5
5
 
6
6
  declare module 'vona-module-a-ssr' {
7
+ export interface ISsrSiteIdRecord {
8
+ web: never;
9
+ }
10
+
7
11
  export interface ISsrSitePublicPathRecord {
8
12
  '': never;
9
13
  }
@@ -20,10 +24,10 @@ export interface ISsrSiteOptionsWeb extends IDecoratorSsrSiteOptions<
20
24
  > {}
21
25
 
22
26
  @SsrSite<ISsrSiteOptionsWeb>({
27
+ siteId: 'web',
23
28
  publicPath: '',
24
29
  bundlePath: 'ssr-cabloyBasicWeb-5.0.0',
25
30
  diagnostics: {
26
- siteName: 'web',
27
31
  buildCommand: 'npm run build:zova:web',
28
32
  },
29
33
  })
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-auth",
3
- "version": "5.1.19",
3
+ "version": "5.1.20",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "keywords": [
@@ -60,7 +60,7 @@ export class ServiceAuth extends BeanBase {
60
60
  state?: IAuthenticateStrategyState,
61
61
  ): Promise<IPassport> {
62
62
  // event: issuePassport
63
- return await this.scope.event.issuePassport.emit(
63
+ const passport = await this.scope.event.issuePassport.emit(
64
64
  { profileUser, entityAuthProvider, clientOptions, state },
65
65
  async ({ profileUser, entityAuthProvider, clientOptions, state }) => {
66
66
  return await this._issuePassportInner(
@@ -71,6 +71,10 @@ export class ServiceAuth extends BeanBase {
71
71
  );
72
72
  },
73
73
  );
74
+ if (passport.user) {
75
+ passport.roles = await this.bean.role.findAllByUserId(passport.user.id);
76
+ }
77
+ return passport;
74
78
  }
75
79
 
76
80
  private async _issuePassportInner(
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-suite-a-auth",
3
- "version": "5.1.21",
3
+ "version": "5.1.22",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "author": "",
@@ -4,6 +4,10 @@ import type { IIconRecord, IPagePathRecord } from '<%=argv.ssrSiteRestNpm%>';
4
4
  import { BeanSsrSiteBase, SsrSite } from 'vona-module-a-ssr';
5
5
 
6
6
  declare module 'vona-module-a-ssr' {
7
+ export interface ISsrSiteIdRecord {
8
+ some: never;
9
+ }
10
+
7
11
  export interface ISsrSitePublicPathRecord {
8
12
  some: never;
9
13
  }
@@ -16,10 +20,8 @@ export interface ISsrSiteOptions<%=argv.beanNameCapitalize%>PagesData {}
16
20
  export interface ISsrSiteOptions<%=argv.beanNameCapitalize%> extends IDecoratorSsrSiteOptions<ISsrSiteOptions<%=argv.beanNameCapitalize%>Pages, ISsrSiteOptions<%=argv.beanNameCapitalize%>PagesData, IIconRecord> {}
17
21
 
18
22
  @SsrSite<ISsrSiteOptions<%=argv.beanNameCapitalize%>>({
23
+ siteId: 'some',
19
24
  publicPath: 'some',
20
25
  bundlePath: 'front-bundle-path',
21
- diagnostics: {
22
- siteName: 'some',
23
- },
24
26
  })
25
27
  export class SsrSite<%=argv.beanNameCapitalize%> extends BeanSsrSiteBase<ISsrSiteOptions<%=argv.beanNameCapitalize%>> {}
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-ssr",
3
- "version": "5.1.24",
3
+ "version": "5.1.25",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "keywords": [
@@ -3,14 +3,14 @@ import type { ILocaleRecord } from 'vona';
3
3
  import { BeanEventBase, Event } from 'vona-module-a-event';
4
4
 
5
5
  import type { BeanSsrSiteBase } from '../lib/beanSsrSiteBase.ts';
6
- import type { TypeEventRetrieveMenusResult } from './event.retrieveMenus.ts';
6
+ import type { ISsrMenusPrepared } from '../types/ssrMenu.ts';
7
7
 
8
8
  export interface TypeEventRetrieveMenusSiteData {
9
9
  ssrSite: BeanSsrSiteBase;
10
10
  locale: keyof ILocaleRecord;
11
11
  }
12
12
 
13
- export type TypeEventRetrieveMenusSiteResult = TypeEventRetrieveMenusResult;
13
+ export type TypeEventRetrieveMenusSiteResult = ISsrMenusPrepared;
14
14
 
15
15
  @Event()
16
16
  export class EventRetrieveMenusSite extends BeanEventBase<
@@ -1,7 +1,7 @@
1
1
  import type { ZovaMetaMode } from '@cabloy/module-info';
2
2
  import type { IParamsAndQuery } from '@cabloy/utils';
3
3
  import type { ILocaleRecord } from 'vona';
4
- import type { IMenuGroup, IMenuItem } from 'vona-module-a-menu';
4
+ import type { IMenuGroup } from 'vona-module-a-menu';
5
5
  import type { IOnionSlice } from 'vona-module-a-onion';
6
6
  import type { TypeEventResolvePathData, TypeEventResolvePathResult } from 'vona-module-a-static';
7
7
 
@@ -16,7 +16,12 @@ import { BeanBase, cast, deepExtend, SymbolModuleName } from 'vona';
16
16
  import { checkErrorJwtExpiredAndThrow } from 'vona-module-a-jwt';
17
17
 
18
18
  import type { TypeEventRetrieveMenusResult } from '../bean/event.retrieveMenus.ts';
19
- import type { IDecoratorSsrMenuOptions, ISsrMenuRecord } from '../types/ssrMenu.ts';
19
+ import type { TypeEventRetrieveMenusSiteResult } from '../bean/event.retrieveMenusSite.ts';
20
+ import type {
21
+ IDecoratorSsrMenuOptions,
22
+ ISsrMenuItemPrepared,
23
+ ISsrMenuRecord,
24
+ } from '../types/ssrMenu.ts';
20
25
  import type { ISsrMenuGroupRecord } from '../types/ssrMenuGroup.ts';
21
26
  import type {
22
27
  IDecoratorSsrSiteOptions,
@@ -139,7 +144,15 @@ export class BeanSsrSiteBase<
139
144
  }
140
145
 
141
146
  async retrieveMenus(): Promise<TypeEventRetrieveMenusResult | undefined> {
142
- return await this._getMenusCache(this.ctx.locale);
147
+ const menusPrepared = await this._getMenusCache(this.ctx.locale);
148
+ if (!menusPrepared) return;
149
+ const menus = menusPrepared.menus
150
+ ?.filter(menu => !menu.roles?.length || this.bean.passport.checkRoleName(menu.roles))
151
+ .map(menu => {
152
+ const { roles: _roles, ...menuPublic } = menu;
153
+ return menuPublic;
154
+ });
155
+ return { menus, groups: menusPrepared.groups };
143
156
  }
144
157
 
145
158
  public get siteOptions() {
@@ -168,6 +181,14 @@ export class BeanSsrSiteBase<
168
181
  },
169
182
  this.$scope.ssr.config.site.default,
170
183
  onionOptions,
184
+ {
185
+ envServer: {
186
+ SITE_ID: onionOptions.siteId,
187
+ },
188
+ envClient: {
189
+ SITE_ID: onionOptions.siteId,
190
+ },
191
+ },
171
192
  );
172
193
  }
173
194
  return this._siteOptions;
@@ -253,9 +274,11 @@ export class BeanSsrSiteBase<
253
274
  };
254
275
  }
255
276
 
256
- private async _getMenusCache(locale: keyof ILocaleRecord): Promise<TypeEventRetrieveMenusResult> {
277
+ private async _getMenusCache(
278
+ locale: keyof ILocaleRecord,
279
+ ): Promise<TypeEventRetrieveMenusSiteResult | undefined> {
257
280
  if (!this.app.meta[SymbolCacheMenus]) this.app.meta[SymbolCacheMenus] = {};
258
- const cacheMenus: Record<keyof ILocaleRecord, TypeEventRetrieveMenusResult> =
281
+ const cacheMenus: Record<keyof ILocaleRecord, TypeEventRetrieveMenusSiteResult> =
259
282
  this.app.meta[SymbolCacheMenus];
260
283
  const beanFullName = this.$beanFullName;
261
284
  const instanceName = this.ctx.instanceName;
@@ -268,7 +291,9 @@ export class BeanSsrSiteBase<
268
291
  return cacheMenus[beanFullName][cacheKey];
269
292
  }
270
293
 
271
- private async _prepareMenus(locale: keyof ILocaleRecord): Promise<TypeEventRetrieveMenusResult> {
294
+ private async _prepareMenus(
295
+ locale: keyof ILocaleRecord,
296
+ ): Promise<TypeEventRetrieveMenusSiteResult | undefined> {
272
297
  return await this.$scope.ssr.event.retrieveMenusSite.emit(
273
298
  { ssrSite: this, locale },
274
299
  async () => {
@@ -287,7 +312,7 @@ export class BeanSsrSiteBase<
287
312
  private _prepareMenusOrGroups<T extends keyof ISsrMenuRecord>(
288
313
  locale: keyof ILocaleRecord,
289
314
  ssrMenus: IOnionSlice<ISsrMenuRecord, T, unknown>[],
290
- ): IMenuItem[];
315
+ ): ISsrMenuItemPrepared[];
291
316
  private _prepareMenusOrGroups<T extends keyof ISsrMenuGroupRecord>(
292
317
  locale: keyof ILocaleRecord,
293
318
  ssrMenus: IOnionSlice<ISsrMenuGroupRecord, T, unknown>[],
@@ -298,7 +323,7 @@ export class BeanSsrSiteBase<
298
323
  ) {
299
324
  const siteOnionName = this.$onionName;
300
325
  // menus
301
- const menus: IMenuItem<keyof SsrSiteOptions['pages']>[] = [];
326
+ const menus: ISsrMenuItemPrepared[] = [];
302
327
  for (const ssrMenu of ssrMenus) {
303
328
  const siteMenuOptions = ssrMenu.beanOptions
304
329
  .options as IDecoratorSsrMenuOptions<IDecoratorSsrSiteOptions>;
@@ -16,22 +16,52 @@ export class ServiceSsr extends BeanBase {
16
16
  if (!this.app.meta[SymbolCacheSites]) this.app.meta[SymbolCacheSites] = {};
17
17
  if (!this.app.meta[SymbolCacheSites][cacheKey]) {
18
18
  // check sites
19
- this.app.meta[SymbolCacheSites][cacheKey] = this.bean.onion.ssrSite
20
- .getOnionsEnabled(this.ctx.instanceName!)
21
- .toReversed()
22
- .sort((site1, site2) => {
23
- const siteOptions1 = site1.beanOptions.options as IDecoratorSsrSiteOptions;
24
- const siteOptions2 = site2.beanOptions.options as IDecoratorSsrSiteOptions;
25
- const publicPath1 = `/${siteOptions1.publicPath}`;
26
- const publicPath2 = `/${siteOptions2.publicPath}`;
27
- return publicPath1 === publicPath2
28
- ? 0
29
- : publicPath1.startsWith(publicPath2)
30
- ? -1
31
- : publicPath2.startsWith(publicPath1)
32
- ? 1
33
- : 0;
34
- });
19
+ const sites = this.bean.onion.ssrSite.getOnionsEnabled(this.ctx.instanceName!).toReversed();
20
+ const siteIds = new Map<string, string>();
21
+ const publicPaths = new Map<string, string>();
22
+ for (const site of sites) {
23
+ const siteOptions = site.beanOptions.options as IDecoratorSsrSiteOptions;
24
+ const beanFullName = site.beanOptions.beanFullName;
25
+ const siteId = siteOptions.siteId;
26
+ if (typeof siteId !== 'string' || !siteId.trim()) {
27
+ throw new Error(
28
+ `Should specify non-empty siteId for SSR site: ${beanFullName} (${cacheKey})`,
29
+ );
30
+ }
31
+ const siteIdDuplicate = siteIds.get(siteId);
32
+ if (siteIdDuplicate) {
33
+ throw new Error(
34
+ `Duplicate SSR siteId "${siteId}" for ${siteIdDuplicate} and ${beanFullName} (${cacheKey})`,
35
+ );
36
+ }
37
+ siteIds.set(siteId, beanFullName);
38
+ const publicPath = siteOptions.publicPath;
39
+ if (typeof publicPath !== 'string') {
40
+ throw new TypeError(
41
+ `Should specify publicPath for SSR site: ${beanFullName} (${cacheKey})`,
42
+ );
43
+ }
44
+ const publicPathDuplicate = publicPaths.get(publicPath);
45
+ if (publicPathDuplicate) {
46
+ throw new Error(
47
+ `Duplicate SSR publicPath "${publicPath}" for ${publicPathDuplicate} and ${beanFullName} (${cacheKey})`,
48
+ );
49
+ }
50
+ publicPaths.set(publicPath, beanFullName);
51
+ }
52
+ this.app.meta[SymbolCacheSites][cacheKey] = sites.sort((site1, site2) => {
53
+ const siteOptions1 = site1.beanOptions.options as IDecoratorSsrSiteOptions;
54
+ const siteOptions2 = site2.beanOptions.options as IDecoratorSsrSiteOptions;
55
+ const publicPath1 = `/${siteOptions1.publicPath}`;
56
+ const publicPath2 = `/${siteOptions2.publicPath}`;
57
+ return publicPath1 === publicPath2
58
+ ? 0
59
+ : publicPath1.startsWith(publicPath2)
60
+ ? -1
61
+ : publicPath2.startsWith(publicPath1)
62
+ ? 1
63
+ : 0;
64
+ });
35
65
  }
36
66
  return this.app.meta[SymbolCacheSites][cacheKey];
37
67
  }
@@ -68,10 +68,8 @@ export class ServiceSsrHandler extends BeanBase {
68
68
  }
69
69
 
70
70
  private _createMissingBundleMessage() {
71
- const diagnostics = this._siteOptions.diagnostics;
72
- const siteName = diagnostics?.siteName ?? String(this._siteOptions.publicPath || 'unknown');
73
- const buildCommand = diagnostics?.buildCommand;
71
+ const buildCommand = this._siteOptions.diagnostics?.buildCommand;
74
72
  const commandHint = buildCommand ? ` Run "${buildCommand}" at the repository root.` : '';
75
- return `Missing SSR bundle for site ${siteName}: ${this._siteAssetDir}.${commandHint}`;
73
+ return `Missing SSR bundle for site ${this._siteOptions.siteId}: ${this._siteAssetDir}.${commandHint}`;
76
74
  }
77
75
  }
@@ -4,6 +4,7 @@ export interface ZovaConfigEnv {
4
4
  META_FLAVOR: ZovaMetaFlavor;
5
5
  META_MODE: ZovaMetaMode;
6
6
  META_APP_MODE: ZovaMetaAppMode;
7
+ SITE_ID: string;
7
8
  APP_ROUTER_MODE: 'hash' | 'history' | 'abstract' | undefined;
8
9
  APP_ROUTER_BASE: string | undefined;
9
10
  APP_PUBLIC_PATH: string | undefined;
@@ -1,6 +1,7 @@
1
1
  import type { ILocaleRecord, OmitNever } from 'vona';
2
- import type { IMenuItem } from 'vona-module-a-menu';
2
+ import type { IMenuGroup, IMenuItem } from 'vona-module-a-menu';
3
3
  import type { IOnionOptionsEnable, ServiceOnion } from 'vona-module-a-onion';
4
+ import type { IRoleNameRecord } from 'vona-module-a-user';
4
5
 
5
6
  import type { ISsrMenuGroupRecord } from './ssrMenuGroup.ts';
6
7
  import type { IDecoratorSsrSiteOptions, ISsrSiteRecord } from './ssrSite.ts';
@@ -12,6 +13,14 @@ export interface ISsrMenuItem<Pages extends {} = {}, Icons extends {} = {}> exte
12
13
  'name' | 'group'
13
14
  > {
14
15
  group?: keyof ISsrMenuGroupRecord | (keyof ISsrMenuGroupRecord)[];
16
+ roles?: (keyof IRoleNameRecord)[];
17
+ }
18
+
19
+ export type ISsrMenuItemPrepared = IMenuItem<any, any> & Pick<ISsrMenuItem<any, any>, 'roles'>;
20
+
21
+ export interface ISsrMenusPrepared {
22
+ menus?: ISsrMenuItemPrepared[];
23
+ groups?: IMenuGroup[];
15
24
  }
16
25
 
17
26
  // should not set default generic = IDecoratorSsrSiteOptions
@@ -46,12 +46,13 @@ export interface SSRContextState {
46
46
  pageData?: unknown;
47
47
  }
48
48
 
49
+ export interface ISsrSiteIdRecord {}
50
+
49
51
  export interface ISsrSitePublicPathRecord {}
50
52
 
51
53
  export interface ISsrSiteRecord {}
52
54
 
53
55
  export interface ISsrSiteDiagnosticsOptions {
54
- siteName?: string;
55
56
  buildCommand?: string;
56
57
  }
57
58
 
@@ -68,6 +69,7 @@ export interface IDecoratorSsrSiteOptions<
68
69
  extends
69
70
  IOnionOptionsEnable,
70
71
  IOnionOptionsMatch<TypeOnionOptionsMatchRule<keyof IInstanceRecord>> {
72
+ siteId: keyof ISsrSiteIdRecord;
71
73
  publicPath: keyof ISsrSitePublicPathRecord;
72
74
  bundlePath: string;
73
75
  diagnostics?: ISsrSiteDiagnosticsOptions;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-suite-a-cabloy",
3
- "version": "5.1.39",
3
+ "version": "5.1.40",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "author": "",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-permission",
3
- "version": "5.1.14",
3
+ "version": "5.1.15",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "keywords": [
@@ -27,6 +27,7 @@ declare module 'vona' {
27
27
  }
28
28
  /** bean: end */
29
29
  /** event: begin */
30
+ export * from '../bean/event.retrievePermissionAction.ts';
30
31
  export * from '../bean/event.retrievePermissions.ts';
31
32
 
32
33
  import 'vona';
@@ -36,6 +37,17 @@ declare module 'vona' {
36
37
  }
37
38
  declare module 'vona-module-a-permission' {
38
39
 
40
+ export interface EventRetrievePermissionAction {
41
+ /** @internal */
42
+ get scope(): ScopeModuleAPermission;
43
+ }
44
+
45
+ export interface EventRetrievePermissionAction {
46
+ get $beanFullName(): 'a-permission.event.retrievePermissionAction';
47
+ get $onionName(): 'a-permission:retrievePermissionAction';
48
+
49
+ }
50
+
39
51
  export interface EventRetrievePermissions {
40
52
  /** @internal */
41
53
  get scope(): ScopeModuleAPermission;
@@ -49,17 +61,21 @@ declare module 'vona-module-a-permission' {
49
61
  }
50
62
  /** event: end */
51
63
  /** event: begin */
64
+ import type { EventRetrievePermissionAction } from '../bean/event.retrievePermissionAction.ts';
52
65
  import type { EventRetrievePermissions } from '../bean/event.retrievePermissions.ts';
53
66
  export interface IModuleEvent {
54
- 'retrievePermissions': EventRetrievePermissions;
67
+ 'retrievePermissionAction': EventRetrievePermissionAction;
68
+ 'retrievePermissions': EventRetrievePermissions;
55
69
  }
56
70
  /** event: end */
57
71
  /** event: begin */
72
+ import type { TypeEventRetrievePermissionActionData, TypeEventRetrievePermissionActionResult } from '../bean/event.retrievePermissionAction.ts';
58
73
  import type { TypeEventRetrievePermissionsData, TypeEventRetrievePermissionsResult } from '../bean/event.retrievePermissions.ts';
59
74
  import type { EventOn } from 'vona-module-a-event';
60
75
  declare module 'vona-module-a-event' {
61
76
  export interface IEventRecord {
62
- 'a-permission:retrievePermissions': EventOn<TypeEventRetrievePermissionsData, TypeEventRetrievePermissionsResult>;
77
+ 'a-permission:retrievePermissionAction': EventOn<TypeEventRetrievePermissionActionData, TypeEventRetrievePermissionActionResult>;
78
+ 'a-permission:retrievePermissions': EventOn<TypeEventRetrievePermissionsData, TypeEventRetrievePermissionsResult>;
63
79
  }
64
80
  }
65
81
  /** event: end */
@@ -88,7 +88,12 @@ export class BeanPermission extends BeanBase {
88
88
  const route = this._getControllerActionRoute(resource, actionKey);
89
89
  if (!route?.route?.meta) return false;
90
90
  if (!this._matchPassportMeta(route.route.meta)) return false;
91
- return await this.retrievePermissionActionByRoles(resource, actionKey);
91
+ return await this.scope.event.retrievePermissionAction.emit(
92
+ { resource, actionKey },
93
+ async () => {
94
+ return await this.retrievePermissionActionByRoles(resource, actionKey);
95
+ },
96
+ );
92
97
  }
93
98
 
94
99
  @Caching.get({
@@ -0,0 +1,16 @@
1
+ import type { IResourceRecord } from 'vona-module-a-openapi';
2
+
3
+ import { BeanEventBase, Event } from 'vona-module-a-event';
4
+
5
+ export interface TypeEventRetrievePermissionActionData {
6
+ resource: keyof IResourceRecord;
7
+ actionKey: string;
8
+ }
9
+
10
+ export type TypeEventRetrievePermissionActionResult = boolean;
11
+
12
+ @Event()
13
+ export class EventRetrievePermissionAction extends BeanEventBase<
14
+ TypeEventRetrievePermissionActionData,
15
+ TypeEventRetrievePermissionActionResult
16
+ > {}
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-user",
3
- "version": "5.1.18",
3
+ "version": "5.1.19",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "keywords": [