cabloy 5.1.239 → 5.1.241

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 (51) hide show
  1. package/.cabloy-version +1 -1
  2. package/CHANGELOG.md +19 -0
  3. package/package.json +1 -1
  4. package/repo-docs/backend/openapi-guide.md +18 -0
  5. package/repo-docs/backend/rate-limit-guide.md +54 -9
  6. package/repo-e2e/specs/home-user-account.spec.ts +53 -8
  7. package/vona/packages-vona/vona/package.json +1 -1
  8. package/vona/pnpm-lock.yaml +16 -141
  9. package/vona/src/suite/a-commerce/modules/commerce-siteadmin/src/dto/operatorContext.ts +4 -1
  10. package/vona/src/suite/a-commerce/modules/commerce-siteadmin/src/service/operator.ts +1 -0
  11. package/vona/src/suite/a-commerce/modules/commerce-siteadmin/test/operatorAccess.test.ts +1 -0
  12. package/vona/src/suite/a-home/modules/home-user/src/bean/meta.version.ts +1 -0
  13. package/vona/src/suite/a-home/modules/home-user/src/controller/passport.ts +1 -0
  14. package/vona/src/suite/a-home/modules/home-user/src/dto/accountCurrent.ts +3 -0
  15. package/vona/src/suite/a-home/modules/home-user/src/dto/accountProfileUpdate.ts +9 -2
  16. package/vona/src/suite/a-home/modules/home-user/src/dto/passportUser.ts +3 -0
  17. package/vona/src/suite/a-home/modules/home-user/src/entity/user.ts +3 -0
  18. package/vona/src/suite/a-home/modules/home-user/src/service/account.ts +7 -3
  19. package/vona/src/suite/a-home/modules/home-user/src/service/userAdapter.ts +1 -0
  20. package/vona/src/suite/a-home/modules/home-user/test/account.test.ts +133 -8
  21. package/vona/src/suite/a-home/modules/home-user/test/accountOpenapi.test.ts +32 -5
  22. package/vona/src/suite/a-home/modules/home-user/test/passport.test.ts +159 -1
  23. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/package.json +1 -1
  24. package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
  25. package/vona/src/suite-vendor/a-vona/modules/a-hmr/package.json +1 -1
  26. package/vona/src/suite-vendor/a-vona/modules/a-hmr/src/service/hmr.ts +5 -12
  27. package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
  28. package/vona/src/suite-vendor/a-vona/modules/a-permission/src/bean/bean.permission.ts +54 -8
  29. package/vona/src/suite-vendor/a-vona/modules/a-permission/test/permission.test.ts +228 -16
  30. package/vona/src/suite-vendor/a-vona/modules/a-ratelimit/package.json +1 -1
  31. package/vona/src/suite-vendor/a-vona/modules/a-ratelimit/src/.metadata/index.ts +26 -0
  32. package/vona/src/suite-vendor/a-vona/modules/a-ratelimit/src/bean/middlewareSystem.aggregate.ts +58 -0
  33. package/vona/src/suite-vendor/a-vona/modules/a-ratelimit/src/service/rateLimit.ts +25 -5
  34. package/vona/src/suite-vendor/a-vona/modules/a-ratelimit/src/types/rateLimit.ts +5 -0
  35. package/vona/src/suite-vendor/a-vona/modules/a-ratelimit/test/aggregate.test.ts +247 -0
  36. package/vona/src/suite-vendor/a-vona/modules/a-user/package.json +1 -1
  37. package/vona/src/suite-vendor/a-vona/modules/a-user/src/types/user.ts +1 -0
  38. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  39. package/zova/src/suite/a-commerce/modules/commerce-siteadmin/src/api/openapi/schemas.ts +68 -52
  40. package/zova/src/suite/a-commerce/modules/commerce-siteadmin/src/api/openapi/types.ts +530 -344
  41. package/zova/src/suite/a-commerce/modules/commerce-siteadmin/src/page/dashboard/controller.tsx +7 -1
  42. package/zova/src/suite/a-home/modules/home-api/src/api/openapi/types.ts +6 -1
  43. package/zova/src/suite/a-home/modules/home-base/src/config/locale/en-us.ts +1 -0
  44. package/zova/src/suite/a-home/modules/home-base/src/config/locale/zh-cn.ts +1 -0
  45. package/zova/src/suite/a-home/modules/home-layoutadmin/src/component/layoutAdmin/render.user.tsx +1 -1
  46. package/zova/src/suite/a-home/modules/home-layoutweb/src/component/layoutWeb/render.user.tsx +3 -1
  47. package/zova/src/suite/a-home/modules/home-passport/src/model/passport.ts +4 -0
  48. package/zova/src/suite/a-home/modules/home-user/src/config/locale/en-us.ts +1 -0
  49. package/zova/src/suite/a-home/modules/home-user/src/config/locale/zh-cn.ts +1 -0
  50. package/zova/src/suite/a-home/modules/home-user/src/model/account.ts +2 -1
  51. package/zova/src/suite/a-home/modules/home-user/src/page/account/controller.tsx +17 -3
