@abtnode/blocklet-services 1.16.18-beta-7aa6be5a → 1.16.18-beta-bb4ebacc

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 (135) hide show
  1. package/api/index.js +2 -7
  2. package/api/libs/connect/session.js +45 -33
  3. package/api/middlewares/check-running.js +1 -1
  4. package/api/routes/blocklet.js +18 -0
  5. package/api/routes/env.js +4 -2
  6. package/api/routes/federated.js +10 -9
  7. package/api/routes/oauth.js +79 -60
  8. package/api/routes/user-session.js +23 -16
  9. package/api/routes/user.js +160 -22
  10. package/api/services/auth/connect/invite.js +21 -12
  11. package/api/services/auth/index.js +77 -13
  12. package/api/services/auth/session.js +48 -1
  13. package/api/services/notification/blocklet-events-notifier.js +1 -0
  14. package/api/socket/channel/component.js +6 -5
  15. package/api/socket/channel/did.js +51 -20
  16. package/api/socket/util.js +1 -0
  17. package/api/util/federated.js +13 -12
  18. package/api/validators/login.js +12 -0
  19. package/build/asset-manifest.json +144 -112
  20. package/build/index.html +1 -1
  21. package/build/service-worker.js +1 -1
  22. package/build/service-worker.js.map +1 -1
  23. package/build/static/css/{807.65f26433.chunk.css → 1233.dba37e64.chunk.css} +1 -1
  24. package/build/static/js/1.138935fc.chunk.js +2 -0
  25. package/build/static/js/{1148.4b2a7347.chunk.js → 1148.5ccba08e.chunk.js} +2 -2
  26. package/build/static/js/1233.276cf2d0.chunk.js +3 -0
  27. package/build/static/js/1233.276cf2d0.chunk.js.LICENSE.txt +23 -0
  28. package/build/static/js/{2291.7ffdc7df.chunk.js → 2291.37b0ad3a.chunk.js} +2 -2
  29. package/build/static/js/2653.980158ea.chunk.js +2 -0
  30. package/build/static/js/2664.da443f31.chunk.js +2 -0
  31. package/build/static/js/2801.33d2f238.chunk.js +2 -0
  32. package/build/static/js/{2838.fcdea2fa.chunk.js → 2838.ec459fda.chunk.js} +3 -3
  33. package/build/static/js/2940.ce32ab3f.chunk.js +2 -0
  34. package/build/static/js/3025.7c99c058.chunk.js +2 -0
  35. package/build/static/js/{3033.6623d19d.chunk.js → 3033.9fe46d7f.chunk.js} +2 -2
  36. package/build/static/js/3076.28658ffc.chunk.js +2 -0
  37. package/build/static/js/{3131.672170fb.chunk.js → 3131.99541aca.chunk.js} +2 -2
  38. package/build/static/js/3430.dc830483.chunk.js +2 -0
  39. package/build/static/js/{3688.48e7b69c.chunk.js → 3688.679796d1.chunk.js} +2 -2
  40. package/build/static/js/3708.7e2ad66b.chunk.js +3 -0
  41. package/build/static/js/3842.e1bb702b.chunk.js +3 -0
  42. package/build/static/js/3953.ddf8be86.chunk.js +2 -0
  43. package/build/static/js/3979.f9a5aa48.chunk.js +3 -0
  44. package/build/static/js/{807.249d2d01.chunk.js.LICENSE.txt → 3979.f9a5aa48.chunk.js.LICENSE.txt} +0 -18
  45. package/build/static/js/3994.e53e7ce2.chunk.js +2 -0
  46. package/build/static/js/4009.65d1c083.chunk.js +2 -0
  47. package/build/static/js/{4023.1658794f.chunk.js → 4023.5fe8180a.chunk.js} +2 -2
  48. package/build/static/js/4076.5ca97365.chunk.js +2 -0
  49. package/build/static/js/4160.e45b5ba1.chunk.js +2 -0
  50. package/build/static/js/{4319.8d9e189e.chunk.js → 4319.9f120992.chunk.js} +2 -2
  51. package/build/static/js/4461.26253c6a.chunk.js +2 -0
  52. package/build/static/js/4587.9a042d46.chunk.js +2 -0
  53. package/build/static/js/4802.217e3956.chunk.js +2 -0
  54. package/build/static/js/4882.0435e418.chunk.js +2 -0
  55. package/build/static/js/5070.31a74ba7.chunk.js +2 -0
  56. package/build/static/js/5468.b54ce65b.chunk.js +2 -0
  57. package/build/static/js/5547.d68bfde3.chunk.js +3 -0
  58. package/build/static/js/556.e1875260.chunk.js +3 -0
  59. package/build/static/js/{5683.47011724.chunk.js → 5683.051a03c1.chunk.js} +2 -2
  60. package/build/static/js/6032.1001afd3.chunk.js +2 -0
  61. package/build/static/js/6139.161b2e7f.chunk.js +2 -0
  62. package/build/static/js/6218.9a4feced.chunk.js +2 -0
  63. package/build/static/js/655.cdee65b4.chunk.js +2 -0
  64. package/build/static/js/{6658.090d923f.chunk.js → 6658.98f9956d.chunk.js} +2 -2
  65. package/build/static/js/{6771.a1510398.chunk.js → 6771.920fa5b6.chunk.js} +3 -3
  66. package/build/static/js/7006.bb90b167.chunk.js +2 -0
  67. package/build/static/js/{716.9c76ad65.chunk.js → 716.e68425d7.chunk.js} +3 -3
  68. package/build/static/js/716.e68425d7.chunk.js.LICENSE.txt +5 -0
  69. package/build/static/js/7305.465df4a1.chunk.js +2 -0
  70. package/build/static/js/7313.b53b59d8.chunk.js +2 -0
  71. package/build/static/js/779.73350f02.chunk.js +2 -0
  72. package/build/static/js/{7832.741ac84f.chunk.js → 7832.81aaf011.chunk.js} +2 -2
  73. package/build/static/js/7858.52930f63.chunk.js +2 -0
  74. package/build/static/js/8016.3ef64a2c.chunk.js +2 -0
  75. package/build/static/js/8181.6c2a7dcb.chunk.js +2 -0
  76. package/build/static/js/8393.ea7ef05d.chunk.js +2 -0
  77. package/build/static/js/840.5bc210dd.chunk.js +2 -0
  78. package/build/static/js/8604.ece06cf7.chunk.js +3 -0
  79. package/build/static/js/{8622.f9639db9.chunk.js → 8622.ebd44812.chunk.js} +2 -2
  80. package/build/static/js/{8641.506d61d5.chunk.js → 8641.bec13444.chunk.js} +2 -2
  81. package/build/static/js/8702.bb84d009.chunk.js +2 -0
  82. package/build/static/js/{8792.8b009f81.chunk.js → 8792.0f55707c.chunk.js} +2 -2
  83. package/build/static/js/8944.559ade67.chunk.js +2 -0
  84. package/build/static/js/9017.56c20b17.chunk.js +2 -0
  85. package/build/static/js/9033.ebf65926.chunk.js +2 -0
  86. package/build/static/js/9107.7f32925d.chunk.js +2 -0
  87. package/build/static/js/9314.f0add972.chunk.js +2 -0
  88. package/build/static/js/9582.2c539fcf.chunk.js +2 -0
  89. package/build/static/js/{9596.5251398b.chunk.js → 9596.a4eead04.chunk.js} +2 -2
  90. package/build/static/js/9865.d5dfa72b.chunk.js +2 -0
  91. package/build/static/js/{9900.cce3f786.chunk.js → 9900.3b72165e.chunk.js} +2 -2
  92. package/build/static/js/9996.5545ea26.chunk.js +2 -0
  93. package/build/static/js/main.36b0deea.js +3 -0
  94. package/build/static/js/{main.50d5cec4.js.LICENSE.txt → main.36b0deea.js.LICENSE.txt} +1 -1
  95. package/package.json +38 -37
  96. package/build/static/js/1431.29b0bc0a.chunk.js +0 -3
  97. package/build/static/js/2093.3ce9b59d.chunk.js +0 -3
  98. package/build/static/js/2653.6610534f.chunk.js +0 -2
  99. package/build/static/js/2686.890b32ed.chunk.js +0 -2
  100. package/build/static/js/2972.2d5268c3.chunk.js +0 -2
  101. package/build/static/js/3025.a48c96f5.chunk.js +0 -2
  102. package/build/static/js/3038.40d611d1.chunk.js +0 -2
  103. package/build/static/js/3490.4d3a51ea.chunk.js +0 -2
  104. package/build/static/js/4022.5ec79b56.chunk.js +0 -2
  105. package/build/static/js/4076.bcc72fcc.chunk.js +0 -2
  106. package/build/static/js/4461.f412553b.chunk.js +0 -2
  107. package/build/static/js/4587.906e3023.chunk.js +0 -2
  108. package/build/static/js/4716.a8a5b75d.chunk.js +0 -2
  109. package/build/static/js/4764.51208c0d.chunk.js +0 -2
  110. package/build/static/js/4802.fe56c787.chunk.js +0 -2
  111. package/build/static/js/5465.dd0f7a20.chunk.js +0 -2
  112. package/build/static/js/5547.5a341867.chunk.js +0 -3
  113. package/build/static/js/5662.03f06ae0.chunk.js +0 -2
  114. package/build/static/js/5673.0dd67df6.chunk.js +0 -2
  115. package/build/static/js/5960.35db200d.chunk.js +0 -2
  116. package/build/static/js/6139.cabc22a2.chunk.js +0 -2
  117. package/build/static/js/6498.698608f8.chunk.js +0 -2
  118. package/build/static/js/6868.881bd141.chunk.js +0 -2
  119. package/build/static/js/7006.8f0645e5.chunk.js +0 -2
  120. package/build/static/js/7050.3281ad5f.chunk.js +0 -2
  121. package/build/static/js/7367.15383006.chunk.js +0 -2
  122. package/build/static/js/779.b64e7db1.chunk.js +0 -2
  123. package/build/static/js/7858.e4167dfd.chunk.js +0 -2
  124. package/build/static/js/807.249d2d01.chunk.js +0 -3
  125. package/build/static/js/8604.a8ef404e.chunk.js +0 -3
  126. package/build/static/js/8702.2d858942.chunk.js +0 -2
  127. package/build/static/js/8944.2d60df14.chunk.js +0 -2
  128. package/build/static/js/main.50d5cec4.js +0 -3
  129. /package/build/static/js/{2838.fcdea2fa.chunk.js.LICENSE.txt → 2838.ec459fda.chunk.js.LICENSE.txt} +0 -0
  130. /package/build/static/js/{2093.3ce9b59d.chunk.js.LICENSE.txt → 3708.7e2ad66b.chunk.js.LICENSE.txt} +0 -0
  131. /package/build/static/js/{716.9c76ad65.chunk.js.LICENSE.txt → 3842.e1bb702b.chunk.js.LICENSE.txt} +0 -0
  132. /package/build/static/js/{5547.5a341867.chunk.js.LICENSE.txt → 5547.d68bfde3.chunk.js.LICENSE.txt} +0 -0
  133. /package/build/static/js/{1431.29b0bc0a.chunk.js.LICENSE.txt → 556.e1875260.chunk.js.LICENSE.txt} +0 -0
  134. /package/build/static/js/{6771.a1510398.chunk.js.LICENSE.txt → 6771.920fa5b6.chunk.js.LICENSE.txt} +0 -0
  135. /package/build/static/js/{8604.a8ef404e.chunk.js.LICENSE.txt → 8604.ece06cf7.chunk.js.LICENSE.txt} +0 -0
