@abtnode/core 1.16.31-beta-52250475 → 1.16.31-beta-a0cc72cf

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/lib/api/team.js CHANGED
@@ -282,7 +282,7 @@ class TeamAPI extends EventEmitter {
282
282
  const now = Date.now();
283
283
  let sessionTtl = SESSION_TTL;
284
284
  let blocklet;
285
- if (teamDid !== nodeInfo.did) {
285
+ if (teamDid !== nodeInfo.did && query?.includeUserSessions) {
286
286
  blocklet = await getBlocklet({ did: teamDid, states: this.states, dataDirs: this.dataDirs });
287
287
  sessionTtl = blocklet.settings?.session?.ttl || SESSION_TTL;
288
288
  }
@@ -310,6 +310,8 @@ class TeamAPI extends EventEmitter {
310
310
  'locale',
311
311
  'tags',
312
312
  'url',
313
+ 'inviter',
314
+ 'generation',
313
315
  'userSessions',
314
316
  // oauth relate fields
315
317
  'sourceProvider',
@@ -32,7 +32,6 @@ const runUserHook = async (label, hookName, args) => {
32
32
  logger.info(`run hook:${hookName}:`, { label, hook });
33
33
 
34
34
  const nodeInfo = await states.node.read();
35
- // FIXME @linchen timeout 应该动态设置或不设置
36
35
  await runScript(hook, [label, hookName].join(':'), {
37
36
  cwd: appDir,
38
37
  env: {
@@ -2372,6 +2372,20 @@ class DiskBlockletManager extends BaseBlockletManager {
2372
2372
  return states.backup.getBlockletBackups({ did });
2373
2373
  }
2374
2374
 
2375
+ async updateInviteSettings({ did, invite }, context) {
2376
+ await states.blockletExtras.setSettings(did, { invite });
2377
+
2378
+ const newState = await this.getBlocklet(did);
2379
+ this.emit(BlockletInternalEvents.appSettingChanged, { appDid: did });
2380
+ this.emit(BlockletEvents.updated, { ...newState, context });
2381
+
2382
+ return newState;
2383
+ }
2384
+
2385
+ getInviteSettings({ did }) {
2386
+ return states.blockletExtras.getSettings(did, 'invite', { enabled: false });
2387
+ }
2388
+
2375
2389
  deleteCache(did) {
2376
2390
  const cache = this.cachedBlocklets.get(did);
2377
2391
  if (cache) {
@@ -4854,6 +4868,8 @@ class FederatedBlockletManager extends DiskBlockletManager {
4854
4868
  'disconnectedAccount',
4855
4869
  'action',
4856
4870
  'sourceAppPid',
4871
+ 'inviter',
4872
+ 'generation',
4857
4873
  ])
4858
4874
  );
4859
4875
  }
package/lib/index.js CHANGED
@@ -377,15 +377,15 @@ function ABTNode(options) {
377
377
  updateBlockletSpaceGateway: blockletManager.updateBlockletSpaceGateway.bind(blockletManager),
378
378
  getBlockletSpaceGateways: blockletManager.getBlockletSpaceGateways.bind(blockletManager),
379
379
 
380
- // auto backup
380
+ // auto backup related
381
381
  updateAutoBackup: blockletManager.updateAutoBackup.bind(blockletManager),
382
-
383
- // blocklet backup record
384
382
  getBlockletBackups: blockletManager.getBlockletBackups.bind(blockletManager),
385
383
 
386
384
  // check update
387
385
  updateAutoCheckUpdate: blockletManager.updateAutoCheckUpdate.bind(blockletManager),
388
386
 
387
+ updateInviteSettings: blockletManager.updateInviteSettings.bind(blockletManager),
388
+
389
389
  // Store
390
390
  getBlockletMeta: StoreUtil.getBlockletMeta,
391
391
  getStoreMeta: StoreUtil.getStoreMeta,
@@ -1,13 +1,15 @@
1
+ const cloneDeep = require('lodash/cloneDeep');
1
2
  const pickBy = require('lodash/pickBy');
2
3
  const get = require('lodash/get');
3
4
  const pick = require('lodash/pick');
4
5
  const uniq = require('lodash/uniq');
5
6
  const { isValid, toAddress } = require('@arcblock/did');
6
- const { PASSPORT_STATUS } = require('@abtnode/constant');
7
+ const { PASSPORT_STATUS, USER_MAX_INVITE_DEPTH } = require('@abtnode/constant');
7
8
  const { BaseState } = require('@abtnode/models');
8
9
  const { Sequelize, Op } = require('sequelize');
9
10
  const { updateConnectedAccount } = require('@abtnode/util/lib/user');
10
11
  const { LOGIN_PROVIDER } = require('@blocklet/constant');
12
+ const logger = require('@abtnode/logger')('@abtnode/core:states:user');
11
13
 
12
14
  const { validateOwner } = require('../util');
13
15
  const { loginSchema, disconnectAccountSchema } = require('../validators/user');
@@ -72,6 +74,7 @@ class User extends ExtendBase {
72
74
  // create user
73
75
  await this.insert({
74
76
  ...user,
77
+ ...(await this._extractInviteInfo(user)),
75
78
  sourceProvider: user.sourceProvider || LOGIN_PROVIDER.WALLET,
76
79
  approved: !!user.approved,
77
80
  });
@@ -94,12 +97,20 @@ class User extends ExtendBase {
94
97
 
95
98
  // FIXME: @wangshijun wrap these in a transaction
96
99
  async updateUser(did, updates) {
97
- const exist = await super.count({ did });
100
+ const exist = await super.findOne({ did });
98
101
  if (!exist) {
99
102
  throw new Error(`user does not exist: ${did}`);
100
103
  }
101
104
 
102
- await super.update({ did }, { $set: updates });
105
+ // Allow to update inviter only when inviter is not set
106
+ const pending = cloneDeep(updates);
107
+ if (exist.inviter) {
108
+ delete pending.inviter;
109
+ } else {
110
+ Object.assign(pending, await this._extractInviteInfo({ did, ...pending }));
111
+ }
112
+
113
+ await super.update({ did }, { $set: pending });
103
114
  await Promise.all(
104
115
  (get(updates, 'passports') || [])
105
116
  .filter((x) => x.id)
@@ -185,16 +196,33 @@ class User extends ExtendBase {
185
196
  /**
186
197
  * Get blocklet service user list
187
198
  */
188
- // eslint-disable-next-line require-await
189
199
  async getUsers({ query, sort, paging } = {}) {
190
200
  const where = {};
191
- const { approved, role, search, tags, includeTags, includePassports, includeUserSessions } = query || {};
201
+ const replacements = {};
202
+
203
+ const {
204
+ approved,
205
+ role,
206
+ search,
207
+ tags,
208
+ invitee,
209
+ inviter,
210
+ generation, // 0 - unlimited, 1 - invited by inviter, 2 - invited by another
211
+ includeTags,
212
+ includePassports,
213
+ includeUserSessions,
214
+ } = query || {};
192
215
  const shouldIncludeTag = (tags && tags.length) || includeTags;
193
216
 
194
217
  if (isNullOrUndefined(approved) === false) {
195
218
  where.approved = approved;
196
219
  }
197
220
 
221
+ const sorting = pickBy(sort, (x) => !isNullOrUndefined(x));
222
+ if (!Object.keys(sorting).length) {
223
+ sorting.createdAt = -1;
224
+ }
225
+
198
226
  if (search) {
199
227
  if (search.length > 50) {
200
228
  throw new Error('the length of search text should not more than 50');
@@ -206,28 +234,92 @@ class User extends ExtendBase {
206
234
  }
207
235
  }
208
236
 
209
- const replacements = {};
237
+ if (inviter && invitee) {
238
+ throw new Error('You can not query by inviter and invitee at the same time');
239
+ }
210
240
 
211
- if (role && role !== '$all' && !where.did) {
212
- replacements.status = PASSPORT_STATUS.VALID;
213
- if (role === '$none') {
214
- where.did = {
215
- [Op.notIn]: Sequelize.literal('(SELECT DISTINCT userDid FROM passports WHERE status = :status)'),
216
- };
217
- } else {
218
- replacements.role = role;
219
- where.did = {
220
- [Op.in]: Sequelize.literal(
221
- '(SELECT DISTINCT userDid FROM passports WHERE name = :role AND status = :status)'
222
- ),
223
- };
241
+ let total = 0;
242
+ if (!where.did) {
243
+ // handle descendant query
244
+ if (inviter) {
245
+ if (isValid(inviter) === false) {
246
+ throw new Error('inviter did invalid');
247
+ }
248
+ const exist = await this.model.findByPk(toAddress(inviter), { attributes: ['did', 'generation'] });
249
+ if (!exist) {
250
+ throw new Error(`inviter not found: ${inviter}`);
251
+ }
252
+
253
+ try {
254
+ const { pageSize: size = 20, page = 1 } = paging || {};
255
+ const pageSize = Math.min(100, size);
256
+ const offset = (page - 1) * pageSize;
257
+ // LIMIT ${pageSize} OFFSET ${offset}
258
+ const subQuery = `
259
+ WITH RECURSIVE UserTree(did,inviter,generation,createdAt) AS (
260
+ SELECT did,inviter,generation,createdAt FROM users WHERE inviter="${exist.did}"
261
+ UNION ALL
262
+ SELECT child.did,child.inviter,child.generation,child.createdAt FROM users AS child INNER JOIN UserTree AS parent ON (child.inviter=parent.did) ORDER BY child.createdAt DESC
263
+ )
264
+ SELECT did,inviter,generation FROM UserTree ${generation > 0 ? `WHERE generation=${(exist.generation > 0 ? exist.generation : 0) + generation}` : ''}`.trim();
265
+ const children = await this.query(subQuery);
266
+ total = children.length;
267
+ where.did = children.slice(offset, offset + pageSize).map((x) => x.did);
268
+ } catch (err) {
269
+ console.error('Failed to get descendants', err);
270
+ where.did = [];
271
+ }
224
272
  }
225
- }
226
273
 
227
- const sorting = pickBy(sort, (x) => !isNullOrUndefined(x));
228
- if (!Object.keys(sorting).length) {
229
- sorting.createdAt = -1;
274
+ // handle ancestor query
275
+ if (invitee) {
276
+ if (isValid(invitee) === false) {
277
+ throw new Error('invitee did invalid');
278
+ }
279
+ const exist = await this.model.findByPk(toAddress(invitee), { attributes: ['did', 'generation'] });
280
+ if (!exist) {
281
+ throw new Error(`invitee not found: ${invitee}`);
282
+ }
283
+
284
+ try {
285
+ const subQuery = `
286
+ WITH RECURSIVE UserTree(did,inviter,generation) AS (
287
+ SELECT did,inviter,generation FROM users WHERE did="${exist.did}"
288
+ UNION ALL
289
+ SELECT
290
+ inviter,
291
+ (SELECT inviter FROM users AS parent WHERE parent.did=child.inviter),
292
+ (SELECT generation FROM users AS parent WHERE parent.did=child.inviter)
293
+ FROM UserTree AS child
294
+ WHERE inviter IS NOT NULL
295
+ )
296
+ SELECT did,inviter,generation FROM UserTree`.trim();
297
+ const children = await this.query(subQuery);
298
+ where.did = children.map((x) => x.did).filter((x) => x !== exist.did);
299
+ } catch (err) {
300
+ console.error('Failed to get ancestors', err);
301
+ where.did = [];
302
+ }
303
+ }
304
+
305
+ // handle role/status query
306
+ if (role && role !== '$all') {
307
+ replacements.status = PASSPORT_STATUS.VALID;
308
+ if (role === '$none') {
309
+ where.did = {
310
+ [Op.notIn]: Sequelize.literal('(SELECT DISTINCT userDid FROM passports WHERE status = :status)'),
311
+ };
312
+ } else {
313
+ replacements.role = role;
314
+ where.did = {
315
+ [Op.in]: Sequelize.literal(
316
+ '(SELECT DISTINCT userDid FROM passports WHERE name = :role AND status = :status)'
317
+ ),
318
+ };
319
+ }
320
+ }
230
321
  }
322
+
231
323
  const include = [];
232
324
  if (shouldIncludeTag) {
233
325
  include.push(this.getTagInclude(tags));
@@ -250,7 +342,7 @@ class User extends ExtendBase {
250
342
  }
251
343
 
252
344
  const result = await this.paginate({ where, include, replacements }, sorting, paging);
253
- return result;
345
+ return { list: result.list, paging: { ...result.paging, total: total || result.paging.total } };
254
346
  }
255
347
 
256
348
  // eslint-disable-next-line require-await
@@ -371,10 +463,10 @@ class User extends ExtendBase {
371
463
  * @param {string} user.did
372
464
  * @param {string} user.pk
373
465
  * @param {ConnectedAccount} user.connectedAccount
374
- * @param {passport} user.passport
375
466
  * @param {string} user.fullName - user profile's name
376
467
  * @param {string} user.avatar - url of user's avatar, eg: bn://avatar/7f8848569405f8cdf8b1b2788ebf7d0f.jpg
377
468
  * @param {string} user.locale - locale
469
+ * @param {string} user.inviter - inviter
378
470
  * @param {Object} [user.extra] - extra data of user
379
471
  * @param {string} user.lastLoginIp - lastLoginIp
380
472
  * @param {('owner'|'admin'|'member'|'guest'|string)} user.role - deprecated user's role
@@ -402,6 +494,8 @@ class User extends ExtendBase {
402
494
  'extra',
403
495
  'lastLoginIp',
404
496
  'remark',
497
+ 'inviter',
498
+ 'generation',
405
499
  'sourceAppPid',
406
500
  ]),
407
501
  lastLoginAt: now,
@@ -413,13 +507,15 @@ class User extends ExtendBase {
413
507
  updates.sourceAppPid = null;
414
508
  }
415
509
 
510
+ Object.assign(updates, await this._extractInviteInfo(raw));
511
+
416
512
  if (exist) {
417
- // HACK: sourceAppPid 不能更新
418
- if (updates.sourceAppPid) {
419
- delete updates.sourceAppPid;
420
- }
421
- // 登录不再更新 locale
513
+ // immutable fields
422
514
  delete updates.locale;
515
+ delete updates.sourceAppPid;
516
+ delete updates.inviter;
517
+ delete updates.generation;
518
+
423
519
  // update user, connectedAccount, passport
424
520
  updates.connectedAccounts = updateConnectedAccount(exist.connectedAccounts, user.connectedAccount);
425
521
  updated = await this.updateUser(exist.did, updates);
@@ -437,6 +533,49 @@ class User extends ExtendBase {
437
533
  return { ...updated, _action: exist ? 'update' : 'add' };
438
534
  }
439
535
 
536
+ async _extractInviteInfo(raw) {
537
+ const info = {};
538
+
539
+ if (raw.inviter) {
540
+ // sybil-attack
541
+ if (isValid(raw.inviter)) {
542
+ const inviterId = toAddress(raw.inviter);
543
+ const inviter = await this.model.findByPk(inviterId, { attributes: ['did', 'generation'] });
544
+ if (inviter) {
545
+ // circle preventing
546
+ const { list: ancestors } = await this.getUsers({ query: { invitee: inviterId } });
547
+ const hasCircle = ancestors.some((x) => x.did === raw.did);
548
+ if (hasCircle) {
549
+ logger.warn('Set inviter result in cycle is not allowed', raw);
550
+ info.inviter = null;
551
+ } else {
552
+ info.inviter = inviterId;
553
+ info.generation = inviter.generation + 1;
554
+ }
555
+ } else {
556
+ logger.warn('Set inviter to non-exist user is not allowed', raw);
557
+ }
558
+ } else {
559
+ logger.warn('Set inviter to invalid did is not allowed', raw);
560
+ info.inviter = null;
561
+ }
562
+
563
+ // anti-land-attack
564
+ if (info.inviter === raw.did) {
565
+ logger.warn('Set inviter to self is not allowed', raw);
566
+ info.inviter = null;
567
+ }
568
+ }
569
+ if (!info.inviter) {
570
+ info.generation = 0;
571
+ }
572
+ if (info.generation > USER_MAX_INVITE_DEPTH) {
573
+ throw new Error('You have exceeded max user invite chain length');
574
+ }
575
+
576
+ return info;
577
+ }
578
+
440
579
  async disconnectUserAccount(raw) {
441
580
  const { error, value: connectedAccount } = disconnectAccountSchema.validate(raw);
442
581
  if (error) {
@@ -357,9 +357,13 @@ const setupAppOwner = async ({ node, sessionId, justCreate = false, context }) =
357
357
  };
358
358
  };
359
359
 
360
- const getLauncherSession = async ({ launcherUrl, launcherSessionId, external = true }) => {
360
+ const getLauncherSession = async ({ launcherUrl, launcherSessionId, external = true }, context) => {
361
361
  const info = await states.node.read();
362
- const result = await getLauncherSessionRaw(info.sk, { launcherUrl, launcherSessionId });
362
+ const result = await getLauncherSessionRaw(info.sk, {
363
+ launcherUrl,
364
+ launcherSessionId,
365
+ locale: context?.query?.locale,
366
+ });
363
367
 
364
368
  // strip sensitive data if call from external
365
369
  if (external && result.launcherSession) {
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.16.31-beta-52250475",
6
+ "version": "1.16.31-beta-a0cc72cf",
7
7
  "description": "",
8
8
  "main": "lib/index.js",
9
9
  "files": [
@@ -19,43 +19,43 @@
19
19
  "author": "wangshijun <wangshijun2010@gmail.com> (http://github.com/wangshijun)",
20
20
  "license": "Apache-2.0",
21
21
  "dependencies": {
22
- "@abtnode/analytics": "1.16.31-beta-52250475",
23
- "@abtnode/auth": "1.16.31-beta-52250475",
24
- "@abtnode/certificate-manager": "1.16.31-beta-52250475",
25
- "@abtnode/constant": "1.16.31-beta-52250475",
26
- "@abtnode/cron": "1.16.31-beta-52250475",
27
- "@abtnode/logger": "1.16.31-beta-52250475",
28
- "@abtnode/models": "1.16.31-beta-52250475",
29
- "@abtnode/queue": "1.16.31-beta-52250475",
30
- "@abtnode/rbac": "1.16.31-beta-52250475",
31
- "@abtnode/router-provider": "1.16.31-beta-52250475",
32
- "@abtnode/static-server": "1.16.31-beta-52250475",
33
- "@abtnode/timemachine": "1.16.31-beta-52250475",
34
- "@abtnode/util": "1.16.31-beta-52250475",
35
- "@arcblock/did": "1.18.132",
36
- "@arcblock/did-auth": "1.18.132",
37
- "@arcblock/did-ext": "^1.18.132",
22
+ "@abtnode/analytics": "1.16.31-beta-a0cc72cf",
23
+ "@abtnode/auth": "1.16.31-beta-a0cc72cf",
24
+ "@abtnode/certificate-manager": "1.16.31-beta-a0cc72cf",
25
+ "@abtnode/constant": "1.16.31-beta-a0cc72cf",
26
+ "@abtnode/cron": "1.16.31-beta-a0cc72cf",
27
+ "@abtnode/logger": "1.16.31-beta-a0cc72cf",
28
+ "@abtnode/models": "1.16.31-beta-a0cc72cf",
29
+ "@abtnode/queue": "1.16.31-beta-a0cc72cf",
30
+ "@abtnode/rbac": "1.16.31-beta-a0cc72cf",
31
+ "@abtnode/router-provider": "1.16.31-beta-a0cc72cf",
32
+ "@abtnode/static-server": "1.16.31-beta-a0cc72cf",
33
+ "@abtnode/timemachine": "1.16.31-beta-a0cc72cf",
34
+ "@abtnode/util": "1.16.31-beta-a0cc72cf",
35
+ "@arcblock/did": "1.18.135",
36
+ "@arcblock/did-auth": "1.18.135",
37
+ "@arcblock/did-ext": "^1.18.135",
38
38
  "@arcblock/did-motif": "^1.1.13",
39
- "@arcblock/did-util": "1.18.132",
40
- "@arcblock/event-hub": "1.18.132",
41
- "@arcblock/jwt": "^1.18.132",
39
+ "@arcblock/did-util": "1.18.135",
40
+ "@arcblock/event-hub": "1.18.135",
41
+ "@arcblock/jwt": "^1.18.135",
42
42
  "@arcblock/pm2-events": "^0.0.5",
43
- "@arcblock/validator": "^1.18.132",
44
- "@arcblock/vc": "1.18.132",
45
- "@blocklet/constant": "1.16.31-beta-52250475",
46
- "@blocklet/env": "1.16.31-beta-52250475",
47
- "@blocklet/meta": "1.16.31-beta-52250475",
48
- "@blocklet/resolver": "1.16.31-beta-52250475",
49
- "@blocklet/sdk": "1.16.31-beta-52250475",
50
- "@blocklet/store": "1.16.31-beta-52250475",
51
- "@did-space/client": "^0.5.28",
43
+ "@arcblock/validator": "^1.18.135",
44
+ "@arcblock/vc": "1.18.135",
45
+ "@blocklet/constant": "1.16.31-beta-a0cc72cf",
46
+ "@blocklet/env": "1.16.31-beta-a0cc72cf",
47
+ "@blocklet/meta": "1.16.31-beta-a0cc72cf",
48
+ "@blocklet/resolver": "1.16.31-beta-a0cc72cf",
49
+ "@blocklet/sdk": "1.16.31-beta-a0cc72cf",
50
+ "@blocklet/store": "1.16.31-beta-a0cc72cf",
51
+ "@did-space/client": "^0.5.31",
52
52
  "@fidm/x509": "^1.2.1",
53
- "@ocap/mcrypto": "1.18.132",
54
- "@ocap/util": "1.18.132",
55
- "@ocap/wallet": "1.18.132",
53
+ "@ocap/mcrypto": "1.18.135",
54
+ "@ocap/util": "1.18.135",
55
+ "@ocap/wallet": "1.18.135",
56
56
  "@slack/webhook": "^5.0.4",
57
57
  "archiver": "^7.0.1",
58
- "axios": "^1.7.2",
58
+ "axios": "^1.7.5",
59
59
  "axon": "^2.0.3",
60
60
  "chalk": "^4.1.2",
61
61
  "cross-spawn": "^7.0.3",
@@ -81,7 +81,7 @@
81
81
  "p-limit": "^3.1.0",
82
82
  "p-retry": "4.6.1",
83
83
  "read-last-lines": "^1.8.0",
84
- "semver": "^7.3.8",
84
+ "semver": "^7.6.3",
85
85
  "sequelize": "^6.35.0",
86
86
  "shelljs": "^0.8.5",
87
87
  "ssri": "^8.0.1",
@@ -103,5 +103,5 @@
103
103
  "jest": "^29.7.0",
104
104
  "unzipper": "^0.10.11"
105
105
  },
106
- "gitHead": "26155b86f103f9e64fd8a23b7fffdb279c71209c"
106
+ "gitHead": "d1eec814979a4086fc5efd7c719687b76c972ec6"
107
107
  }