@@ -63,7 +63,10 @@ export class ServiceAccount extends BeanBase {
63
63
  const locale = this._resolveLocale(command.locale, user.locale);
64
64
  const tz = this._resolveTimezone(command.tz, user.tz);
65
65
  await this.scope.model.user.updateById(user.id, {
66
- name: command.name,
66
+ displayName:
67
+ command.displayName === undefined
68
+ ? user.displayName
69
+ : command.displayName?.trim() || undefined,
67
70
  avatar,
68
71
  locale,
69
72
  tz,
@@ -218,12 +221,12 @@ export class ServiceAccount extends BeanBase {
218
221
  await this.bean.mail.send({
219
222
  to: currentUser.email,
220
223
  subject: replaceTemplate(this.scope.locale.ActivationEmailSubject(), {
221
- userName: currentUser.name,
224
+ userName: currentUser.displayName || currentUser.name,
222
225
  link,
223
226
  siteName: this.ctx.instance.title || this.app.meta.env.APP_TITLE,
224
227
  }),
225
228
  text: replaceTemplate(this.scope.locale.ActivationEmailBody(), {
226
- userName: currentUser.name,
229
+ userName: currentUser.displayName || currentUser.name,
227
230
  link,
228
231
  siteName: this.ctx.instance.title || this.app.meta.env.APP_TITLE,
229
232
  }),
@@ -521,6 +524,7 @@ export class ServiceAccount extends BeanBase {
521
524
  const eligibleEmail = user.email?.trim() || undefined;
522
525
  return {
523
526
  name: user.name,
527
+ displayName: user.displayName,
524
528
  avatar: user.avatar,
525
529
  locale: user.locale,
526
530
  tz: user.tz,
@@ -13,6 +13,7 @@ export class ServiceUserAdapter extends BeanBase implements IUserAdapter {
13
13
  async userOfProfile(profile: IAuthUserProfile): Promise<Partial<IUser>> {
14
14
  return {
15
15
  name: profile.username!,
16
+ displayName: profile.displayName?.trim() || undefined,
16
17
  email: profile.emails?.[0].value,
17
18
  avatar: profile.photos?.[0].value,
18
19
  locale: profile.locale || this.ctx.locale,
@@ -34,19 +34,22 @@ describe('account.test.ts', { concurrency: false }, () => {
34
34
  innerAccess: false,
35
35
  });
36
36
  assert.equal(current.name, fixture!.name);
37
+ assert.equal(current.displayName, undefined);
37
38
  assert.equal(current.hasSimpleAuth, true);
38
39
  assert.equal(current.canSendSetPasswordLink, false);
39
40
 
40
41
  const updated = await app.bean.executor.performAction('patch', profilePath, {
41
42
  innerAccess: false,
42
43
  body: {
43
- name: `${fixture!.name}-updated`,
44
+ displayName: ' Updated display name ',
45
+ name: `${fixture!.name}-must-not-rename`,
44
46
  locale: 'en_us',
45
47
  tz: 'America/New_York',
46
48
  ignored: 'must-not-persist',
47
49
  },
48
50
  });
49
- assert.equal(updated.name, `${fixture!.name}-updated`);
51
+ assert.equal(updated.name, fixture!.name);
52
+ assert.equal(updated.displayName, 'Updated display name');
50
53
  assert.equal(updated.locale, 'en-us');
51
54
  assert.equal(updated.tz, 'America/New_York');
52
55
  } finally {
@@ -56,13 +59,23 @@ describe('account.test.ts', { concurrency: false }, () => {
56
59
 
57
60
  await app.bean.executor.mockCtx(async () => {
58
61
  const user = await app.bean.user.findOneById(fixture!.userId);
59
- assert.equal(user?.name, `${fixture!.name}-updated`);
62
+ assert.equal(user?.name, fixture!.name);
63
+ assert.equal(user?.displayName, 'Updated display name');
60
64
  assert.ok(user);
61
65
  assert.equal('ignored' in user, false);
66
+ assert.equal(
67
+ await app.bean.user.findOneByName(`${fixture!.name}-must-not-rename`),
68
+ undefined,
69
+ );
62
70
  });
63
71
 
64
72
  await app.bean.executor.mockCtx(async () => {
65
- await app.bean.passport.signinSystem('mock', -10002 as any, `${fixture!.name}-updated`);
73
+ const jwt = await app.bean.auth.authenticate('auth-simple:simple', {
74
+ clientName: 'default',
75
+ state: { intention: 'login' },
76
+ clientOptions: { username: fixture!.name, password: 'initial-password' },
77
+ });
78
+ assert.ok(jwt?.accessToken);
66
79
  try {
67
80
  const result = await app.bean.executor.performAction('post', passwordChangePath, {
68
81
  innerAccess: false,
@@ -91,6 +104,105 @@ describe('account.test.ts', { concurrency: false }, () => {
91
104
  }
92
105
  });
93
106
 
107
+ it('retains omitted displayName, clears null or blank, and validates the trimmed limit', async () => {
108
+ let fixture: IAccountFixture | undefined;
109
+ try {
110
+ fixture = await createFixture();
111
+ const displayName = 'd'.repeat(100);
112
+ const updated = await updateProfile(fixture, { displayName: ` ${displayName} ` });
113
+ assert.equal(updated.displayName, displayName);
114
+ assert.equal(updated.name, fixture.name);
115
+
116
+ const retained = await updateProfile(fixture, { locale: 'en-us' });
117
+ assert.equal(retained.displayName, displayName);
118
+
119
+ await app.bean.executor.mockCtx(async () => {
120
+ await app.bean.passport.signinSystem('mock', -10005 as any, fixture!.name);
121
+ try {
122
+ const account = app.scope('home-user').service.account;
123
+ const result = await account.updateProfile({
124
+ name: `${fixture!.name}-service-must-not-rename`,
125
+ } as unknown as Parameters<typeof account.updateProfile>[0]);
126
+ assert.equal(result.name, fixture!.name);
127
+ assert.equal(result.displayName, displayName);
128
+ } finally {
129
+ await app.bean.passport.signout();
130
+ }
131
+ });
132
+
133
+ for (const invalid of ['d'.repeat(101), ` ${'d'.repeat(101)} `]) {
134
+ await assert.rejects(
135
+ () => updateProfile(fixture!, { displayName: invalid }),
136
+ (error: { code?: number }) => error.code === 422,
137
+ );
138
+ }
139
+ await app.bean.executor.mockCtx(async () => {
140
+ const user = await app.bean.user.findOneById(fixture!.userId);
141
+ assert.equal(user?.name, fixture!.name);
142
+ assert.equal(user?.displayName, displayName);
143
+ });
144
+
145
+ for (const empty of [null, ' \t\n ', '']) {
146
+ await updateProfile(fixture, { displayName: 'Before clearing' });
147
+ const cleared = await updateProfile(fixture, { displayName: empty });
148
+ assert.equal(cleared.name, fixture.name);
149
+ assert.equal(cleared.displayName, undefined);
150
+ await app.bean.executor.mockCtx(async () => {
151
+ const user = await app.bean.user.findOneById(fixture!.userId);
152
+ assert.ok(user);
153
+ assert.ok(user.displayName == null);
154
+ await app.bean.passport.signinSystem('mock', -10006 as any, fixture!.name);
155
+ try {
156
+ const current = await app.bean.executor.performAction('get', currentPath, {
157
+ innerAccess: false,
158
+ });
159
+ assert.equal(current.displayName, undefined);
160
+ } finally {
161
+ await app.bean.passport.signout();
162
+ }
163
+ });
164
+ }
165
+ } finally {
166
+ if (fixture) await removeFixture(fixture);
167
+ }
168
+ });
169
+
170
+ it('allows two users to share displayName while retaining their original username logins', async () => {
171
+ const fixtures: IAccountFixture[] = [];
172
+ const displayName = `Shared ${crypto.randomUUID()}`;
173
+ try {
174
+ fixtures.push(await createFixture());
175
+ fixtures.push(await createFixture());
176
+ for (const fixture of fixtures) {
177
+ const updated = await updateProfile(fixture, { displayName });
178
+ assert.equal(updated.name, fixture.name);
179
+ assert.equal(updated.displayName, displayName);
180
+ }
181
+ for (const fixture of fixtures) {
182
+ await app.bean.executor.mockCtx(async () => {
183
+ try {
184
+ const jwt = await app.bean.auth.authenticate('auth-simple:simple', {
185
+ clientName: 'default',
186
+ state: { intention: 'login' },
187
+ clientOptions: { username: fixture.name, password: 'initial-password' },
188
+ });
189
+ assert.ok(jwt?.accessToken);
190
+ assert.equal(app.bean.passport.currentUser?.id.toString(), fixture.userId);
191
+ const current = await app.bean.executor.performAction('get', currentPath, {
192
+ innerAccess: false,
193
+ });
194
+ assert.equal(current.name, fixture.name);
195
+ assert.equal(current.displayName, displayName);
196
+ } finally {
197
+ await app.bean.passport.signout();
198
+ }
199
+ });
200
+ }
201
+ } finally {
202
+ for (const fixture of fixtures.reverse()) await removeFixture(fixture);
203
+ }
204
+ });
205
+
94
206
  it('accepts a freshly uploaded avatar during profile update', async () => {
95
207
  let fixture: IAccountFixture | undefined;
96
208
  let imageId: string | number | undefined;
@@ -133,7 +245,6 @@ describe('account.test.ts', { concurrency: false }, () => {
133
245
  const updated = await app.bean.executor.performAction('patch', profilePath, {
134
246
  innerAccess: false,
135
247
  body: {
136
- name: fixture!.name,
137
248
  avatar: uploadedUrl,
138
249
  },
139
250
  });
@@ -145,7 +256,7 @@ describe('account.test.ts', { concurrency: false }, () => {
145
256
 
146
257
  const preserved = await app.bean.executor.performAction('patch', profilePath, {
147
258
  innerAccess: false,
148
- body: { name: fixture!.name },
259
+ body: {},
149
260
  });
150
261
  assert.equal(preserved.avatar, uploadedUrl);
151
262
 
@@ -153,7 +264,6 @@ describe('account.test.ts', { concurrency: false }, () => {
153
264
  const thirdPartyUpdated = await app.bean.executor.performAction('patch', profilePath, {
154
265
  innerAccess: false,
155
266
  body: {
156
- name: fixture!.name,
157
267
  avatar: thirdPartyAvatar,
158
268
  },
159
269
  });
@@ -161,7 +271,7 @@ describe('account.test.ts', { concurrency: false }, () => {
161
271
 
162
272
  const cleared = await app.bean.executor.performAction('patch', profilePath, {
163
273
  innerAccess: false,
164
- body: { name: fixture!.name, avatar: null },
274
+ body: { avatar: null },
165
275
  });
166
276
  assert.equal(cleared.avatar, undefined);
167
277
  } finally {
@@ -214,6 +324,20 @@ describe('account.test.ts', { concurrency: false }, () => {
214
324
  });
215
325
  });
216
326
 
327
+ async function updateProfile(fixture: IAccountFixture, body: Record<string, unknown>) {
328
+ return await app.bean.executor.mockCtx(async () => {
329
+ await app.bean.passport.signinSystem('mock', -10007 as any, fixture.name);
330
+ try {
331
+ return await app.bean.executor.performAction('patch', profilePath, {
332
+ innerAccess: false,
333
+ body,
334
+ });
335
+ } finally {
336
+ await app.bean.passport.signout();
337
+ }
338
+ });
339
+ }
340
+
217
341
  async function createFixture(): Promise<IAccountFixture> {
218
342
  return await app.bean.executor.mockCtx(async () => {
219
343
  const name = `account-test-${crypto.randomUUID()}`;
@@ -244,6 +368,7 @@ async function removeFixture(fixture: IAccountFixture): Promise<void> {
244
368
  await app.bean.executor.mockCtx(async () => {
245
369
  await app.scope('a-auth').model.auth.deleteById(fixture.authId);
246
370
  await app.scope('auth-simple').model.authSimple.deleteById(fixture.authSimpleId);
371
+ await app.scope('home-user').model.roleUser.delete({ userId: fixture.userId });
247
372
  await app.bean.user.removeById(fixture.userId);
248
373
  });
249
374
  }
@@ -110,25 +110,38 @@ describe('accountOpenapi.test.ts', () => {
110
110
  assert.equal(property?.rest?.form?.required, true);
111
111
  }
112
112
  }
113
+ if (expectation.action === 'current' || expectation.action === 'updateProfile') {
114
+ const response = operation.responses?.['200'];
115
+ assert.ok(response && 'content' in response);
116
+ const body = resolveSchema(doc, response.content?.['application/json']?.schema);
117
+ const current = resolveSchema(doc, body.properties?.data);
118
+ assert.deepEqual(inlineSchema(current.properties?.displayName).type, ['string', 'null']);
119
+ assert.equal(current.required?.includes('displayName') ?? false, false);
120
+ assert.equal(inlineSchema(current.properties?.name).type, 'string');
121
+ assert.ok(current.required?.includes('name'));
122
+ }
113
123
  if (expectation.action === 'updateProfile') {
114
124
  const schemaResolved = requestSchema(doc, operation);
115
125
  assert.deepEqual(Object.keys(schemaResolved?.properties ?? {}), [
116
- 'name',
126
+ 'displayName',
117
127
  'avatar',
118
128
  'locale',
119
129
  'tz',
120
130
  ]);
121
- assert.deepEqual(schemaResolved?.required, ['name']);
131
+ assert.deepEqual(schemaResolved.required ?? [], []);
132
+ assert.equal(schemaResolved.properties?.name, undefined);
122
133
  for (const [field, title] of [
123
- ['name', 'home-user::AccountDisplayName'],
134
+ ['displayName', 'home-user::AccountDisplayName'],
124
135
  ['avatar', 'home-user::AccountAvatar'],
125
136
  ['locale', 'home-user::AccountLocale'],
126
137
  ['tz', 'home-user::AccountTimezone'],
127
138
  ]) {
128
139
  assert.equal(inlineSchema(schemaResolved.properties?.[field]).title?.toString(), title);
129
140
  }
130
- assert.equal(inlineSchema(schemaResolved.properties?.name).minLength, 1);
131
- assert.equal(inlineSchema(schemaResolved.properties?.name).maxLength, 100);
141
+ const displayName = inlineSchema(schemaResolved.properties?.displayName);
142
+ assert.deepEqual(displayName.type, ['string', 'null']);
143
+ assert.equal(displayName.maxLength, 100);
144
+ assert.ok(!displayName.minLength);
132
145
  assert.deepEqual(inlineSchema(schemaResolved.properties?.avatar).type, [
133
146
  'string',
134
147
  'null',
@@ -154,6 +167,20 @@ function inlineSchema(
154
167
  return schema;
155
168
  }
156
169
 
170
+ function resolveSchema(
171
+ doc: OpenAPIObject,
172
+ schema: ReferenceObject | SchemaObject | undefined,
173
+ ): ISchemaObjectExtensionField {
174
+ assert.ok(schema);
175
+ if ('$ref' in schema && typeof schema.$ref === 'string') {
176
+ return resolveSchema(
177
+ doc,
178
+ doc.components?.schemas?.[schema.$ref.replace('#/components/schemas/', '')],
179
+ );
180
+ }
181
+ return inlineSchema(schema);
182
+ }
183
+
157
184
  function requestSchema(
158
185
  doc: OpenAPIObject,
159
186
  operation: OperationObject,
@@ -1,5 +1,5 @@
1
1
  import type { SchemaObject } from 'openapi3-ts/oas31';
2
- import type { IPassport } from 'vona-module-a-user';
2
+ import type { IAuthUserProfile, IPassport } from 'vona-module-a-user';
3
3
 
4
4
  import assert from 'node:assert';
5
5
  import { describe, it, mock } from 'node:test';
@@ -12,6 +12,7 @@ function createPassport(): IPassport {
12
12
  user: {
13
13
  id: 'passport-response-user',
14
14
  name: 'Passport response fixture',
15
+ displayName: 'Passport display name',
15
16
  activated: true,
16
17
  accountStatus: 'active',
17
18
  instanceId: 'untrusted-user-instance',
@@ -38,12 +39,26 @@ describe('passport.test.ts', { concurrency: false }, () => {
38
39
  assert.ok(result);
39
40
  assert.equal(result.instanceId, instanceId);
40
41
  assert.equal(result.user.id, passport.user!.id);
42
+ assert.equal(result.user.name, passport.user!.name);
43
+ assert.equal(result.user.displayName, passport.user!.displayName);
41
44
  assert.equal(result.auth, passport.auth);
42
45
  assert.equal(result.roles, passport.roles);
43
46
  assert.equal('instanceId' in result.user, false);
44
47
  });
45
48
  });
46
49
 
50
+ it('current leaves an absent displayName unset instead of falling back to the username', async () => {
51
+ await app.bean.executor.mockCtx(async () => {
52
+ const passport = createPassport();
53
+ delete passport.user!.displayName;
54
+ app.ctx.state.passport = passport;
55
+ const result = await app.bean._getBean(ControllerPassport).current();
56
+ assert.ok(result);
57
+ assert.equal(result.user.name, passport.user!.name);
58
+ assert.equal(result.user.displayName, undefined);
59
+ });
60
+ });
61
+
47
62
  it('current returns explicit null when the request has no instance', async () => {
48
63
  await app.bean.executor.mockCtx(
49
64
  async () => {
@@ -98,6 +113,8 @@ describe('passport.test.ts', { concurrency: false }, () => {
98
113
  assert.equal(response.passport.instanceId, app.ctx.instance?.id ?? null);
99
114
  assert.ok(Object.hasOwn(response.passport, 'instanceId'));
100
115
  assert.equal(response.passport.user.id, 'passport-response-user');
116
+ assert.equal(response.passport.user.displayName, 'Passport display name');
117
+ assert.equal(response.passport.user.name, 'Passport response fixture');
101
118
  assert.equal(response.jwt, jwt);
102
119
  }
103
120
  assert.equal(authenticate.mock.callCount(), 4);
@@ -112,6 +129,144 @@ describe('passport.test.ts', { concurrency: false }, () => {
112
129
  }
113
130
  });
114
131
 
132
+ it('userOfProfile maps only an actual trimmed provider displayName without name fallbacks', async () => {
133
+ const profile: IAuthUserProfile = {
134
+ id: 'display-name-profile',
135
+ username: 'provider-username',
136
+ name: { givenName: 'Given', familyName: 'Family' },
137
+ emails: [{ value: 'profile@example.test' }],
138
+ photos: [{ value: 'https://example.test/avatar.png' }],
139
+ };
140
+ for (const displayName of [undefined, '', ' \t ', ' Provider display name ']) {
141
+ await app.bean.executor.mockCtx(async () => {
142
+ const user = await app.scope('home-user').service.userAdapter.userOfProfile({
143
+ ...profile,
144
+ ...(displayName === undefined ? {} : { displayName }),
145
+ });
146
+ assert.equal(user.name, profile.username);
147
+ assert.equal(user.displayName, displayName?.trim() || undefined);
148
+ assert.equal(user.email, profile.emails![0].value);
149
+ assert.equal(user.avatar, profile.photos![0].value);
150
+ });
151
+ }
152
+ });
153
+
154
+ it('OAuth creation maps displayName but existing login and linking preserve account profile fields', async () => {
155
+ const suffix = crypto.randomUUID();
156
+ const oauthName = `display-oauth-${suffix}`;
157
+ const linkedName = `display-linked-${suffix}`;
158
+ const profileId = `display-profile-${suffix}`;
159
+ const linkedProfileId = `display-link-${suffix}`;
160
+ const profile: IAuthUserProfile = {
161
+ id: profileId,
162
+ username: oauthName,
163
+ displayName: ' Initial provider display ',
164
+ confirmed: true,
165
+ };
166
+ let oauthUserId: string | undefined;
167
+ let linkedUserId: string | undefined;
168
+ try {
169
+ await app.bean.executor.mockCtx(async () => {
170
+ const provider = await app.bean.authProvider.get({
171
+ providerName: 'auth-oauth:oauth',
172
+ clientName: 'github',
173
+ });
174
+ const passport = await app
175
+ .scope('a-auth')
176
+ .service.auth.issuePassport(profile, provider, {});
177
+ assert.ok(passport.user);
178
+ oauthUserId = passport.user.id.toString();
179
+ assert.equal(passport.user.name, oauthName);
180
+ assert.equal(passport.user.displayName, 'Initial provider display');
181
+ await app.bean.user.updateById(oauthUserId, { displayName: 'Account-owned display' });
182
+ });
183
+
184
+ await app.bean.executor.mockCtx(async () => {
185
+ const provider = await app.bean.authProvider.get({
186
+ providerName: 'auth-oauth:oauth',
187
+ clientName: 'github',
188
+ });
189
+ const passport = await app.scope('a-auth').service.auth.issuePassport(
190
+ { ...profile, username: `${oauthName}-changed`, displayName: 'Changed provider display' },
191
+ provider,
192
+ {},
193
+ {
194
+ intention: 'login',
195
+ authProviderId: provider.id,
196
+ instanceName: app.ctx.instanceName!,
197
+ locale: app.ctx.locale,
198
+ tz: app.ctx.tz,
199
+ },
200
+ );
201
+ assert.equal(passport.user?.id.toString(), oauthUserId);
202
+ assert.equal(passport.user?.name, oauthName);
203
+ assert.equal(passport.user?.displayName, 'Account-owned display');
204
+ const persisted = await app.bean.user.findOneById(oauthUserId!);
205
+ assert.equal(persisted?.displayName, 'Account-owned display');
206
+ });
207
+
208
+ await app.bean.executor.mockCtx(async () => {
209
+ const user = await app.bean.user.register({ name: linkedName }, true);
210
+ linkedUserId = user.id.toString();
211
+ });
212
+ await app.bean.executor.mockCtx(async () => {
213
+ const user = await app.bean.user.findOneById(linkedUserId!);
214
+ assert.ok(user);
215
+ await app.bean.passport.setCurrent({ user });
216
+ try {
217
+ const provider = await app.bean.authProvider.get({
218
+ providerName: 'auth-oauth:oauth',
219
+ clientName: 'github',
220
+ });
221
+ const passport = await app.scope('a-auth').service.auth.issuePassport(
222
+ { ...profile, id: linkedProfileId },
223
+ provider,
224
+ {},
225
+ {
226
+ intention: 'associate',
227
+ authProviderId: provider.id,
228
+ instanceName: app.ctx.instanceName!,
229
+ locale: app.ctx.locale,
230
+ tz: app.ctx.tz,
231
+ },
232
+ );
233
+ assert.equal(passport.user?.id.toString(), linkedUserId);
234
+ assert.equal(passport.user?.name, linkedName);
235
+ assert.equal(passport.user?.displayName, undefined);
236
+ const persisted = await app.bean.user.findOneById(linkedUserId!);
237
+ assert.equal(persisted?.name, linkedName);
238
+ assert.equal(persisted?.displayName, undefined);
239
+ const auth = await app.scope('a-auth').model.auth.get({
240
+ authProviderId: provider.id,
241
+ profileId: linkedProfileId,
242
+ });
243
+ assert.equal(auth?.userId.toString(), linkedUserId);
244
+ } finally {
245
+ await app.bean.passport.setCurrent(undefined);
246
+ }
247
+ });
248
+ } finally {
249
+ await app.bean.executor.mockCtx(async () => {
250
+ const provider = await app.bean.authProvider.get({
251
+ providerName: 'auth-oauth:oauth',
252
+ clientName: 'github',
253
+ });
254
+ for (const id of [linkedProfileId, profileId]) {
255
+ await app
256
+ .scope('a-auth')
257
+ .model.auth.delete({ authProviderId: provider.id, profileId: id });
258
+ }
259
+ for (const name of [linkedName, oauthName]) {
260
+ const user = await app.bean.user.findOneByName(name);
261
+ if (user) {
262
+ await app.scope('home-user').model.roleUser.delete({ userId: user.id });
263
+ await app.bean.user.removeById(user.id);
264
+ }
265
+ }
266
+ });
267
+ }
268
+ });
269
+
115
270
  it('OpenAPI exposes a required nullable table identity on current and every Passport/JWT response', async () => {
116
271
  await app.bean.executor.mockCtx(async () => {
117
272
  const actions = [
@@ -148,6 +303,9 @@ describe('passport.test.ts', { concurrency: false }, () => {
148
303
  const instanceId = resolve(passport.properties?.instanceId as SchemaObject);
149
304
  const user = resolve(passport.properties?.user as SchemaObject);
150
305
  const userId = resolve(user.properties?.id as SchemaObject);
306
+ const displayName = resolve(user.properties?.displayName as SchemaObject);
307
+ assert.deepEqual(displayName.type, ['string', 'null']);
308
+ assert.equal(user.required?.includes('displayName') ?? false, false);
151
309
  const types = (schema: SchemaObject): string[] => {
152
310
  if (schema.anyOf)
153
311
  return schema.anyOf.flatMap(item => types(resolve(item as SchemaObject)));
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-rbac",
3
- "version": "5.0.17",
3
+ "version": "5.0.18",
4
4
  "description": "",
5
5
  "keywords": [
6
6
  "Vona Module"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-suite-a-cabloy",
3
- "version": "5.1.69",
3
+ "version": "5.1.70",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "author": "",
@@ -10,7 +10,7 @@
10
10
  "vona-module-a-cabloy": "workspace:^",
11
11
  "vona-module-a-layoutprofile": "^5.0.2",
12
12
  "vona-module-a-markdown": "^5.0.3",
13
- "vona-module-a-rbac": "^5.0.17",
13
+ "vona-module-a-rbac": "^5.0.18",
14
14
  "vona-module-a-socket": "workspace:^",
15
15
  "vona-module-a-ssr": "workspace:^",
16
16
  "vona-module-a-ssrhmr": "workspace:^"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-hmr",
3
- "version": "5.1.15",
3
+ "version": "5.1.16",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "keywords": [
@@ -34,7 +34,7 @@ export class ServiceHmr extends BeanBase {
34
34
  if (sceneName === '_error') {
35
35
  await this._reloadError(moduleName!, fileModule.errors);
36
36
  } else if (sceneName === '_locale') {
37
- await this._reloadLocale(moduleName!, fileModule.default, file);
37
+ await this._reloadLocale();
38
38
  } else if (sceneName === '_config') {
39
39
  await this._reloadConfig(moduleName!, fileModule.config);
40
40
  } else if (sceneName === '_constant') {
@@ -50,17 +50,10 @@ export class ServiceHmr extends BeanBase {
50
50
  deepExtend(this.app.meta.error.ebErrors[moduleName], errors);
51
51
  }
52
52
 
53
- private async _reloadLocale(moduleName: string, moduleLocales: {}, file: string) {
54
- const locale = path.basename(file, '.ts');
55
- // localeModules
56
- if (!this.app.meta.localeModules[moduleName]) this.app.meta.localeModules[moduleName] = {};
57
- this.app.meta.localeModules[moduleName][locale] = Object.assign(
58
- {},
59
- moduleLocales,
60
- this.app.meta.hmrCacheLocaleModules[moduleName]?.[locale],
61
- );
62
- // openapi
63
- await this.bean.openapi.clearAllCaches();
53
+ private async _reloadLocale() {
54
+ // Ordinary dictionaries also contribute to the ordered global fallback map.
55
+ // Restart to remove stale values and restore project/provider precedence in both maps.
56
+ this.app.bean.worker.reload();
64
57
  }
65
58
 
66
59
  private async _reloadConfig(moduleName: string, config: TypeModuleResourceConfig) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-permission",
3
- "version": "5.1.33",
3
+ "version": "5.1.34",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "keywords": [