@@ -1,5 +1,5 @@
1
1
  const { NODE_SERVICES, WELLKNOWN_SERVICE_PATH_PREFIX } = require('@abtnode/constant');
2
- const { getApplicationInfo, upsertUserSession } = require('@abtnode/auth/lib/auth');
2
+ const { getApplicationInfo } = require('@abtnode/auth/lib/auth');
3
3
  const { fromAppDid } = require('@arcblock/did-ext');
4
4
  const { extractUserAvatar } = require('@abtnode/util/lib/user');
5
5
  const formatContext = require('@abtnode/util/lib/format-context');
@@ -7,12 +7,15 @@ const createTranslator = require('@abtnode/util/lib/translate');
7
7
  const logger = require('@abtnode/logger')('blocklet-services:user');
8
8
  const { isFromPublicKey } = require('@arcblock/did');
9
9
  const { LOGIN_PROVIDER } = require('@blocklet/constant');
10
+ const { fromPublicKey } = require('@ocap/wallet');
11
+ const sortBy = require('lodash/sortBy');
12
+ const head = require('lodash/head');
10
13
 
11
14
  const { isInvitedUserOnly, createTokenFn, getDidConnectVersion } = require('../util');
12
15
  const initJwt = require('../libs/jwt');
13
16
  const { getAvatarByUrl } = require('../libs/auth/utils');
