cabloy 5.1.242 → 5.1.244

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 (45) hide show
  1. package/.cabloy-version +1 -1
  2. package/.github/workflows/vona-test-sqlite3.yml +1 -1
  3. package/CHANGELOG.md +26 -0
  4. package/package.json +1 -1
  5. package/repo-agent-governance/managed-assets.json +6 -6
  6. package/repo-agent-governance/policies/repository.md +1 -0
  7. package/repo-agent-governance/skills/cabloy-frontend-scaffold/SKILL.md +4 -2
  8. package/repo-docs/backend/auth-guide.md +48 -0
  9. package/repo-docs/backend/demonstration-mode-guide.md +23 -3
  10. package/repo-docs/backend/openapi-guide.md +1 -1
  11. package/repo-docs/frontend/behavior-guide.md +12 -0
  12. package/repo-e2e/specs/home-user-account.spec.ts +98 -3
  13. package/vona/env/.env +2 -0
  14. package/vona/packages-cli/cli/package.json +1 -1
  15. package/vona/packages-cli/cli-set-api/package.json +1 -1
  16. package/vona/packages-cli/cli-set-api/src/lib/bean/cli.bin.build.ts +11 -0
  17. package/vona/packages-cli/cli-set-api/test/binBuildFreshInputs.test.ts +152 -0
  18. package/vona/packages-utils/babel-plugin-vona-bean-module/package.json +1 -1
  19. package/vona/packages-utils/babel-plugin-vona-bean-module/src/index.test.ts +27 -0
  20. package/vona/packages-utils/babel-plugin-vona-bean-module/src/index.ts +1 -0
  21. package/vona/packages-vona/vona/package.json +1 -1
  22. package/vona/packages-vona/vona-core/package.json +1 -1
  23. package/vona/packages-vona/vona-core/src/lib/core/application.ts +2 -2
  24. package/vona/packages-vona/vona-mock/package.json +1 -1
  25. package/vona/src/suite/a-demo/modules/demo-demonstration/src/bean/guard.demonstration.ts +8 -1
  26. package/vona/src/suite/a-demo/modules/demo-demonstration/test/guardDemonstration.test.ts +79 -3
  27. package/vona/src/suite/a-home/modules/home-user/test/passportSession.test.ts +380 -0
  28. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/package.json +1 -1
  29. package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
  30. package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +1 -1
  31. package/vona/src/suite-vendor/a-vona/modules/a-openapiutils/package.json +1 -1
  32. package/vona/src/suite-vendor/a-vona/modules/a-openapiutils/src/lib/decorator/mcp.ts +2 -2
  33. package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
  34. package/vona/src/suite-vendor/a-vona/modules/a-user/package.json +1 -1
  35. package/vona/src/suite-vendor/a-vona/modules/a-user/src/bean/bean.passport.ts +82 -20
  36. package/vona/src/suite-vendor/a-vona/modules/a-user/src/config/config.ts +4 -1
  37. package/vona/src/suite-vendor/a-vona/modules/a-user/src/lib/auth.ts +20 -1
  38. package/vona/src/suite-vendor/a-vona/modules/a-user/src/main.ts +3 -0
  39. package/vona/src/suite-vendor/a-vona/modules/a-user/src/service/authTokenAdapter.ts +4 -1
  40. package/vona/src/suite-vendor/a-vona/modules/a-user/src/service/redisToken.ts +6 -2
  41. package/vona/src/suite-vendor/a-vona/modules/a-user/src/types/auth.ts +8 -0
  42. package/vona/src/suite-vendor/a-vona/modules/a-user/src/types/passport.ts +1 -0
  43. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  44. package/zova/src/suite/a-home/modules/home-layoutadmin/src/component/layoutAdmin/render.user.tsx +5 -4
  45. package/zova/src/suite/a-home/modules/home-layoutweb/src/component/layoutWeb/render.user.tsx +5 -4
@@ -10,13 +10,19 @@ import { catchError, isNil } from '@cabloy/utils';
10
10
  import { BeanBase, beanFullNameFromOnionName } from 'vona';
11
11
  import { Bean } from 'vona-module-a-bean';
