@abtnode/blocklet-services 1.16.18-beta-aa01bd8e → 1.16.18-beta-f4777312

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 (106) hide show
  1. package/api/index.js +1 -7
  2. package/api/libs/image.js +19 -12
  3. package/api/routes/env.js +4 -2
  4. package/api/routes/federated.js +4 -1
  5. package/api/routes/oauth.js +11 -2
  6. package/api/routes/user.js +4 -4
  7. package/api/services/auth/index.js +77 -13
  8. package/api/services/auth/session.js +48 -1
  9. package/api/services/notification/blocklet-events-notifier.js +1 -0
  10. package/api/socket/channel/component.js +6 -5
  11. package/api/socket/channel/did.js +51 -20
  12. package/api/socket/util.js +1 -0
  13. package/build/asset-manifest.json +107 -95
  14. package/build/index.html +1 -1
  15. package/build/service-worker.js +1 -1
  16. package/build/service-worker.js.map +1 -1
  17. package/build/static/css/{4448.de9040c4.chunk.css → 1233.dba37e64.chunk.css} +1 -1
  18. package/build/static/css/{5547.5f9be15f.chunk.css → 5547.e016de4c.chunk.css} +3 -3
  19. package/build/static/js/{1148.e614f706.chunk.js → 1148.5ccba08e.chunk.js} +2 -2
  20. package/build/static/js/{4448.f0c62086.chunk.js → 1233.276cf2d0.chunk.js} +3 -3
  21. package/build/static/js/{4448.f0c62086.chunk.js.LICENSE.txt → 1233.276cf2d0.chunk.js.LICENSE.txt} +6 -0
  22. package/build/static/js/2653.980158ea.chunk.js +2 -0
  23. package/build/static/js/2664.da443f31.chunk.js +2 -0
  24. package/build/static/js/2801.33d2f238.chunk.js +2 -0
  25. package/build/static/js/2940.ce32ab3f.chunk.js +2 -0
  26. package/build/static/js/3025.7c99c058.chunk.js +2 -0
  27. package/build/static/js/{3033.6623d19d.chunk.js → 3033.9fe46d7f.chunk.js} +2 -2
  28. package/build/static/js/{3131.672170fb.chunk.js → 3131.99541aca.chunk.js} +2 -2
  29. package/build/static/js/3430.dc830483.chunk.js +2 -0
  30. package/build/static/js/{3688.48e7b69c.chunk.js → 3688.679796d1.chunk.js} +2 -2
  31. package/build/static/js/3708.7e2ad66b.chunk.js +3 -0
  32. package/build/static/js/3842.e1bb702b.chunk.js +3 -0
  33. package/build/static/js/3953.ddf8be86.chunk.js +2 -0
  34. package/build/static/js/{4023.a2e9db00.chunk.js → 4023.5fe8180a.chunk.js} +2 -2
  35. package/build/static/js/4076.f5369a1d.chunk.js +2 -0
  36. package/build/static/js/4160.e45b5ba1.chunk.js +2 -0
  37. package/build/static/js/{4461.26253c6a.chunk.js → 4461.3cd698bf.chunk.js} +2 -2
  38. package/build/static/js/4587.9a042d46.chunk.js +2 -0
  39. package/build/static/js/4802.217e3956.chunk.js +2 -0
  40. package/build/static/js/5070.31a74ba7.chunk.js +2 -0
  41. package/build/static/js/5468.b54ce65b.chunk.js +2 -0
  42. package/build/static/js/5547.570ded36.chunk.js +3 -0
  43. package/build/static/js/{5547.8447edf9.chunk.js.LICENSE.txt → 5547.570ded36.chunk.js.LICENSE.txt} +2 -3
  44. package/build/static/js/556.e1875260.chunk.js +3 -0
  45. package/build/static/js/{5569.a7e151fc.chunk.js → 5569.e4bfe697.chunk.js} +2 -2
  46. package/build/static/js/6032.1001afd3.chunk.js +2 -0
  47. package/build/static/js/{6139.5867193a.chunk.js → 6139.161b2e7f.chunk.js} +2 -2
  48. package/build/static/js/6218.4f3036a7.chunk.js +2 -0
  49. package/build/static/js/{6629.376863d2.chunk.js → 6629.a4a3fb70.chunk.js} +3 -3
  50. package/build/static/js/{6658.090d923f.chunk.js → 6658.98f9956d.chunk.js} +2 -2
  51. package/build/static/js/{716.9c76ad65.chunk.js → 716.e68425d7.chunk.js} +3 -3
  52. package/build/static/js/716.e68425d7.chunk.js.LICENSE.txt +5 -0
  53. package/build/static/js/7305.465df4a1.chunk.js +2 -0
  54. package/build/static/js/7313.b53b59d8.chunk.js +2 -0
  55. package/build/static/js/779.73350f02.chunk.js +2 -0
  56. package/build/static/js/7858.52930f63.chunk.js +2 -0
  57. package/build/static/js/8016.3ef64a2c.chunk.js +2 -0
  58. package/build/static/js/{8181.86477cc5.chunk.js → 8181.6c2a7dcb.chunk.js} +2 -2
  59. package/build/static/js/8393.ea7ef05d.chunk.js +2 -0
  60. package/build/static/js/840.5bc210dd.chunk.js +2 -0
  61. package/build/static/js/{8622.66a81bc0.chunk.js → 8622.ebd44812.chunk.js} +2 -2
  62. package/build/static/js/8641.bec13444.chunk.js +2 -0
  63. package/build/static/js/8702.bb84d009.chunk.js +2 -0
  64. package/build/static/js/{8792.8b009f81.chunk.js → 8792.0f55707c.chunk.js} +2 -2
  65. package/build/static/js/8944.559ade67.chunk.js +2 -0
  66. package/build/static/js/9033.ebf65926.chunk.js +2 -0
  67. package/build/static/js/9107.7f32925d.chunk.js +2 -0
  68. package/build/static/js/9314.f0add972.chunk.js +2 -0
  69. package/build/static/js/{9596.5251398b.chunk.js → 9596.a4eead04.chunk.js} +2 -2
  70. package/build/static/js/9865.d5dfa72b.chunk.js +2 -0
  71. package/build/static/js/{9900.cce3f786.chunk.js → 9900.3b72165e.chunk.js} +2 -2
  72. package/build/static/js/main.6f9907fb.js +3 -0
  73. package/build/static/js/{main.63103d24.js.LICENSE.txt → main.6f9907fb.js.LICENSE.txt} +1 -1
  74. package/package.json +27 -26
  75. package/build/static/js/1343.69031e0b.chunk.js +0 -2
  76. package/build/static/js/1581.ed4544c6.chunk.js +0 -2
  77. package/build/static/js/2026.514f995a.chunk.js +0 -2
  78. package/build/static/js/2362.ab5308a8.chunk.js +0 -2
  79. package/build/static/js/2506.dd9284b6.chunk.js +0 -2
  80. package/build/static/js/2653.6610534f.chunk.js +0 -2
  81. package/build/static/js/2972.2d5268c3.chunk.js +0 -2
  82. package/build/static/js/3025.aa129833.chunk.js +0 -2
  83. package/build/static/js/3038.40d611d1.chunk.js +0 -2
  84. package/build/static/js/3683.6f169a32.chunk.js +0 -2
  85. package/build/static/js/3920.5c63a2b9.chunk.js +0 -3
  86. package/build/static/js/4076.bcc72fcc.chunk.js +0 -2
  87. package/build/static/js/4247.c25f1945.chunk.js +0 -2
  88. package/build/static/js/4587.906e3023.chunk.js +0 -2
  89. package/build/static/js/4716.a8a5b75d.chunk.js +0 -2
  90. package/build/static/js/4764.51208c0d.chunk.js +0 -2
  91. package/build/static/js/4802.cd83ecef.chunk.js +0 -2
  92. package/build/static/js/5547.8447edf9.chunk.js +0 -3
  93. package/build/static/js/556.4d5cc702.chunk.js +0 -3
  94. package/build/static/js/6032.48c0a152.chunk.js +0 -2
  95. package/build/static/js/7050.3281ad5f.chunk.js +0 -2
  96. package/build/static/js/779.383b4b4a.chunk.js +0 -2
  97. package/build/static/js/7858.4e5b7c88.chunk.js +0 -2
  98. package/build/static/js/8702.2d858942.chunk.js +0 -2
  99. package/build/static/js/8944.07bcf75f.chunk.js +0 -2
  100. package/build/static/js/8983.19b79a23.chunk.js +0 -2
  101. package/build/static/js/9880.13ac9520.chunk.js +0 -2
  102. package/build/static/js/main.63103d24.js +0 -3
  103. /package/build/static/js/{3920.5c63a2b9.chunk.js.LICENSE.txt → 3708.7e2ad66b.chunk.js.LICENSE.txt} +0 -0
  104. /package/build/static/js/{716.9c76ad65.chunk.js.LICENSE.txt → 3842.e1bb702b.chunk.js.LICENSE.txt} +0 -0
  105. /package/build/static/js/{556.4d5cc702.chunk.js.LICENSE.txt → 556.e1875260.chunk.js.LICENSE.txt} +0 -0
  106. /package/build/static/js/{6629.376863d2.chunk.js.LICENSE.txt → 6629.a4a3fb70.chunk.js.LICENSE.txt} +0 -0