14
17
  const { ApiError } = require('../util/error');
15
- const { loginWalletSchema, loginOAuthSchema } = require('../validators/login');
18
+ const { loginWalletSchema, loginOAuthSchema, loginUserWalletSchema } = require('../validators/login');
16
19
  const verifySig = require('../middlewares/verify-sig');
17
20
  const { getAvatarByEmail } = require('../libs/auth/utils');
18
21
  const ensureBlocklet = require('../middlewares/ensure-blocklet');
@@ -213,7 +216,6 @@ async function loginOAuth(
213
216
  if (!validateUser(loginParams.user) || !validateUser(loginParams.user.connectedAccount)) {
214
217
  throw new ApiError(400, t('userInfoError', locale));
215
218
  }
216
- // TODO: @zhanghan 暂不支持统一登录模式登录
217
219
  const doc = await node.loginUser(loginParams);
218
220
  return doc;
219
221
  }
@@ -240,14 +242,14 @@ async function login(req, node, options) {
240
242
  const { did: teamDid, wallet: blockletWallet, secret } = await req.getBlockletInfo();
241
243
  const blocklet = await req.getBlocklet();
242
244
 
243
- let doc;
245
+ let userDoc;
244
246
  if (provider === LOGIN_PROVIDER.WALLET) {
245
- doc = await loginWallet(
247
+ userDoc = await loginWallet(
246
248
  { did, pk, avatar, email, fullName },
247
249
  { node, req, locale, componentId, teamDid, updateInfo, sourceAppPid }
248
250
  );
249
251
  } else {
250
- doc = await loginOAuth(
252
+ userDoc = await loginOAuth(
251
253
  { provider, id, avatar, email, fullName },
252
254
  { node, req, locale, componentId, teamDid, blockletWallet, updateInfo, sourceAppPid }
253
255
  );
@@ -259,9 +261,9 @@ async function login(req, node, options) {
259
261
  await node.createAuditLog(
260
262
  {
261
263
  action: 'login',
262
- args: { teamDid, userDid: doc.did, passport, provider },
263
- context: formatContext(Object.assign(req, { user: doc })),
264
- result: doc,
264
+ args: { teamDid, userDid: userDoc.did, passport, provider },
265
+ context: formatContext(Object.assign(req, { user: userDoc })),
266
+ result: userDoc,
265
267
  },
266
268
  node
267
269
  );
@@ -271,35 +273,66 @@ async function login(req, node, options) {
271
273
  const createToken = createTokenFn(createSessionToken);
272
274
  const sessionConfig = blocklet.settings?.session || {};
273
275
  const { sessionToken, refreshToken } = createToken(
274
- doc.did,
276
+ userDoc.did,
275
277
  {
276
278
  secret,
277
279
  passport,
278
280
  role: passport.role,
279
- fullName: doc.fullName,
281
+ fullName: userDoc.fullName,
280
282
  provider,
281
283
  walletOS: 'api',
282
284
  },
283
285
  { ...sessionConfig, didConnectVersion: getDidConnectVersion(req) }
284
286
  );
285
- const userSession = await upsertUserSession(
286
- {
287
- visitorId,
288
- teamDid,
289
- userDid: doc.did,
290
- appPid: teamDid,
291
- status: 'online',
292
- },
293
- { req, node }
294
- );
287
+ const lastLoginIp = req.headers['x-real-ip'];
288
+ const ua = req.headers['user-agent'];
289
+ const userSession = await node.upsertUserSession({
290
+ visitorId,
291
+ teamDid,
292
+ userDid: userDoc.did,
293
+ appPid: teamDid,
294
+ status: 'online',
295
+ ua,
296
+ lastLoginIp,
297
+ });
298
+ node.syncUserSession({
299
+ teamDid,
300
+ userDid: userDoc.did,
301
+ visitorId: userSession.visitorId,
302
+ passportId: passport?.id,
303
+ targetAppPid: sourceAppPid,
304
+ ua,
305
+ lastLoginIp,
306
+ });
295
307
  return {
296
- user: doc,
308
+ user: userDoc,
297
309
  token: sessionToken,
298
310
  refreshToken,
299
311
  visitorId: userSession?.visitorId || '',
300
312
  };
301
313
  }
302
314
 
315
+ async function verifyUserSig({ userDid, signature, teamDid, data }, { node }) {
316
+ const currentUser = await node.getUser({
317
+ teamDid,
318
+ user: {
319
+ did: userDid,
320
+ },
321
+ options: {
322
+ enableConnectedAccount: true,
323
+ },
324
+ });
325
+ if (!currentUser) {
326
+ throw new Error('user is not exist');
327
+ }
328
+ const userWallet = fromPublicKey(currentUser.pk);
329
+ const valid = userWallet.verify(data, signature);
330
+ if (!valid) {
331
+ throw new Error('invalid signature');
332
+ }
333
+ return currentUser;
334
+ }
335
+
303
336
  module.exports = {
304
337
  init(server, node, options) {
305
338
  async function loginFn(req, res) {
@@ -318,6 +351,111 @@ module.exports = {
318
351
  server.post(`${prefix}/login`, verifySig, loginFn);
319
352
  server.post(`${prefixApi}/login`, verifySig, loginFn);
320
353
 
354
+ /**
355
+ * @description 用于在 native 钱包中完成用户的自动登录
356
+ * @summary 暂时不允许用户注册,只允许登录
357
+ */
358
+ server.post(`${prefixApi}/loginByWallet`, async (req, res) => {
359
+ const { userDid, signature, walletOS, nonce, visitorId, passportId, sourceAppPid, locale } = req.body;
360
+ const { error } = loginUserWalletSchema.validate({
361
+ userDid,
362
+ signature,
363
+ walletOS,
364
+ nonce,
365
+ visitorId,
366
+ passportId,
367
+ sourceAppPid,
368
+ locale,
369
+ });
370
+ if (error) {
371
+ throw new ApiError(400, error.message);
372
+ }
373
+ const { did: teamDid, secret } = await req.getBlockletInfo();
374
+ const currentUser = await verifyUserSig({ userDid, signature, teamDid, data: nonce }, { node });
375
+ const { createSessionToken } = initJwt(node, options);
376
+ const createToken = createTokenFn(createSessionToken);
377
+ const blocklet = await req.getBlocklet();
378
+ const sessionConfig = blocklet.settings?.session || {};
379
+ const passports = currentUser?.passports || [];
380
+
381
+ let passport = null;
382
+ if (passportId) {
383
+ passport = passports.find((x) => x.passportId === passportId);
384
+ }
385
+ if (!passport) {
386
+ const now = new Date().getTime();
387
+ passport = head(
388
+ sortBy(passports, (x) => {
389
+ const lastLoginAt = new Date(x.lastLoginAt).getTime();
390
+ if (typeof lastLoginAt === 'number') {
391
+ return now - lastLoginAt;
392
+ }
393
+ return now;
394
+ })
395
+ );
396
+ }
397
+
398
+ if (!passport) {
399
+ passport = { name: 'Guest', role: 'guest' };
400
+ }
401
+
402
+ const lastLoginIp = req.get('x-real-ip');
403
+ const loginedUser = await node.loginUser({
404
+ teamDid,
405
+ user: {
406
+ did: currentUser.did,
407
+ pk: currentUser.pk,
408
+ locale,
409
+ lastLoginIp,
410
+ sourceAppPid,
411
+ connectedAccount: {
412
+ provider: LOGIN_PROVIDER.WALLET,
413
+ did: currentUser.did,
414
+ pk: currentUser.pk,
415
+ },
416
+ },
417
+ });
418
+
419
+ const { sessionToken, refreshToken } = createToken(
420
+ loginedUser.did,
421
+ {
422
+ secret,
423
+ passport,
424
+ role: passport?.role || 'guest',
425
+ fullName: loginedUser.fullName,
426
+ provider: 'wallet',
427
+ walletOS,
428
+ },
429
+ { ...sessionConfig, didConnectVersion: getDidConnectVersion(req) }
430
+ );
431
+ const ua = req.headers['user-agent'];
432
+ const userSession = await node.upsertUserSession({
433
+ visitorId,
434
+ teamDid,
435
+ userDid: loginedUser.did,
436
+ appPid: teamDid,
437
+ status: 'online',
438
+ ua,
439
+ lastLoginIp,
440
+ });
441
+ node.syncUserSession({
442
+ teamDid,
443
+ userDid,
444
+ visitorId: userSession.visitorId,
445
+ passportId,
446
+ targetAppPid: sourceAppPid,
447
+ ua,
448
+ lastLoginIp,
449
+ });
450
+
451
+ res.json({
452
+ sessionToken,
453
+ refreshToken,
454
+ nonce,
455
+ visitorId: userSession.visitorId,
456
+ });
457
+ });
458
+
321
459
  server.post(`${prefixApi}/logout`, ensureBlocklet(), async (req, res) => {
322
460
  if (req.user) {
323
461
  const { blocklet } = req;
@@ -6,7 +6,6 @@ const {
6
6
  messages,
7
7
  checkWalletVersion,
8
8
  beforeInvitationRequest,
9
- upsertUserSession,
10
9
  } = require('@abtnode/auth/lib/auth');
11
10
  const { getSourceAppPid } = require('@blocklet/sdk/lib/util/login');
12
11
  const { NODE_SERVICES, WELLKNOWN_SERVICE_PATH_PREFIX } = require('@abtnode/constant');
@@ -98,17 +97,27 @@ module.exports = function createRoutes(node, authenticator, createSessionToken)
98
97
  { ...sessionConfig, didConnectVersion: getDidConnectVersion(req) }
99
98
  );
100
99
 
101
- const userSessionDoc = await upsertUserSession(
102
- {
103
- teamDid,
104
- visitorId,
105
- userDid,
106
- appPid: teamDid,
107
- passportId: response.data.id,
108
- status: 'online',
109
- },
110
- { req, node, sourceAppPid }
111
- );
100
+ const lastLoginIp = req.headers['x-real-ip'];
101
+ const ua = req.headers['user-agent'];
102
+ const userSessionDoc = await node.upsertUserSession({
103
+ teamDid,
104
+ visitorId,
105
+ userDid,
106
+ appPid: teamDid,
107
+ passportId: response.data.id,
108
+ status: 'online',
109
+ ua,
110
+ lastLoginIp,
111
+ });
112
+ node.syncUserSession({
113
+ teamDid,
114
+ userDid,
115
+ visitorId: userSessionDoc.visitorId,
116
+ passportId: passport?.id,
117
+ targetAppPid: sourceAppPid,
118
+ ua,
119
+ lastLoginIp,
120
+ });
112
121
  await updateSession(
113
122
  {
114
123
  sessionToken,
@@ -84,12 +84,16 @@ const init = ({ node, options }) => {
84
84
  };
85
85
 
86
86
  /**
87
- * @returns {object} res
88
- * {boolean} res.blocked
89
- * {boolean} res.authenticated
90
- * {boolean} res.authorized
91
- * {boolean} res.ignored
92
- * {string} res.payable
87
+ * @typedef {object} CheckAuthRes
88
+ * @property {boolean} blocked
89
+ * @property {boolean} authenticated
90
+ * @property {boolean} authorized
91
+ * @property {boolean} ignored
92
+ * @property {string} payable
93
+ */
94
+
95
+ /**
96
+ * @returns {CheckAuthRes} res
93
97
  */
94
98
  const checkAuth = async ({ req } = {}) => {
95
99
  const config = (await req.getServiceConfig(NODE_SERVICES.AUTH)) || {};
@@ -107,24 +111,74 @@ const init = ({ node, options }) => {
107
111
 
108
112
  const teamDid = req.getBlockletDid();
109
113
 
114
+ // 所有人都可以访问的情况
110
115
  if (!config.whoCanAccess || config.whoCanAccess === WHO_CAN_ACCESS.ALL) {
111
116
  if (config.blockUnauthenticated && !req.user) {
112
- return { blocked: true, authenticated: false };
117
+ return {
118
+ blocked: true,
119
+ authenticated: false,
120
+ };
113
121
  }
114
122
  } else if (!req.user) {
123
+ // 需要被邀请才能访问的情况
115
124
  const rbac = await node.getRBAC(teamDid);
116
125
  const allRoles = await rbac.getRoles(teamDid);
117
126
  const payableRole = allRoles.find((x) => x.extra?.acquire?.pay);
118
- return { blocked: true, authenticated: false, payable: payableRole?.extra?.acquire?.pay };
127
+ return {
128
+ blocked: true,
129
+ authenticated: false,
130
+ payable: payableRole?.extra?.acquire?.pay,
131
+ };
119
132
  } else if (config.whoCanAccess === WHO_CAN_ACCESS.OWNER && req.user.role !== ROLES.OWNER) {
120
- return { blocked: true, authenticated: true, authorized: false };
133
+ // 需要 owner 才能访问
134
+ return {
135
+ blocked: true,
136
+ authenticated: true,
137
+ authorized: false,
138
+ requiredRoles: [
139
+ {
140
+ name: ROLES.OWNER,
141
+ title: 'Owner',
142
+ description: 'Owner',
143
+ },
144
+ ],
145
+ };
121
146
  } else if (config.whoCanAccess.startsWith(WHO_CAN_ACCESS_PREFIX_ROLES)) {
147
+ // 指定 passport 才能访问
148
+ // 需要用户拥有的权限
122
149
  const roles = getRolesFromAuthConfig(config);
123
150
  if (!roles.includes(req.user.role)) {
124
151
  const rbac = await node.getRBAC(teamDid);
152
+ // 系统中的所有权限
125
153
  const allRoles = await rbac.getRoles(teamDid);
126
- const payableRole = allRoles.find((x) => roles.includes(x.name) && x.extra?.acquire?.pay);
127
- return { blocked: true, authenticated: true, authorized: false, payable: payableRole?.extra?.acquire?.pay };
154
+
155
+ return {
156
+ blocked: true,
157
+ authenticated: true,
158
+ authorized: false,
159
+ requiredRoles: roles
160
+ .map((item) => {
161
+ if (item.name === ROLES.OWNER) {
162
+ return {
163
+ name: item.name,
164
+ title: 'Owner',
165
+ description: 'Owner',
166
+ };
167
+ }
168
+ const findRole = allRoles.find((x) => x.name === item);
169
+ if (!findRole) {
170
+ return null;
171
+ }
172
+
173
+ return {
174
+ name: findRole.name,
175
+ title: findRole.title,
176
+ description: findRole.description,
177
+ payable: findRole?.extra?.acquire?.pay,
178
+ };
179
+ })
180
+ .filter((x) => x),
181
+ };
128
182
  }
129
183
  }
130
184
 
@@ -254,7 +308,7 @@ const init = ({ node, options }) => {
254
308
  };
255
309
 
256
310
  middlewares.checkAuth = async (req, res, next) => {
257
- const { blocked, authenticated, authorized, payable } = await checkAuth({ req });
311
+ const { blocked, authenticated, authorized, payable, requiredRoles } = await checkAuth({ req });
258
312
 
259
313
  if (blocked) {
260
314
  if (!authenticated) {
@@ -269,7 +323,17 @@ const init = ({ node, options }) => {
269
323
 
270
324
  if (!authorized) {
271
325
  if (req.accepts(['html', 'json']) === 'html') {
272
- res.redirect(getRedirectUrl({ req, pagePath: '/login', params: { authenticated: 1, payable } }));
326
+ res.redirect(
327
+ getRedirectUrl({
328
+ req,
329
+ pagePath: '/login',
330
+ params: {
331
+ authenticated: 1,
332
+ payable,
333
+ requiredRoles: JSON.stringify(requiredRoles),
334
+ },
335
+ })
336
+ );
273
337
  } else {
274
338
  // Security principles: user should not known the reason
275
339
  res.status(404).json({ code: 404, error: REASON_404 });
@@ -1,6 +1,8 @@
1
1
  const nocache = require('nocache');
2
2
  const joinUrl = require('url-join');
3
3
  const pick = require('lodash/pick');
4
+ const omit = require('lodash/omit');
5
+ const merge = require('lodash/merge');
4
6
  const {
5
7
  WELLKNOWN_SERVICE_PATH_PREFIX,
6
8
  USER_AVATAR_URL_PREFIX,
@@ -9,6 +11,7 @@ const {
9
11
  } = require('@abtnode/constant');
10
12
  const { LOGIN_PROVIDER } = require('@blocklet/constant');
11
13
 
14
+ const isUrl = require('is-url');
12
15
  const { PREFIXES } = require('../../util/constants');
13
16
  const { createTokenFn, getDidConnectVersion } = require('../../util');
14
17
 
@@ -57,7 +60,7 @@ module.exports = {
57
60
  .map((x) => pick(x, ['id', 'name', 'title', 'role']));
58
61
 
59
62
  res.json({
60
- user,
63
+ user: omit(user, ['extra']),
61
64
  provider: req.user.provider || LOGIN_PROVIDER.WALLET,
62
65
  walletOS: req.user.walletOS,
63
66
  });
@@ -67,6 +70,50 @@ module.exports = {
67
70
  router.get(sessionApi, nocache(), sessionBearerToken, handleSession);
68
71
  router.post(sessionApi, nocache(), sessionBearerToken, handleSession);
69
72
 
73
+ // update user extra: settings, webhooks
74
+ const extraApi = `${prefix}/api/user/extra`;
75
+ const checkUser = async (req, res, next) => {
76
+ const { token } = req;
77
+ await ensureUser({ req, token });
78
+ if (!req.user) {
79
+ res.status(403).json(null);
80
+ } else {
81
+ next();
82
+ }
83
+ };
84
+ router.get(extraApi, nocache(), sessionBearerToken, checkUser, async (req, res) => {
85
+ const teamDid = req.getBlockletDid();
86
+ const user = await node.getUser({ teamDid, user: { did: req.user.did } });
87
+ res.json(user.extra || {});
88
+ });
89
+ router.post(extraApi, nocache(), sessionBearerToken, checkUser, async (req, res) => {
90
+ const teamDid = req.getBlockletDid();
91
+ const exist = await node.getUser({ teamDid, user: { did: req.user.did } });
92
+ const user = await node.updateUserExtra({
93
+ teamDid,
94
+ did: req.user.did,
95
+ extra: JSON.stringify(merge({}, exist.extra || {}, req.body)),
96
+ });
97
+ res.json(user.extra);
98
+ });
99
+ router.put(extraApi, nocache(), sessionBearerToken, checkUser, async (req, res) => {
100
+ if (['slack', 'api'].includes(req.body.type) === false) {
101
+ res.status(400).send({ error: 'invalid webhook type' });
102
+ return;
103
+ }
104
+
105
+ if (isUrl(req.body.url) === false) {
106
+ res.status(400).send({ error: 'invalid webhook url' });
107
+ }
108
+
109
+ await node.sendTestMessage({
110
+ webhook: { type: req.body.type, params: [{ name: 'url', value: req.body.url }] },
111
+ message: `This is a test message from user ${req.user.did}`,
112
+ });
113
+
114
+ res.json({ success: true });
115
+ });
116
+
70
117
  router.post(`${prefix}/api/did/refreshSession`, refreshBearerToken, async (req, res) => {
71
118
  const token = req.refreshToken;
72
119
  if (token) {
@@ -189,6 +189,7 @@ const init = ({ node, notificationService }) => {
189
189
  notification: {
190
190
  title: message.title[locale] || message.title[DEFAULT_LOCALE],
191
191
  body: message.body[locale] || message.body[DEFAULT_LOCALE],
192
+ source: 'app',
192
193
  },
193
194
  };
194
195
  notificationService.sendToUser.exec(input);
@@ -100,17 +100,18 @@ const sendToAppComponents = async ({ event, appDid, componentDid: inputComponent
100
100
  const componentDid = component.meta.did;
101
101
 
102
102
  if (!inputComponentDid || componentDid === inputComponentDid) {
103
- // realAppDid is diff with appDid when app development mode
104
- const realAppDid = app.appDid || appDid;
103
+ // appPid is diff with appDid when app development mode
104
+ // appPid is diff with appDid when app has been rotated
105
+ const appPid = app.appPid || appDid;
105
106
 
106
107
  // eslint-disable-next-line no-loop-func
107
- broadcast(wsServer, getComponentChannel(realAppDid, componentDid), event, notification, async (count) => {
108
+ broadcast(wsServer, getComponentChannel(appPid, componentDid), event, notification, async (count) => {
108
109
  // FIXME @linchen 组件以 cluster 模式启动时, 是否确保所有组件实例都收到消息?
109
110
  if (count <= 0) {
110
- logger.info('Online component client was not found', { realAppDid, componentDid });
111
+ logger.info('Online component client was not found', { appPid, componentDid });
111
112
  await lock.acquire();
112
113
  try {
113
- await states.message.insert({ did: getCacheId(realAppDid, componentDid), event, data: notification });
114
+ await states.message.insert({ did: getCacheId(appPid, componentDid), event, data: notification });
114
115
  lock.release();
115
116
  } catch (error) {
116
117
  lock.release();
@@ -8,6 +8,9 @@ const { NODE_MODES } = require('@abtnode/constant');
8
8
  const { getWalletDid } = require('@blocklet/sdk/lib/did');
9
9
  const JWT = require('@arcblock/jwt');
10
10
  const pMap = require('p-map');
11
+ const uniqBy = require('lodash/uniqBy');
12
+ const uniq = require('lodash/uniq');
13
+ const get = require('lodash/get');
11
14
 
12
15
  // eslint-disable-next-line global-require
13
16
  const logger = require('@abtnode/logger')(`${require('../../../package.json').name}:notification`);
@@ -34,21 +37,25 @@ const sendToUserDid = async ({ sender, receiver: rawDid, notification, options,
34
37
  const { keepForOfflineUser = true } = options || {};
35
38
  const receiver = Array.isArray(rawDid) ? rawDid : [rawDid];
36
39
 
37
- const receiverDidList = [];
38
- const receiverEmailList = [];
40
+ let receiverDidList = [];
41
+ let receiverEmailList = [];
42
+ let webhookList = [];
43
+
44
+ const webhookSenders = new Map();
39
45
 
40
46
  // sender.appDid 就是当前 blockletDid,通知的发送方就是这个 blocklet 本身
41
47
  const teamDid = sender.appDid;
42
48
 
43
49
  await pMap(receiver, async (item) => {
44
- const userInfoItem = await node.getUser({
45
- teamDid,
46
- user: { did: item },
47
- options: { enableConnectedAccount: true },
48
- });
49
- const receiverDid = getWalletDid(userInfoItem) || item;
50
- const receiverEmail = userInfoItem?.email;
51
- if (receiverDid) {
50
+ const user = await node.getUser({ teamDid, user: { did: item }, options: { enableConnectedAccount: true } });
51
+ const walletEnabled = get(user, 'extra.notifications.wallet', true);
52
+ const emailEnabled = get(user, 'extra.notifications.email', true);
53
+ const webhooks = get(user, 'extra.webhooks', []);
54
+
55
+ const receiverDid = getWalletDid(user) || item;
56
+ const receiverEmail = user?.email;
57
+
58
+ if (receiverDid && walletEnabled) {
52
59
  try {
53
60
  await validateReceiver(receiverDid);
54
61
  receiverDidList.push(receiverDid);
@@ -56,17 +63,21 @@ const sendToUserDid = async ({ sender, receiver: rawDid, notification, options,
56
63
  /* empty */
57
64
  }
58
65
  }
59
- if (receiverEmail) {
66
+ if (receiverEmail && emailEnabled) {
60
67
  try {
61
68
  await validateEmail(receiverEmail);
62
69
  receiverEmailList.push({
63
70
  email: receiverEmail,
64
- locale: userInfoItem?.locale || 'en',
71
+ locale: user?.locale || 'en',
65
72
  });
66
73
  } catch {
67
74
  /* empty */
68
75
  }
69
76
  }
77
+
78
+ if (Array.isArray(webhooks)) {
79
+ webhookList.push(...webhooks.filter((x) => x.type && x.url));
80
+ }
70
81
  });
71
82
 
72
83
  if (receiverDidList.length === 0 && receiverEmailList.length === 0) {
@@ -85,6 +96,11 @@ const sendToUserDid = async ({ sender, receiver: rawDid, notification, options,
85
96
  // parse notification
86
97
  const notifications = parseNotification(notification, senderInfo);
87
98
 
99
+ // uniq receivers
100
+ receiverDidList = uniq(receiverDidList);
101
+ receiverEmailList = uniqBy(receiverEmailList, 'email');
102
+ webhookList = uniqBy(webhookList, 'url');
103
+
88
104
  // send notification
89
105
  notifications.forEach((data) => {
90
106
  for (const receiverDid of receiverDidList) {
@@ -95,14 +111,29 @@ const sendToUserDid = async ({ sender, receiver: rawDid, notification, options,
95
111
  }
96
112
  });
97
113
  }
98
- // NOTICE: 目前只有 notification 的通知能够发送邮件,并且 type 可能为 undefined
99
- if ([undefined, NOTIFICATION_TYPES.NOTIFICATION].includes(data.type)) {
100
- for (const receiverEmail of receiverEmailList) {
101
- sendEmail(receiverEmail.email, data, { teamDid: sender.appDid, node, locale: receiverEmail.locale }).catch(
102
- (error) => {
103
- logger.error('Failed to send email', { error });
104
- }
105
- );
114
+
115
+ // Do not send email for component activities
116
+ if (data.source !== 'app') {
117
+ // NOTICE: 目前只有 notification 的通知能够发送邮件,并且 type 可能为 undefined
118
+ if ([undefined, NOTIFICATION_TYPES.NOTIFICATION].includes(data.type)) {
119
+ for (const receiverEmail of receiverEmailList) {
120
+ sendEmail(receiverEmail.email, data, { teamDid: sender.appDid, node, locale: receiverEmail.locale }).catch(
121
+ (error) => {
122
+ logger.error('Failed to send email', { error });
123
+ }
124
+ );
125
+ }
126
+ }
127
+
128
+ // send webhook
129
+ for (const webhook of webhookList) {
130
+ let webhookSender = webhookSenders.get(webhook.type);
131
+ if (!webhookSender) {
132
+ webhookSender = node.getMessageSender(webhook.type);
133
+ webhookSenders.set(webhook.type, webhookSender);
134
+ }
135
+
136
+ webhookSender.sendNotification(webhook.url, notification);
106
137
  }
107
138
  }
108
139
  });
@@ -29,6 +29,7 @@ const parseNotification = (notification, senderInfo) => {
29
29
  did: senderInfo.permanentWallet.address,
30
30
  pk: senderInfo.permanentWallet.pk,
31
31
  name: senderInfo.name,
32
+ logo: senderInfo.logo,
32
33
  // actualDid is the did of the application that is used to decrypt the message if needed
33
34
  actualDid: senderInfo.wallet.address,
34
35
  actualPk: senderInfo.wallet.pk,