12
12
 
13
- import type { IAuth, IAuthIdRecord, IAuthTokenOptions, ISigninOptions } from '../types/auth.ts';
13
+ import type {
14
+ IAuth,
15
+ IAuthIdRecord,
16
+ IAuthTokenOptions,
17
+ ISigninOptions,
18
+ TypeAuthTokenMode,
19
+ } from '../types/auth.ts';
14
20
  import type { IAuthTokenAdapter } from '../types/authToken.ts';
15
21
  import type { IPassport, IPassportAdapter } from '../types/passport.ts';
16
22
  import type { IRole, IRoleNameRecord } from '../types/role.ts';
17
23
  import type { IUser, IUserNameRecord } from '../types/user.ts';
18
24
 
19
- import { $getAuthIdSystem } from '../lib/auth.ts';
25
+ import { $getAuthIdSystem, $getAuthTokenMode } from '../lib/auth.ts';
20
26
 
21
27
  @Bean()
22
28
  export class BeanPassport extends BeanBase {
@@ -43,6 +49,10 @@ export class BeanPassport extends BeanBase {
43
49
  return this._passportAdapter;
44
50
  }
45
51
 
52
+ public get authTokenMode(): TypeAuthTokenMode {
53
+ return $getAuthTokenMode(this.scope.config.authToken);
54
+ }
55
+
46
56
  public get isAuthenticated(): boolean {
47
57
  const user = this.currentUser;
48
58
  return !!user && !user.anonymous;
@@ -71,6 +81,7 @@ export class BeanPassport extends BeanBase {
71
81
  }
72
82
 
73
83
  public async setCurrent(passport: IPassport | undefined) {
84
+ this.ctx.state.passportPayloadData = undefined;
74
85
  this.ctx.state.passport = await this.passportAdapter.setCurrent(passport);
75
86
  }
76
87
 
@@ -107,9 +118,13 @@ export class BeanPassport extends BeanBase {
107
118
  ttl: this.scope.config.authToken.ttl,
108
119
  },
109
120
  );
110
- const payloadData = await this._passportSerialize(passport, authTokenOptions);
121
+ const payloadData = await this._passportSerialize(passport, authTokenOptions, true);
111
122
  // jwt token
112
- return await this.bean.jwt.create(payloadData, { dev: passport.auth?.id.toString() === '-1' });
123
+ const jwt = await this.bean.jwt.create(payloadData, {
124
+ dev: passport.auth?.id.toString() === '-1',
125
+ });
126
+ this.ctx.state.passportPayloadData = payloadData;
127
+ return jwt;
113
128
  }
114
129
 
115
130
  public async signout(): Promise<void> {
@@ -117,7 +132,10 @@ export class BeanPassport extends BeanBase {
117
132
  const passport = this.current;
118
133
  if (!passport) return;
119
134
  // removeAuthToken
120
- const payloadData = await this.passportAdapter.serialize(passport);
135
+ const payloadData =
136
+ this.authTokenMode === 'session'
137
+ ? this._getCurrentPayloadData()
138
+ : await this.passportAdapter.serialize(passport);
121
139
  await this.authTokenAdapter.remove(payloadData);
122
140
  // event
123
141
  await this.scope.event.signout.emit(passport);
@@ -191,6 +209,7 @@ export class BeanPassport extends BeanBase {
191
209
  }
192
210
  // setCurrent
193
211
  await this.setCurrent(passport);
212
+ this.ctx.state.passportPayloadData = payloadData;
194
213
  return { payloadData, passport };
195
214
  }
196
215
 
@@ -211,7 +230,9 @@ export class BeanPassport extends BeanBase {
211
230
  );
212
231
  payloadData = await this._handlePayloadData(payloadData, authTokenOptions);
213
232
  // jwt token
214
- return await this.bean.jwt.create(payloadData);
233
+ const jwt = await this.bean.jwt.create(payloadData);
234
+ this.ctx.state.passportPayloadData = payloadData;
235
+ return jwt;
215
236
  }
216
237
 
217
238
  // only created by accessToken