package/api/index.js CHANGED
@@ -162,11 +162,6 @@ module.exports = function createServer(node, serverOptions = {}) {
162
162
  logger.error('send to component error', { error });
163
163
  });
164
164
  }
165
-
166
- // backward compatibility, should be removed if BlockletSDK@1.6.14 is not supported anymore
167
- notificationService.sendToApp.exec({ event, appDid, data }).catch((error) => {
168
- logger.error('send to app error', { error });
169
- });
170
165
  });
171
166
  });
172
167
 
@@ -285,7 +280,7 @@ module.exports = function createServer(node, serverOptions = {}) {
285
280
  server.use(
286
281
  `${WELLKNOWN_SERVICE_PATH_PREFIX}${pathname}`,
287
282
  checkMemberPermission,
288
- proxyToDaemon({ proxy, pathname, ...options })
283
+ proxyToDaemon({ proxy, ...options })
289
284
  );
290
285
  });
291
286
 
@@ -463,7 +458,6 @@ module.exports = function createServer(node, serverOptions = {}) {
463
458
 
464
459
  BlockletEventsNotifier.init({ node, notificationService });
465
460
 
466
- server.sendToApp = ({ event, appDid, data }) => notificationService.sendToApp.exec({ event, appDid, data });
467
461
  server.sendToAppComponents = ({ event, appDid, data }) =>
468
462
  notificationService.sendToAppComponents.exec({ event, appDid, data });
469
463
 
package/api/libs/image.js CHANGED
@@ -128,6 +128,8 @@ const isImageRequest = (req) => {
128
128
  return true;
129
129
  };
130
130
 
131
+ const getImageContentType = (extension) => (extension === 'svg' ? 'image/svg+xml' : `image/${extension}`);
132
+
131
133
  const tasks = {};
132
134
  const processAndRespond = (req, res, cacheDir, getSrc, ext, sendOptions = { maxAge: '356d', immutable: true }) => {
133
135
  if (fs.existsSync(cacheDir) === false) {
@@ -159,7 +161,7 @@ const processAndRespond = (req, res, cacheDir, getSrc, ext, sendOptions = { maxA
159
161
  const cacheKey = md5(stringify({ target: req.target, path: req.originalUrl, params }));
160
162
  const destPath = getCacheFilePath(cacheDir, `${cacheKey}.${params.f || extension}`);
161
163
  if (fs.existsSync(destPath)) {
162
- res.header('Content-Type', `image/${params.f || extension}`);
164
+ res.header('Content-Type', getImageContentType(params.f || extension));
163
165
  res.sendFile(destPath, sendOptions);
164
166
  return;
165
167
  }
@@ -176,7 +178,7 @@ const processAndRespond = (req, res, cacheDir, getSrc, ext, sendOptions = { maxA
176
178
  tasks[cacheKey]
177
179
  .then(() => {
178
180
  logger.info('image filter succeed', { params, url: req.originalUrl, destPath });
179
- res.header('Content-Type', `image/${params.f || extension}`);
181
+ res.header('Content-Type', getImageContentType(params.f || extension));
180
182
  res.sendFile(destPath, sendOptions);
181
183
  })
182
184
  .catch((err) => {
@@ -192,6 +194,21 @@ const processAndRespond = (req, res, cacheDir, getSrc, ext, sendOptions = { maxA
192
194
 
193
195
  const processImage = (src, extension, dest, params) => {
194
196
  return new Promise((resolve, reject) => {
197
+ // output stream
198
+ const out = fs.createWriteStream(dest);
199
+ out.on('close', () => {
200
+ resolve(dest);
201
+ });
202
+
203
+ out.on('error', (err) => {
204
+ reject(err);
205
+ });
206
+
207
+ if (extension === 'svg') {
208
+ src.pipe(out);
209
+ return;
210
+ }
211
+
195
212
  const {
196
213
  imageFilter,
197
214
  w: width,
@@ -251,16 +268,6 @@ const processImage = (src, extension, dest, params) => {
251
268
  pipeline.negate();
252
269
  }
253
270
 
254
- // output stream
255
- const out = fs.createWriteStream(dest);
256
- out.on('close', () => {
257
- resolve(dest);
258
- });
259
-
260
- out.on('error', (err) => {
261
- reject(err);
262
- });
263
-
264
271
  pipeline[format || EXTENSIONS[extension]]({ quality, progressive: !!progressive, force: true });
265
272
 
266
273
  pipeline.on('error', (err) => {
package/api/routes/env.js CHANGED
@@ -30,11 +30,13 @@ module.exports = {
30
30
  apiPrefix: "${pathPrefix.replace(/\/+$/, '')}${WELLKNOWN_SERVICE_PATH_PREFIX}",
31
31
  ${groupPathPrefix ? `groupPathPrefix: "${groupPathPrefix}",` : ''}
32
32
  webWalletUrl: "${info.webWalletUrl || config.webWalletUrl || opts.webWalletUrl}",
33
- nftDomainUrl: "${info.nftDomainUrl}",
33
+ nftDomainUrl: "${info.nftDomainUrl || ''}",
34
34
  passportColor: "${passportColor}",
35
35
  serverDid: "${info.did}",
36
36
  serverVersion: "${info.version}",
37
- mode: "${info.mode}"
37
+ mode: "${info.mode}",
38
+ ownerNft: ${JSON.stringify(info.ownerNft || '')},
39
+ launcher: ${JSON.stringify(info.launcher || '')}
38
40
  }`);
39
41
  });
40
42
  },
@@ -339,7 +339,10 @@ module.exports = {
339
339
  pendingUserList.push(
340
340
  limitSync(async () => {
341
341
  await syncFnMaps[user.action]?.(
342
- { ...user, sourceAppPid: user.sourceAppPid === teamDid ? undefined : user.sourceAppPid },
342
+ {
343
+ ...user,
344
+ sourceAppPid: user.sourceAppPid === teamDid ? null : user.sourceAppPid,
345
+ },
343
346
  { node, teamDid, dataDir }
344
347
  );
345
348
  })
@@ -64,7 +64,7 @@ function getAuthClient(blocklet, provider) {
64
64
 
65
65
  async function login(req, node, options) {
66
66
  const blocklet = await req.getBlocklet();
67
- const { token, locale = 'en', provider, componentId, sourceAppPid, visitorId } = req.body;
67
+ const { token, locale = 'en', provider, componentId, sourceAppPid = null, visitorId } = req.body;
68
68
 
69
69
  if (!blocklet.settings?.owner) {
70
70
  throw new ApiError(400, t('oauthCantBeOwner', locale));
@@ -264,7 +264,15 @@ async function login(req, node, options) {
264
264
  }
265
265
 
266
266
  async function invite(req, node, options) {
267
- const { locale, inviteId, token, baseUrl, provider = LOGIN_PROVIDER.AUTH0, sourceAppPid, visitorId } = req.body;
267
+ const {
268
+ locale,
269
+ inviteId,
270
+ token,
271
+ baseUrl,
272
+ provider = LOGIN_PROVIDER.AUTH0,
273
+ sourceAppPid = null,
274
+ visitorId,
275
+ } = req.body;
268
276
  const blocklet = await req.getBlocklet();
269
277
  let userWallet;
270
278
  let oauthInfo;
@@ -390,6 +398,7 @@ async function invite(req, node, options) {
390
398
  syncData.users.push({
391
399
  ...syncUserData,
392
400
  action: 'connectAccount',
401
+ // HACK: @zhanghan 这里会造成 master 中的用户也增加 sourceAppPid 字段,需要在 sync 接收端处理
393
402
  sourceAppPid: sourceAppPid || masterSite?.appPid,
394
403
  });
395
404
  }
@@ -102,7 +102,7 @@ async function composeProfileData({ avatar, fullName, email }, { node, req, team
102
102
 
103
103
  async function loginWallet(
104
104
  { did, pk, avatar, email, fullName },
105
- { node, req, locale, componentId, teamDid, updateInfo = true, sourceAppPid }
105
+ { node, req, locale, componentId, teamDid, updateInfo = true, sourceAppPid = null }
106
106
  ) {
107
107
  const provider = LOGIN_PROVIDER.WALLET;
108
108
  const { error } = loginWalletSchema.validate({
@@ -162,7 +162,7 @@ async function loginWallet(
162
162
 
163
163
  async function loginOAuth(
164
164
  { provider, id, avatar, email, fullName },
165
- { node, req, locale, componentId, teamDid, blockletWallet, updateInfo = true, sourceAppPid }
165
+ { node, req, locale, componentId, teamDid, blockletWallet, updateInfo = true, sourceAppPid = null }
166
166
  ) {
167
167
  const { error } = loginOAuthSchema.validate({
168
168
  provider,
@@ -232,7 +232,7 @@ async function login(req, node, options) {
232
232
  locale = 'en',
233
233
  updateInfo = true,
234
234
  visitorId,
235
- sourceAppPid,
235
+ sourceAppPid = null,
236
236
  } = req.body;
237
237
 
238
238
  const componentId = req.get('x-blocklet-component-id');
@@ -356,7 +356,7 @@ module.exports = {
356
356
  * @summary 暂时不允许用户注册,只允许登录
357
357
  */
358
358
  server.post(`${prefixApi}/loginByWallet`, async (req, res) => {
359
- const { userDid, signature, walletOS, nonce, visitorId, passportId, sourceAppPid, locale } = req.body;
359
+ const { userDid, signature, walletOS, nonce, visitorId, passportId, sourceAppPid = null, locale } = req.body;
360
360
  const { error } = loginUserWalletSchema.validate({
361
361
  userDid,
362
362
  signature,
@@ -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,