@abtnode/blocklet-services 1.16.17-beta-3232a7af → 1.16.17-beta-703fb879

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 (81) hide show
  1. package/api/index.js +2 -0
  2. package/api/libs/connect/session.js +17 -2
  3. package/api/libs/open-graph/emoji.js +80 -0
  4. package/api/libs/open-graph/index.js +14 -1
  5. package/api/routes/federated.js +108 -2
  6. package/api/routes/oauth.js +46 -9
  7. package/api/routes/user-session.js +165 -0
  8. package/api/routes/user.js +21 -0
  9. package/api/services/auth/connect/login.js +13 -2
  10. package/api/services/auth/index.js +17 -8
  11. package/api/services/image/index.js +5 -0
  12. package/api/util/index.js +8 -1
  13. package/api/util/user-session.js +47 -0
  14. package/build/asset-manifest.json +79 -75
  15. package/build/index.html +1 -1
  16. package/build/service-worker.js +1 -1
  17. package/build/service-worker.js.map +1 -1
  18. package/build/static/css/{5982.ac464505.chunk.css → 8702.3ffd8486.chunk.css} +1 -1
  19. package/build/static/js/1148.e5dc907a.chunk.js +2 -0
  20. package/build/static/js/{6711.fa6dfc37.chunk.js → 1359.add26d23.chunk.js} +2 -2
  21. package/build/static/js/{2093.e2fce549.chunk.js → 2093.3192d241.chunk.js} +3 -3
  22. package/build/static/js/{2653.1307684f.chunk.js → 2653.6a6c1c6c.chunk.js} +2 -2
  23. package/build/static/js/2972.2d5268c3.chunk.js +2 -0
  24. package/build/static/js/3025.e2b1dac4.chunk.js +2 -0
  25. package/build/static/js/{9409.7d4a802c.chunk.js → 3033.1bbcc14e.chunk.js} +2 -2
  26. package/build/static/js/{1462.a4d2a902.chunk.js → 3131.f75fa792.chunk.js} +2 -2
  27. package/build/static/js/{3688.6edc63ba.chunk.js → 3688.2cea0226.chunk.js} +2 -2
  28. package/build/static/js/{4023.8cfc3ec6.chunk.js → 4023.a79b0858.chunk.js} +2 -2
  29. package/build/static/js/4076.e73cb63a.chunk.js +2 -0
  30. package/build/static/js/{6737.53cccea9.chunk.js → 4587.5b31e02e.chunk.js} +2 -2
  31. package/build/static/js/4716.1ecc2bb6.chunk.js +2 -0
  32. package/build/static/js/{4802.b2e38d47.chunk.js → 4802.d185b9f1.chunk.js} +2 -2
  33. package/build/static/js/5012.551dbbc3.chunk.js +2 -0
  34. package/build/static/js/5547.5a341867.chunk.js +3 -0
  35. package/build/static/js/5569.a7e151fc.chunk.js +2 -0
  36. package/build/static/js/{1565.f3579441.chunk.js → 5662.919cc4a2.chunk.js} +2 -2
  37. package/build/static/js/{9102.6dab0f45.chunk.js → 5982.d9eb7e90.chunk.js} +2 -2
  38. package/build/static/js/{6139.f7ca5bd6.chunk.js → 6139.cf6eb9b1.chunk.js} +2 -2
  39. package/build/static/js/6473.d89e783e.chunk.js +2 -0
  40. package/build/static/js/{8437.13d39ce4.chunk.js → 6637.b2e35031.chunk.js} +2 -2
  41. package/build/static/js/6658.0a27e04e.chunk.js +2 -0
  42. package/build/static/js/7050.3281ad5f.chunk.js +2 -0
  43. package/build/static/js/{716.cf9ba82c.chunk.js → 716.438b9c22.chunk.js} +3 -3
  44. package/build/static/js/779.99b3f0d2.chunk.js +2 -0
  45. package/build/static/js/7858.bac05f8f.chunk.js +2 -0
  46. package/build/static/js/8622.8478c900.chunk.js +2 -0
  47. package/build/static/js/8641.f2bb0ffc.chunk.js +2 -0
  48. package/build/static/js/{5982.0abe67c3.chunk.js → 8702.957f131a.chunk.js} +2 -2
  49. package/build/static/js/873.d6d31b32.chunk.js +3 -0
  50. package/build/static/js/{8792.fa2c5f5e.chunk.js → 8792.eff43237.chunk.js} +2 -2
  51. package/build/static/js/8944.09a1c53a.chunk.js +2 -0
  52. package/build/static/js/9403.b72e020c.chunk.js +2 -0
  53. package/build/static/js/9596.0e51cd6e.chunk.js +2 -0
  54. package/build/static/js/9900.b66896e6.chunk.js +2 -0
  55. package/build/static/js/{4682.f90d26fd.chunk.js → 9982.4f1ebb7f.chunk.js} +2 -2
  56. package/build/static/js/main.3cf7fedb.js +3 -0
  57. package/package.json +22 -22
  58. package/build/static/js/1480.88c590c2.chunk.js +0 -2
  59. package/build/static/js/1760.374a6e01.chunk.js +0 -2
  60. package/build/static/js/2393.528e5343.chunk.js +0 -2
  61. package/build/static/js/2836.f3dbcb77.chunk.js +0 -2
  62. package/build/static/js/3025.7dc2fe04.chunk.js +0 -2
  63. package/build/static/js/4164.5adbbbba.chunk.js +0 -2
  64. package/build/static/js/4420.05d9cb43.chunk.js +0 -2
  65. package/build/static/js/4716.bd8bed1b.chunk.js +0 -2
  66. package/build/static/js/5176.1963ce79.chunk.js +0 -2
  67. package/build/static/js/5547.e35129c6.chunk.js +0 -3
  68. package/build/static/js/6186.67c279db.chunk.js +0 -2
  69. package/build/static/js/6378.29466d9e.chunk.js +0 -2
  70. package/build/static/js/6576.02f6c898.chunk.js +0 -2
  71. package/build/static/js/6856.2eb98754.chunk.js +0 -2
  72. package/build/static/js/7226.4eca7c02.chunk.js +0 -3
  73. package/build/static/js/941.7757135a.chunk.js +0 -2
  74. package/build/static/js/9620.301493bc.chunk.js +0 -2
  75. package/build/static/js/9899.277d673f.chunk.js +0 -2
  76. package/build/static/js/main.f1e55cd8.js +0 -3
  77. /package/build/static/js/{2093.e2fce549.chunk.js.LICENSE.txt → 2093.3192d241.chunk.js.LICENSE.txt} +0 -0
  78. /package/build/static/js/{5547.e35129c6.chunk.js.LICENSE.txt → 5547.5a341867.chunk.js.LICENSE.txt} +0 -0
  79. /package/build/static/js/{716.cf9ba82c.chunk.js.LICENSE.txt → 716.438b9c22.chunk.js.LICENSE.txt} +0 -0
  80. /package/build/static/js/{7226.4eca7c02.chunk.js.LICENSE.txt → 873.d6d31b32.chunk.js.LICENSE.txt} +0 -0
  81. /package/build/static/js/{main.f1e55cd8.js.LICENSE.txt → main.3cf7fedb.js.LICENSE.txt} +0 -0