@@ -247,7 +268,10 @@ export class BeanPassport extends BeanBase {
247
268
 
248
269
  public async createOauthCodeFromOauthAuthToken(accessToken: string, options?: IJwtSignOptions) {
249
270
  // payloadData
250
- const payloadData = await this.bean.jwt.get('access').verify(accessToken);
271
+ const payloadData =
272
+ this.authTokenMode === 'session'
273
+ ? (await this.checkAuthToken(accessToken))?.payloadData
274
+ : await this.bean.jwt.get('access').verify(accessToken);
251
275
  if (!payloadData) return this.app.throw(401);
252
276
  // code
253
277
  return await this.bean.jwt.createOauthCode(payloadData, options);
@@ -282,7 +306,24 @@ export class BeanPassport extends BeanBase {
282
306
  return true;
283
307
  }
284
308
 
285
- private async _passportSerialize(passport: IPassport, authTokenOptions?: IAuthTokenOptions) {
309
+ private _getCurrentPayloadData(): IPayloadData {
310
+ const payloadData = this.ctx.state.passportPayloadData;
311
+ if (!payloadData?.[this.scope.config.payloadData.fields.token]) return this.app.throw(401);
312
+ return payloadData;
313
+ }
314
+
315
+ private async _passportSerialize(
316
+ passport: IPassport,
317
+ authTokenOptions?: IAuthTokenOptions,
318
+ signin = false,
319
+ ) {
320
+ if (this.authTokenMode === 'session') {
321
+ if (signin) {
322
+ const payloadData = await this.passportAdapter.serialize(passport);
323
+ return await this.authTokenAdapter.create(payloadData, authTokenOptions?.ttl);
324
+ }
325
+ return await this._handlePayloadData(this._getCurrentPayloadData(), authTokenOptions);
326
+ }
286
327
  // serialize
287
328
  const payloadData = await this.passportAdapter.serialize(passport);
288
329
  return await this._handlePayloadData(payloadData, authTokenOptions);
@@ -296,6 +337,18 @@ export class BeanPassport extends BeanBase {
296
337
  const stateless = authTokenOptions?.stateless;
297
338
  const strategy = authTokenOptions?.strategy ?? 'refresh';
298
339
  const ttl = authTokenOptions?.ttl;
340
+ if (this.authTokenMode === 'session') {
341
+ if (stateless) throw new Error('Session auth-token mode requires stateful authentication');
342
+ if (strategy === 'reissue') {
343
+ throw new Error('Session auth-token mode does not support the reissue refresh strategy');
344
+ }
345
+ const verified = await this.authTokenAdapter.verify(payloadData);
346
+ if (!verified) return this.app.throw(401);
347
+ if (strategy === 'refresh') {
348
+ await this.authTokenAdapter.refresh(payloadData, ttl);
349
+ }
350
+ return payloadData;
351
+ }
299
352
  if (stateless) {
300
353
  // do nothing
301
354
  return payloadData;
@@ -320,17 +373,26 @@ export class BeanPassport extends BeanBase {
320
373
  authTokenOptionsCustom: IAuthTokenOptions | undefined,
321
374
  authTokenOptionsDefault: IAuthTokenOptions,
322
375
  ) {
323
- if (!authProviderId) return authTokenOptionsDefault;
324
- const { clientOptions } = await this.bean.authProvider.getClientOptions(
325
- {
326
- id: authProviderId,
327
- },
328
- { authTokenOptions: authTokenOptionsCustom ?? {} },
329
- {
330
- authTokenOptions: authTokenOptionsDefault,
331
- },
332
- );
333
- if (!clientOptions) this.app.throw(403);
334
- return clientOptions.authTokenOptions!;
376
+ let authTokenOptions = authTokenOptionsDefault;
377
+ if (this.authTokenMode === 'session') {
378
+ authTokenOptions = { ...authTokenOptions, ...authTokenOptionsCustom };
379
+ }
380
+ if (authProviderId) {
381
+ const { clientOptions } = await this.bean.authProvider.getClientOptions(
382
+ {
383
+ id: authProviderId,
384
+ },
385
+ { authTokenOptions: authTokenOptionsCustom ?? {} },
386
+ {
387
+ authTokenOptions: authTokenOptionsDefault,
388
+ },
389
+ );
390
+ if (!clientOptions) this.app.throw(403);
391
+ authTokenOptions = clientOptions.authTokenOptions!;
392
+ }
393
+ if (this.authTokenMode === 'session' && authTokenOptions.stateless) {
394
+ throw new Error('Session auth-token mode requires stateful authentication');
395
+ }
396
+ return authTokenOptions;
335
397
  }
336
398
  }
@@ -2,12 +2,15 @@ import type { VonaApplication } from 'vona';
2
2
 
3
3
  import type { ConfigUser } from '../types/auth.ts';
4
4
 
5
- export function config(_app: VonaApplication) {
5
+ import { $parseAuthTokenMode } from '../lib/auth.ts';
6
+
7
+ export function config(app: VonaApplication) {
6
8
  const configUser: ConfigUser = {
7
9
  user: {
8
10
  autoActivate: false,
9
11
  },
10
12
  authToken: {
13
+ mode: $parseAuthTokenMode(app.meta.env.AUTH_TOKEN_MODE),
11
14
  stateless: false,
12
15
  strategy: {
13
16
  refreshAuthToken: 'refresh',
@@ -1,6 +1,25 @@
1
1
  import type { TableIdentity } from 'table-identity';
2
2
 
3
- import type { IAuthIdRecord } from '../types/auth.ts';
3
+ import type { IAuthIdRecord, IAuthTokenConfig, TypeAuthTokenMode } from '../types/auth.ts';
4
+
5
+ export function $parseAuthTokenMode(value: unknown): TypeAuthTokenMode {
6
+ if (value === undefined) return 'auth';
7
+ if (value === 'auth' || value === 'session') return value;
8
+ throw new Error('AUTH_TOKEN_MODE / authToken.mode must be auth or session');
9
+ }
10
+
11
+ export function $getAuthTokenMode(config: IAuthTokenConfig): TypeAuthTokenMode {
12
+ const mode = $parseAuthTokenMode(config.mode);
13
+ if (mode === 'session') {
14
+ if (config.stateless) {
15
+ throw new Error('Session auth-token mode requires stateful authentication');
16
+ }
17
+ if (config.strategy.refreshAuthToken === 'reissue') {
18
+ throw new Error('Session auth-token mode does not support the reissue refresh strategy');
19
+ }
20
+ }
21
+ return mode;
22
+ }
4
23
 
5
24
  export function $getAuthIdSystem<K extends keyof IAuthIdRecord>(
6
25
  _authName: IAuthIdRecord[K],
@@ -2,9 +2,12 @@ import type { IModuleMain, VonaContext } from 'vona';
2
2
 
3
3
  import { BeanSimple } from 'vona';
4
4
 
5
+ import { $getAuthTokenMode } from './lib/auth.ts';
6
+
5
7
  export class Main extends BeanSimple implements IModuleMain {
6
8
  async moduleLoading() {}
7
9
  async moduleLoaded() {
10
+ $getAuthTokenMode(this.app.config.modules['a-user'].authToken);
8
11
  // user
9
12
  Object.defineProperty(this.app.context, 'user', {
10
13
  enumerable: false,
@@ -10,7 +10,10 @@ import type { IUser } from '../types/user.ts';
10
10
  export class ServiceAuthTokenAdapter extends BeanBase implements IAuthTokenAdapter {
11
11
  async create(payloadData: IPayloadData, ttl?: number): Promise<IPayloadData> {
12
12
  const authIdStr = this._getAuthId(payloadData)?.toString();
13
- const token = authIdStr === '-1' ? createHash(authIdStr) : uuidv4();
13
+ const token =
14
+ this.bean.passport.authTokenMode === 'auth' && authIdStr === '-1'
15
+ ? createHash(authIdStr)
16
+ : uuidv4();
14
17
  const payloadDataNew = Object.assign({}, payloadData, {
15
18
  [this.scope.config.payloadData.fields.token]: token,
16
19
  });
@@ -47,11 +47,15 @@ export class ServiceRedisToken extends BeanBase {
47
47
 
48
48
  private _getAuthRedisKey(payloadData: IPayloadData) {
49
49
  if (!this.ctx.instance) return;
50
- return `${this._getUserId(payloadData)}:${this._getAuthId(payloadData)}`;
50
+ const key = `${this._getUserId(payloadData)}:${this._getAuthId(payloadData)}`;
51
+ if (this.bean.passport.authTokenMode === 'auth') return key;
52
+ const token = this._getToken(payloadData);
53
+ if (!token) return;
54
+ return `${key}:${token}`;
51
55
  }
52
56
 
53
57
  private _getAuthRedisKeyPrefix(user: IUser) {
54
- return `${user.id}`;
58
+ return `${user.id}:`;
55
59
  }
56
60
 
57
61
  private _getToken(payloadData: IPayloadData) {
@@ -21,8 +21,10 @@ export interface IAuth {
21
21
  }
22
22
 
23
23
  export type TypeAuthTokenStrategy = 'reissue' | 'refresh' | 'reuse';
24
+ export type TypeAuthTokenMode = 'auth' | 'session';
24
25
 
25
26
  export interface IAuthTokenConfig {
27
+ mode: TypeAuthTokenMode;
26
28
  stateless: boolean;
27
29
  strategy: {
28
30
  refreshAuthToken: TypeAuthTokenStrategy;
@@ -60,3 +62,9 @@ export interface IAuthTokenOptions {
60
62
  export interface ISigninOptions {
61
63
  authToken?: IAuthTokenOptions;
62
64
  }
65
+
66
+ declare module 'vona' {
67
+ export interface VonaConfigEnv {
68
+ AUTH_TOKEN_MODE: string | undefined;
69
+ }
70
+ }
@@ -20,6 +20,7 @@ export interface IPassportAdapter {
20
20
  declare module 'vona' {
21
21
  export interface ContextState {
22
22
  passport?: IPassport;
23
+ passportPayloadData?: IPayloadData;
23
24
  }
24
25
 
25
26
  export interface VonaContext {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-suite-a-vona",
3
- "version": "5.1.112",
3
+ "version": "5.1.114",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "author": "",
@@ -1,5 +1,6 @@
1
1
  import { BeanRenderBase } from 'zova';
2
2
  import { Render } from 'zova-module-a-bean';
3
+ import { ZButton } from 'zova-module-basic-button';
3
4
  import { resolveImagePreviewUrl } from 'zova-module-basic-image';
4
5
  import { closeNearestDetails } from 'zova-module-home-base';
5
6
 
@@ -32,14 +33,14 @@ export class RenderUser extends BeanRenderBase {
32
33
  </a>
33
34
  </li>
34
35
  <li>
35
- <a
36
- onClick={event => {
37
- this.$passport.logout().mutate();
36
+ <ZButton
37
+ onPerform={async event => {
38
38
  closeNearestDetails(event);
39
+ await this.$passport.logout().mutateAsync();
39
40
  }}
40
41
  >
41
42
  {this.scope.locale.Logout()}
42
- </a>
43
+ </ZButton>
43
44
  </li>
44
45
  </ul>
45
46
  </details>
@@ -1,5 +1,6 @@
1
1
  import { BeanRenderBase, ClientOnly } from 'zova';
2
2
  import { Render } from 'zova-module-a-bean';
3
+ import { ZButton } from 'zova-module-basic-button';
3
4
  import { resolveImagePreviewUrl } from 'zova-module-basic-image';
4
5
  import { closeNearestDetails } from 'zova-module-home-base';
5
6
 
@@ -39,14 +40,14 @@ export class RenderUser extends BeanRenderBase {
39
40
  </a>
40
41
  </li>
41
42
  <li>
42
- <a
43
- onClick={event => {
44
- this.$passport.logout().mutate();
43
+ <ZButton
44
+ onPerform={async event => {
45
45
  closeNearestDetails(event);
46
+ await this.$passport.logout().mutateAsync();
46
47
  }}
47
48
  >
48
49
  {this.scope.locale.Logout()}
49
- </a>
50
+ </ZButton>
50
51
  </li>
51
52
  </>
52
53
  ) : (