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.
- package/.cabloy-version +1 -1
- package/.github/workflows/vona-test-sqlite3.yml +1 -1
- package/CHANGELOG.md +26 -0
- package/package.json +1 -1
- package/repo-agent-governance/managed-assets.json +6 -6
- package/repo-agent-governance/policies/repository.md +1 -0
- package/repo-agent-governance/skills/cabloy-frontend-scaffold/SKILL.md +4 -2
- package/repo-docs/backend/auth-guide.md +48 -0
- package/repo-docs/backend/demonstration-mode-guide.md +23 -3
- package/repo-docs/backend/openapi-guide.md +1 -1
- package/repo-docs/frontend/behavior-guide.md +12 -0
- package/repo-e2e/specs/home-user-account.spec.ts +98 -3
- package/vona/env/.env +2 -0
- package/vona/packages-cli/cli/package.json +1 -1
- package/vona/packages-cli/cli-set-api/package.json +1 -1
- package/vona/packages-cli/cli-set-api/src/lib/bean/cli.bin.build.ts +11 -0
- package/vona/packages-cli/cli-set-api/test/binBuildFreshInputs.test.ts +152 -0
- package/vona/packages-utils/babel-plugin-vona-bean-module/package.json +1 -1
- package/vona/packages-utils/babel-plugin-vona-bean-module/src/index.test.ts +27 -0
- package/vona/packages-utils/babel-plugin-vona-bean-module/src/index.ts +1 -0
- package/vona/packages-vona/vona/package.json +1 -1
- package/vona/packages-vona/vona-core/package.json +1 -1
- package/vona/packages-vona/vona-core/src/lib/core/application.ts +2 -2
- package/vona/packages-vona/vona-mock/package.json +1 -1
- package/vona/src/suite/a-demo/modules/demo-demonstration/src/bean/guard.demonstration.ts +8 -1
- package/vona/src/suite/a-demo/modules/demo-demonstration/test/guardDemonstration.test.ts +79 -3
- package/vona/src/suite/a-home/modules/home-user/test/passportSession.test.ts +380 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/package.json +1 -1
- package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
- package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +1 -1
- 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/decorator/mcp.ts +2 -2
- 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/bean/bean.passport.ts +82 -20
- package/vona/src/suite-vendor/a-vona/modules/a-user/src/config/config.ts +4 -1
- package/vona/src/suite-vendor/a-vona/modules/a-user/src/lib/auth.ts +20 -1
- package/vona/src/suite-vendor/a-vona/modules/a-user/src/main.ts +3 -0
- package/vona/src/suite-vendor/a-vona/modules/a-user/src/service/authTokenAdapter.ts +4 -1
- package/vona/src/suite-vendor/a-vona/modules/a-user/src/service/redisToken.ts +6 -2
- package/vona/src/suite-vendor/a-vona/modules/a-user/src/types/auth.ts +8 -0
- package/vona/src/suite-vendor/a-vona/modules/a-user/src/types/passport.ts +1 -0
- package/vona/src/suite-vendor/a-vona/package.json +1 -1
- package/zova/src/suite/a-home/modules/home-layoutadmin/src/component/layoutAdmin/render.user.tsx +5 -4
- 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 {
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
{
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
{
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
+
}
|
package/zova/src/suite/a-home/modules/home-layoutadmin/src/component/layoutAdmin/render.user.tsx
CHANGED
|
@@ -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
|
-
<
|
|
36
|
-
|
|
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
|
-
</
|
|
43
|
+
</ZButton>
|
|
43
44
|
</li>
|
|
44
45
|
</ul>
|
|
45
46
|
</details>
|
package/zova/src/suite/a-home/modules/home-layoutweb/src/component/layoutWeb/render.user.tsx
CHANGED
|
@@ -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
|
-
<
|
|
43
|
-
|
|
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
|
-
</
|
|
50
|
+
</ZButton>
|
|
50
51
|
</li>
|
|
51
52
|
</>
|
|
52
53
|
) : (
|