package/api/index.js CHANGED
@@ -38,6 +38,7 @@ const createEnvRoutes = require('./routes/env');
38
38
  const createOAuthRoutes = require('./routes/oauth');
39
39
  const createFederatedRoutes = require('./routes/federated');
40
40
  const createUserRoutes = require('./routes/user');
41
+ const createUserSessionRoutes = require('./routes/user-session');
41
42
  const createBlockletRoutes = require('./routes/blocklet');
42
43
  const createConnectRelayRoutes = require('./routes/connect/relay');
43
44
  const createConnectSessionRoutes = require('./routes/connect/session');
@@ -260,6 +261,7 @@ module.exports = function createServer(node, serverOptions = {}) {
260
261
  // API: auth
261
262
  createOAuthRoutes.init(server, node, options);
262
263
  createFederatedRoutes.init(server, node, options);
264
+ createUserSessionRoutes.init(server, node, options);
263
265
  createUserRoutes.init(server, node, options);
264
266
  createEnvRoutes.init(server, node, options);
265
267
  createBlockletRoutes.init(server, node);
@@ -49,6 +49,7 @@ const { transferPassport } = require('../auth/utils');
49
49
  const { migrateAccount, declareAccount } = require('../../services/oauth');
50
50
  const { getTrustedIssuers, getFederatedTrustedIssuers } = require('../../util/blocklet-utils');
51
51
  const { getUserAvatarUrl, migrateAuth0, getFederatedMaster, shouldSyncFederated } = require('../../util/federated');
52
+ const { upsertUserSession } = require('../../util/user-session');
52
53
 
53
54
  const vcTypes = [VC_TYPE_GENERAL_PASSPORT, VC_TYPE_NODE_PASSPORT];
54
55
 
@@ -210,6 +211,7 @@ module.exports = {
210
211
  createSessionToken,
211
212
  componentId,
212
213
  action,
214
+ visitorId,
213
215
  }) => {
214
216
  const blocklet = await request.getBlocklet();
215
217
  const blockletInfo = await request.getBlockletInfo();
@@ -408,9 +410,8 @@ module.exports = {
408
410
  );
409
411
  }
410
412
 
411
- // NOTICE: 采用异步来更新,不阻塞接口的正常响应
412
413
  if (shouldSyncFederated(sourceAppPid, masterSite, blocklet)) {
413
- node.syncFederated({
414
+ await node.syncFederated({
414
415
  did: teamDid,
415
416
  data: {
416
417
  users: [
@@ -429,6 +430,18 @@ module.exports = {
429
430
  });
430
431
  }
431
432
 
433
+ const userSessionDoc = await upsertUserSession(
434
+ {
435
+ teamDid,
436
+ visitorId,
437
+ userDid: realDid,
438
+ appPid: teamDid,
439
+ passportId: passport?.id,
440
+ status: 'online',
441
+ },
442
+ { req: request, node, sourceAppPid }
443
+ );
444
+
432
445
  // Generate new session token that client can save to localStorage
433
446
  const createToken = createTokenFn(createSessionToken);
434
447
  const sessionConfig = blocklet.settings?.session || {};
@@ -468,6 +481,7 @@ module.exports = {
468
481
  data: vc,
469
482
  sessionToken,
470
483
  refreshToken,
484
+ visitorId: userSessionDoc.visitorId,
471
485
  nextWorkflowData: {
472
486
  userDid: realDid,
473
487
  },
@@ -477,6 +491,7 @@ module.exports = {
477
491
  return {
478
492
  sessionToken,
479
493
  refreshToken,
494
+ visitorId: userSessionDoc.visitorId,
480
495
  nextWorkflowData: {
481
496
  userDid: realDid,
482
497
  },
@@ -0,0 +1,80 @@
1
+ /* eslint-disable consistent-return */
2
+ /* eslint-disable no-bitwise */
3
+ const fetch = require('node-fetch').default;
4
+
5
+ const U200D = String.fromCharCode(8205); // zero-width joiner
6
+ const UFE0Fg = /\uFE0F/g; // variation selector regex
7
+
8
+ const getIconCode = (char) => toCodePoint(char.indexOf(U200D) < 0 ? char.replace(UFE0Fg, '') : char);
9
+
10
+ const toCodePoint = (unicodeSurrogates) => {
11
+ const r = [];
12
+ let c = 0;
13
+ let p = 0;
14
+ let i = 0;
15
+ while (i < unicodeSurrogates.length) {
16
+ c = unicodeSurrogates.charCodeAt(i++);
17
+ if (p) {
18
+ r.push((65536 + ((p - 55296) << 10) + (c - 56320)).toString(16));
19
+ p = 0;
20
+ } else if (c >= 55296 && c <= 56319) {
21
+ p = c;
22
+ } else {
23
+ r.push(c.toString(16));
24
+ }
25
+ }
26
+ return r.join('-');
27
+ };
28
+
29
+ const apis = {
30
+ twemoji: (code) => `https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/svg/${code.toLowerCase()}.svg`,
31
+ openmoji: 'https://cdn.jsdelivr.net/npm/@svgmoji/openmoji@2.0.0/svg/',
32
+ blobmoji: 'https://cdn.jsdelivr.net/npm/@svgmoji/blob@2.0.0/svg/',
33
+ noto: 'https://cdn.jsdelivr.net/gh/svgmoji/svgmoji/packages/svgmoji__noto/svg/',
34
+ fluent: (code) => `https://cdn.jsdelivr.net/gh/shuding/fluentui-emoji-unicode/assets/${code.toLowerCase()}_color.svg`,
35
+ fluentFlat: (code) =>
36
+ `https://cdn.jsdelivr.net/gh/shuding/fluentui-emoji-unicode/assets/${code.toLowerCase()}_flat.svg`,
37
+ };
38
+
39
+ // https://github.com/svgmoji/svgmoji
40
+ const loadEmoji = (code, type) => {
41
+ const api = apis[type] ?? apis.twemoji;
42
+ if (typeof api === 'function') {
43
+ return fetch(api(code));
44
+ }
45
+ return fetch(`${api}${code.toUpperCase()}.svg`);
46
+ };
47
+
48
+ const cache = new Map();
49
+ const loadDynamicAsset = (emojiType = 'twemoji') => {
50
+ const fn = async (languageCode, text) => {
51
+ if (languageCode === 'emoji') {
52
+ const code = getIconCode(text);
53
+ try {
54
+ const emoji = await loadEmoji(code, emojiType);
55
+ return `data:image/svg+xml;base64,${btoa(await emoji.text())}`;
56
+ } catch (err) {
57
+ console.error(`Failed to fetch emoji: ${text}:${code}`, err);
58
+ }
59
+ }
60
+ };
61
+
62
+ return async (...args) => {
63
+ const key = JSON.stringify({ ...args, emojiType });
64
+ const cached = cache.get(key);
65
+ if (cached) {
66
+ return cached;
67
+ }
68
+
69
+ const font = await fn(...args);
70
+ cache.set(key, font);
71
+ return font;
72
+ };
73
+ };
74
+
75
+ module.exports = {
76
+ apis,
77
+ getIconCode,
78
+ loadEmoji,
79
+ loadDynamicAsset,
80
+ };
@@ -15,6 +15,7 @@ const logger = require('@abtnode/logger')('@abtnode/blocklet-services/og');
15
15
 
16
16
  const { getTemplate, getLogoSvg } = require('./template');
17
17
  const { getCacheFilePath } = require('../image');
18
+ const emoji = require('./emoji');
18
19
 
19
20
  const TEMPLATES = ['default', 'section', 'cover'];
20
21
 
@@ -64,6 +65,12 @@ const schema = Joi.object({
64
65
  logo: Joi.string()
65
66
  .uri({ scheme: ['https'] })
66
67
  .optional(),
68
+
69
+ // custom emoji
70
+ emoji: Joi.string()
71
+ .valid(...Object.keys(emoji.apis))
72
+ .optional()
73
+ .default('twemoji'),
67
74
  }).options({ stripUnknown: true, allowUnknown: true, noDefaults: false });
68
75
 
69
76
  const generateTasks = {};
@@ -133,9 +140,14 @@ const convertExternalImage = (url, dest) => {
133
140
  headers: { Accept: 'image/*' },
134
141
  },
135
142
  (res) => {
143
+ if (res.statusCode && res.statusCode >= 400) {
144
+ reject(new Error(`unexpected external image status: ${res.statusCode}`));
145
+ return;
146
+ }
147
+
136
148
  const [type] = (res.headers['content-type'] || '').split('/');
137
149
  if (type !== 'image') {
138
- reject(new Error('external image is not an image'));
150
+ reject(new Error(`unexpected external image format: ${type}`));
139
151
  return;
140
152
  }
141
153
 
@@ -226,6 +238,7 @@ const generateOgImage = async (params, tmpDir) => {
226
238
  style: 'normal',
227
239
  },
228
240
  ],
241
+ loadAdditionalAsset: emoji.loadDynamicAsset(params.emoji),
229
242
  });
230
243
 
231
244
  if (params.format === 'svg') {
@@ -312,7 +312,7 @@ module.exports = {
312
312
  const { blocklet } = req;
313
313
  const { verifySite } = req.body;
314
314
  const teamDid = blocklet.appPid;
315
- const { users = null, sites = null } = req.body.verifyData;
315
+ const { users = null, sites = null, userSessions = null } = req.body.verifyData;
316
316
 
317
317
  // FIXME: @zhanghan 校验 users 和 sites 数据合法性
318
318
  const pendingList = [];
@@ -346,11 +346,31 @@ module.exports = {
346
346
  }
347
347
  }
348
348
 
349
+ if (!isNil(userSessions)) {
350
+ if (Array.isArray(userSessions)) {
351
+ for (const userSession of userSessions) {
352
+ const { action, ...userSessionItem } = userSession;
353
+ pendingList.push(
354
+ limitSync(async () => {
355
+ if (action === 'login') {
356
+ await node.upsertUserSession(
357
+ { ...userSessionItem, teamDid, status: 'online' },
358
+ { req, node, sourceAppPid: teamDid }
359
+ );
360
+ } else if (action === 'logout') {
361
+ await node.logoutUser({ ...userSessionItem, teamDid });
362
+ }
363
+ })
364
+ );
365
+ }
366
+ }
367
+ }
368
+
349
369
  await Promise.all(pendingList);
350
370
  await node.createAuditLog(
351
371
  {
352
372
  action: 'syncFederated',
353
- args: { users, sites, callerSite: verifySite, teamDid },
373
+ args: { users, sites, userSessions, callerSite: verifySite, teamDid },
354
374
  context: {
355
375
  user: getAuditLogActorByFederatedSite(verifySite),
356
376
  },
@@ -661,6 +681,84 @@ module.exports = {
661
681
  res.json({ sessionToken, refreshToken });
662
682
  });
663
683
 
684
+ // 用于在 master 站点登录页面获取 member 登录的 token
685
+ server.post(`${prefix}/loginByMaster`, ensureBlocklet(), verifyFederatedCall(), async (req, res) => {
686
+ const { verifySite } = req.body;
687
+ const { passport, user, walletOS, provider } = req.body.verifyData;
688
+ const { createSessionToken } = initJwt(node, options);
689
+ const createToken = createTokenFn(createSessionToken);
690
+ const { secret } = await req.getBlockletInfo();
691
+ const { blocklet } = req;
692
+ const teamDid = blocklet.appPid;
693
+
694
+ const sessionConfig = blocklet.settings?.session || {};
695
+ const prevUser = await node.getUser({
696
+ teamDid,
697
+ user: { did: user.did },
698
+ options: { enableConnectedAccount: true },
699
+ });
700
+ // HACK: member 调用 master 时,将 passport 的 role 还原为 master 中原有的 role
701
+ const targetPassport = passport?.id ? (prevUser?.passports || []).find((item) => item.id === passport.id) : null;
702
+
703
+ // HACK: 用户在 master 中存在时,不更新任何用户信息;不存在时,将新增一个用户
704
+ const filterUserInfo = prevUser ? {} : user;
705
+ if (filterUserInfo.avatar) {
706
+ let avatar = await getAvatarByUrl(filterUserInfo.avatar);
707
+ const nodeInfo = await req.getNodeInfo();
708
+
709
+ const { dataDir } = await getApplicationInfo({ node, nodeInfo, teamDid });
710
+ avatar = await extractUserAvatar(avatar, { dataDir });
711
+ filterUserInfo.avatar = avatar;
712
+ }
713
+ const realDid = prevUser?.did || user.did;
714
+ const realPk = prevUser?.pk || user.pk;
715
+ // NOTICE: 这里是 Master 登录,不需要 sourceAppPid 字段
716
+ const newUser = await node.loginUser({
717
+ teamDid,
718
+ user: {
719
+ ...filterUserInfo,
720
+ did: realDid,
721
+ pk: realPk,
722
+ passport: targetPassport,
723
+ connectedAccount: {
724
+ provider: provider || LOGIN_PROVIDER.WALLET,
725
+ did: user.did,
726
+ pk: user.pk,
727
+ },
728
+ },
729
+ });
730
+
731
+ const { sessionToken, refreshToken } = createToken(
732
+ user.did,
733
+ {
734
+ secret,
735
+ passport: targetPassport,
736
+ role: targetPassport?.role || 'guest',
737
+ fullName: newUser.fullName,
738
+ provider: provider || LOGIN_PROVIDER.WALLET,
739
+ walletOS,
740
+ sourceAppPid: verifySite.appPid,
741
+ },
742
+ {
743
+ ...sessionConfig,
744
+ didConnectVersion: getDidConnectVersion(req),
745
+ }
746
+ );
747
+
748
+ await node.createAuditLog(
749
+ {
750
+ action: 'loginByMaster',
751
+ args: { masterSite: verifySite, teamDid, blocklet },
752
+ context: {
753
+ user: newUser,
754
+ },
755
+ },
756
+ node
757
+ );
758
+
759
+ res.json({ sessionToken, refreshToken });
760
+ });
761
+
664
762
  // member 向 master 申请 auth0 账号的 migrate
665
763
  server.post(`${prefix}/migrateAuth0`, ensureBlocklet(), verifyFederatedCall(), async (req, res) => {
666
764
  const { blocklet } = req;
@@ -728,5 +826,13 @@ module.exports = {
728
826
  );
729
827
  res.json(data);
730
828
  });
829
+
830
+ server.post(`${prefix}/getPassport`, ensureBlocklet(), verifyFederatedCall(), async (req, res) => {
831
+ const { blocklet } = req;
832
+ const teamDid = blocklet.appPid;
833
+ const { passportId } = req.body.verifyData;
834
+ const result = await node.getPassportById({ teamDid, passportId });
835
+ res.json(result);
836
+ });
731
837
  },
732
838
  };
@@ -24,6 +24,7 @@ const { sendToUser } = require('../libs/notification');
24
24
  const { isInvitedUserOnly, createTokenFn, getDidConnectVersion } = require('../util');
25
25
  const { ApiError } = require('../util/error');
26
26
  const { loginAuth0, getFederatedMaster, getOAuthUserInfo, shouldSyncFederated } = require('../util/federated');
27
+ const { upsertUserSession } = require('../util/user-session');
27
28
 
28
29
  const PREFIX = WELLKNOWN_SERVICE_PATH_PREFIX;
29
30
 
@@ -64,7 +65,7 @@ function getAuthClient(blocklet, provider) {
64
65
 
65
66
  async function login(req, node, options) {
66
67
  const blocklet = await req.getBlocklet();
67
- const { token, locale = 'en', provider, componentId, sourceAppPid } = req.body;
68
+ const { token, locale = 'en', provider, componentId, sourceAppPid, visitorId } = req.body;
68
69
 
69
70
  if (!blocklet.settings?.owner) {
70
71
  throw new ApiError(400, t('oauthCantBeOwner', locale));
@@ -187,7 +188,6 @@ async function login(req, node, options) {
187
188
  );
188
189
 
189
190
  const masterSite = getFederatedMaster(blocklet);
190
- // NOTICE: 采用异步来更新,不阻塞接口的正常响应
191
191
  if (shouldSyncFederated(sourceAppPid, masterSite, blocklet)) {
192
192
  const syncUserData = {
193
193
  did: userDid,
@@ -196,7 +196,7 @@ async function login(req, node, options) {
196
196
  avatar: getUserAvatarUrl(appUrl, profile.avatar),
197
197
  connectedAccount: [connectedAccount],
198
198
  };
199
- node.syncFederated({
199
+ await node.syncFederated({
200
200
  did: teamDid,
201
201
  data: {
202
202
  users: [
@@ -210,6 +210,22 @@ async function login(req, node, options) {
210
210
  });
211
211
  }
212
212
 
213
+ const userSessionDoc = await upsertUserSession(
214
+ {
215
+ teamDid,
216
+ userDid,
217
+ visitorId,
218
+ appPid: teamDid,
219
+ passportId: passport?.id,
220
+ status: 'online',
221
+ },
222
+ {
223
+ node,
224
+ req,
225
+ sourceAppPid: sourceAppPid || masterSite.appPid,
226
+ }
227
+ );
228
+
213
229
  const { createSessionToken } = initJwt(node, options);
214
230
 
215
231
  const createToken = createTokenFn(createSessionToken);
@@ -235,11 +251,12 @@ async function login(req, node, options) {
235
251
  return {
236
252
  sessionToken,
237
253
  refreshToken,
254
+ visitorId: userSessionDoc.visitorId,
238
255
  };
239
256
  }
240
257
 
241
258
  async function invite(req, node, options) {
242
- const { locale, inviteId, token, baseUrl, provider = LOGIN_PROVIDER.AUTH0, sourceAppPid } = req.body;
259
+ const { locale, inviteId, token, baseUrl, provider = LOGIN_PROVIDER.AUTH0, sourceAppPid, visitorId } = req.body;
243
260
  const blocklet = await req.getBlocklet();
244
261
  let userWallet;
245
262
  let oauthInfo;
@@ -312,6 +329,7 @@ async function invite(req, node, options) {
312
329
  locale,
313
330
  provider,
314
331
  });
332
+ const masterSite = getFederatedMaster(blocklet);
315
333
 
316
334
  if (currentUser) {
317
335
  const walletDid = getWalletDid(currentUser);
@@ -351,8 +369,7 @@ async function invite(req, node, options) {
351
369
  sourceAppPid,
352
370
  },
353
371
  });
354
- const masterSite = getFederatedMaster(blocklet);
355
- // NOTICE: 采用异步来更新,不阻塞接口的正常响应
372
+
356
373
  if (shouldSyncFederated(sourceAppPid, masterSite, blocklet)) {
357
374
  const syncUserData = {
358
375
  did: userDid,
@@ -360,7 +377,7 @@ async function invite(req, node, options) {
360
377
  ...profile,
361
378
  connectedAccount: [connectedAccount],
362
379
  };
363
- node.syncFederated({
380
+ await node.syncFederated({
364
381
  did: teamDid,
365
382
  data: {
366
383
  users: [
@@ -391,7 +408,27 @@ async function invite(req, node, options) {
391
408
  return sessionToken;
392
409
  }
393
410
 
394
- return { sessionToken, refreshToken };
411
+ const userSessionDoc = await upsertUserSession(
412
+ {
413
+ teamDid,
414
+ userDid,
415
+ visitorId,
416
+ appPid: teamDid,
417
+ passportId: passport.id,
418
+ status: 'online',
419
+ },
420
+ {
421
+ node,
422
+ req,
423
+ sourceAppPid: sourceAppPid || masterSite.appPid,
424
+ }
425
+ );
426
+
427
+ return {
428
+ sessionToken,
429
+ refreshToken,
430
+ visitorId: userSessionDoc.visitorId,
431
+ };
395
432
  }
396
433
 
397
434
  // 给 DID Wallet 绑定 Auth0 的流程
@@ -679,8 +716,8 @@ module.exports = {
679
716
 
680
717
  async function getUserFn(req, res) {
681
718
  const { token, provider } = req.body;
682
- const blocklet = await req.getBlocklet();
683
719
  const { wallet: blockletWallet } = await req.getBlockletInfo();
720
+ const blocklet = await req.getBlocklet();
684
721
  const authClient = getAuthClient(blocklet, provider);
685
722
  const oauthInfo = await authClient.getProfile(token);
686
723
  const userWallet = fromAppDid(oauthInfo.sub, blockletWallet.secretKey);
@@ -0,0 +1,165 @@
1
+ /* eslint-disable no-await-in-loop */
2
+ const { WELLKNOWN_SERVICE_PATH_PREFIX } = require('@abtnode/constant');
3
+ const { LOGIN_PROVIDER } = require('@blocklet/constant');
4
+ const defaults = require('lodash/defaults');
5
+ const cloneDeep = require('lodash/cloneDeep');
6
+ const pLimit = require('p-limit');
7
+
8
+ const ensureBlocklet = require('../middlewares/ensure-blocklet');
9
+ const { upsertUserSession } = require('../util/user-session');
10
+ const { getUserAvatarUrl } = require('../util/federated');
11
+
12
+ const prefix = `${WELLKNOWN_SERVICE_PATH_PREFIX}/api/user-session`;
13
+ const limit = pLimit(5);
14
+
15
+ async function getPassportRole(passportId, { node, teamDid }) {
16
+ const passport = await node.getPassportById({ teamDid, passportId });
17
+ return passport?.role;
18
+ }
19
+
20
+ async function getPassportRoleFromFederatedSite(passportId, { appPid, node, teamDid, federated }) {
21
+ const currentSite = federated.sites.find((x) => x.appPid === appPid);
22
+ if (currentSite) {
23
+ const passport = await node.getPassportFromFederated({
24
+ site: currentSite,
25
+ passportId,
26
+ teamDid,
27
+ });
28
+ return passport?.role;
29
+ }
30
+ return undefined;
31
+ }
32
+
33
+ async function patchUserSessionData(userSession, { blocklet, appPid, teamDid, node }) {
34
+ // 修正 avatar 地址,从 bn:// 转换为 http
35
+ if (userSession.user?.avatar) {
36
+ userSession.user.avatar = getUserAvatarUrl(userSession.user.avatar, blocklet);
37
+ }
38
+
39
+ const federated = defaults(cloneDeep(blocklet.settings.federated || {}), {
40
+ config: {
41
+ appId: blocklet.appDid,
42
+ appPid: teamDid,
43
+ },
44
+ sites: [],
45
+ });
46
+ const site = federated.sites.find((siteItem) => siteItem.appPid === userSession.appPid);
47
+ // 获取 appName,展示在前端
48
+ if (site) {
49
+ userSession.appName = site.appName;
50
+ }
51
+
52
+ // 通过 userSession 信息判断 passport 对应的 role(针对 appPid 做转换)
53
+ let role;
54
+ if (userSession.passportId) {
55
+ if (appPid === teamDid) {
56
+ role = await getPassportRole(userSession?.passportId, { node, teamDid });
57
+ } else {
58
+ role = await getPassportRoleFromFederatedSite(userSession.passportId, {
59
+ appPid,
60
+ node,
61
+ teamDid,
62
+ federated,
63
+ });
64
+ }
65
+ } else {
66
+ role = userSession.user.role;
67
+ }
68
+ userSession.user.role = role || 'guest';
69
+ }
70
+
71
+ module.exports = {
72
+ init(app, node) {
73
+ // NOTE: 保留 /login 路由,该功能不是针对于某一个实体来操作的,需要更明确表达意图
74
+ app.post(`${prefix}/login`, ensureBlocklet(), async (req, res) => {
75
+ const { blocklet } = req;
76
+ const { visitorId, userDid, appPid, passportId } = req.body;
77
+ if (!userDid) {
78
+ res.status(400).json({ error: 'userDid is required' });
79
+ return;
80
+ }
81
+ if (!appPid) {
82
+ res.status(400).json({ error: 'appPid is required' });
83
+ return;
84
+ }
85
+
86
+ const teamDid = blocklet.appPid;
87
+
88
+ const userSessions = await node.getUserSession({
89
+ teamDid,
90
+ userDid,
91
+ visitorId,
92
+ });
93
+
94
+ if (userSessions.length > 0) {
95
+ const user = await node.getUser({ teamDid, user: { did: userDid } });
96
+ const federated = defaults(cloneDeep(blocklet.settings.federated || {}), {
97
+ config: {
98
+ appId: blocklet.appDid,
99
+ appPid: teamDid,
100
+ },
101
+ sites: [],
102
+ });
103
+
104
+ const memberSite = federated.sites.find((item) => item.appPid === appPid);
105
+
106
+ const result = await node.loginFederated({
107
+ did: appPid,
108
+ data: {
109
+ user,
110
+ passport: passportId ? { id: passportId } : undefined,
111
+ walletOS: 'web',
112
+ provider: LOGIN_PROVIDER.WALLET,
113
+ },
114
+ site: memberSite,
115
+ });
116
+
117
+ const userSessionDoc = await upsertUserSession(
118
+ {
119
+ teamDid: appPid,
120
+ userDid,
121
+ visitorId,
122
+ appPid,
123
+ passportId,
124
+ status: 'online',
125
+ },
126
+ {
127
+ node,
128
+ req,
129
+ sourceAppPid: teamDid,
130
+ }
131
+ );
132
+
133
+ res.json({ ...result, visitorId: userSessionDoc.visitorId });
134
+ } else {
135
+ res.json({});
136
+ }
137
+ });
138
+
139
+ app.get(`${prefix}`, ensureBlocklet(), async (req, res) => {
140
+ const { blocklet } = req;
141
+ const teamDid = blocklet.appPid;
142
+ const { visitorId, userDid, appPid = teamDid } = req.query;
143
+
144
+ if (!visitorId) {
145
+ res.json([]);
146
+ return;
147
+ }
148
+
149
+ const userSessions = await node.getUserSession({
150
+ teamDid,
151
+ userDid,
152
+ visitorId,
153
+ });
154
+
155
+ patchUserSessionData();
156
+
157
+ const pendingList = userSessions.map((item) =>
158
+ limit(() => patchUserSessionData(item, { blocklet, appPid, teamDid, node }))
159
+ );
160
+ await Promise.all(pendingList);
161
+
162
+ res.json(userSessions);
163
+ });
164
+ },
165
+ };
@@ -15,6 +15,7 @@ const { ApiError } = require('../util/error');
15
15
  const { loginWalletSchema, loginOAuthSchema } = require('../validators/login');
16
16
  const verifySig = require('../middlewares/verify-sig');
17
17
  const { getAvatarByEmail } = require('../libs/auth/utils');
18
+ const ensureBlocklet = require('../middlewares/ensure-blocklet');
18
19
 
19
20
  const validateUser = (user) => {
20
21
  try {
@@ -298,5 +299,25 @@ module.exports = {
298
299
  }
299
300
  server.post(`${prefix}/login`, verifySig, loginFn);
300
301
  server.post(`${prefixApi}/login`, verifySig, loginFn);
302
+
303
+ server.post(`${prefixApi}/logout`, ensureBlocklet(), async (req, res) => {
304
+ if (req.user) {
305
+ const { blocklet } = req;
306
+ const teamDid = blocklet.appPid;
307
+ const { visitorId } = req.body;
308
+ if (!visitorId) {
309
+ res.status(400).json({ error: 'visitorId is required' });
310
+ return;
311
+ }
312
+
313
+ await node.logoutUser({
314
+ userDid: req.user.did,
315
+ visitorId,
316
+ appPid: teamDid,
317
+ teamDid,
318
+ });
319
+ }
320
+ res.json({});
321
+ });
301
322
  },
302
323
  };