@abtnode/core 1.17.8-beta-20260109-075740-5f484e08 → 1.17.8-beta-20260113-015027-32a1cec4

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 (65) hide show
  1. package/lib/api/team/access-key-manager.js +104 -0
  2. package/lib/api/team/invitation-manager.js +461 -0
  3. package/lib/api/team/notification-manager.js +189 -0
  4. package/lib/api/team/oauth-manager.js +60 -0
  5. package/lib/api/team/org-crud-manager.js +202 -0
  6. package/lib/api/team/org-manager.js +56 -0
  7. package/lib/api/team/org-member-manager.js +403 -0
  8. package/lib/api/team/org-query-manager.js +126 -0
  9. package/lib/api/team/org-resource-manager.js +186 -0
  10. package/lib/api/team/passport-manager.js +670 -0
  11. package/lib/api/team/rbac-manager.js +335 -0
  12. package/lib/api/team/session-manager.js +540 -0
  13. package/lib/api/team/store-manager.js +198 -0
  14. package/lib/api/team/tag-manager.js +230 -0
  15. package/lib/api/team/user-auth-manager.js +132 -0
  16. package/lib/api/team/user-manager.js +78 -0
  17. package/lib/api/team/user-query-manager.js +299 -0
  18. package/lib/api/team/user-social-manager.js +354 -0
  19. package/lib/api/team/user-update-manager.js +224 -0
  20. package/lib/api/team/verify-code-manager.js +161 -0
  21. package/lib/api/team.js +439 -3287
  22. package/lib/blocklet/manager/disk/auth-manager.js +68 -0
  23. package/lib/blocklet/manager/disk/backup-manager.js +288 -0
  24. package/lib/blocklet/manager/disk/cleanup-manager.js +157 -0
  25. package/lib/blocklet/manager/disk/component-manager.js +83 -0
  26. package/lib/blocklet/manager/disk/config-manager.js +191 -0
  27. package/lib/blocklet/manager/disk/controller-manager.js +64 -0
  28. package/lib/blocklet/manager/disk/delete-reset-manager.js +328 -0
  29. package/lib/blocklet/manager/disk/download-manager.js +96 -0
  30. package/lib/blocklet/manager/disk/env-config-manager.js +311 -0
  31. package/lib/blocklet/manager/disk/federated-manager.js +651 -0
  32. package/lib/blocklet/manager/disk/hook-manager.js +124 -0
  33. package/lib/blocklet/manager/disk/install-component-manager.js +95 -0
  34. package/lib/blocklet/manager/disk/install-core-manager.js +448 -0
  35. package/lib/blocklet/manager/disk/install-download-manager.js +313 -0
  36. package/lib/blocklet/manager/disk/install-manager.js +36 -0
  37. package/lib/blocklet/manager/disk/install-upgrade-manager.js +340 -0
  38. package/lib/blocklet/manager/disk/job-manager.js +467 -0
  39. package/lib/blocklet/manager/disk/lifecycle-manager.js +26 -0
  40. package/lib/blocklet/manager/disk/notification-manager.js +343 -0
  41. package/lib/blocklet/manager/disk/query-manager.js +562 -0
  42. package/lib/blocklet/manager/disk/settings-manager.js +507 -0
  43. package/lib/blocklet/manager/disk/start-manager.js +611 -0
  44. package/lib/blocklet/manager/disk/stop-restart-manager.js +292 -0
  45. package/lib/blocklet/manager/disk/update-manager.js +153 -0
  46. package/lib/blocklet/manager/disk.js +669 -5796
  47. package/lib/blocklet/manager/helper/blue-green-start-blocklet.js +5 -0
  48. package/lib/blocklet/manager/lock.js +18 -0
  49. package/lib/event/index.js +28 -24
  50. package/lib/util/blocklet/app-utils.js +192 -0
  51. package/lib/util/blocklet/blocklet-loader.js +258 -0
  52. package/lib/util/blocklet/config-manager.js +232 -0
  53. package/lib/util/blocklet/did-document.js +240 -0
  54. package/lib/util/blocklet/environment.js +555 -0
  55. package/lib/util/blocklet/health-check.js +449 -0
  56. package/lib/util/blocklet/install-utils.js +365 -0
  57. package/lib/util/blocklet/logo.js +57 -0
  58. package/lib/util/blocklet/meta-utils.js +269 -0
  59. package/lib/util/blocklet/port-manager.js +141 -0
  60. package/lib/util/blocklet/process-manager.js +504 -0
  61. package/lib/util/blocklet/runtime-info.js +105 -0
  62. package/lib/util/blocklet/validation.js +418 -0
  63. package/lib/util/blocklet.js +98 -3066
  64. package/lib/util/wallet-app-notification.js +40 -0
  65. package/package.json +22 -22
package/lib/api/team.js CHANGED
@@ -1,233 +1,36 @@
1
1
  const { EventEmitter } = require('events');
2
- const axios = require('@abtnode/util/lib/axios');
3
- const pick = require('lodash/pick');
4
- const omit = require('lodash/omit');
5
- const defaults = require('lodash/defaults');
6
- const isEmpty = require('lodash/isEmpty');
7
2
  const throttle = require('lodash/throttle');
8
- const cloneDeep = require('@abtnode/util/lib/deep-clone');
9
- const { joinURL, withoutTrailingSlash } = require('ufo');
10
3
  const { Op } = require('sequelize');
11
4
  const dayjs = require('@abtnode/util/lib/dayjs');
12
5
  const logger = require('@abtnode/logger')('@abtnode/core:api:team');
13
6
  const pAll = require('p-all');
14
- const {
15
- ROLES,
16
- genPermissionName,
17
- PASSPORT_STATUS,
18
- WELLKNOWN_SERVICE_PATH_PREFIX,
19
- MAX_USER_PAGE_SIZE,
20
- MAIN_CHAIN_ENDPOINT,
21
- USER_AVATAR_URL_PREFIX,
22
- SESSION_TTL,
23
- EVENTS,
24
- NOTIFICATION_SEND_CHANNEL,
25
- USER_SESSION_STATUS,
26
- WEBHOOK_CONSECUTIVE_FAILURES_THRESHOLD,
27
- } = require('@abtnode/constant');
28
- const { isValid: isValidDid } = require('@arcblock/did');
29
- const { BlockletEvents, TeamEvents, BlockletInternalEvents } = require('@blocklet/constant');
30
- const { sendToUser } = require('@blocklet/sdk/lib/util/send-notification');
31
- const {
32
- createPassportVC,
33
- createPassport,
34
- upsertToPassports,
35
- createUserPassport,
36
- } = require('@abtnode/auth/lib/passport');
37
- const { formatError } = require('@blocklet/error');
38
- const { getPassportStatusEndpoint, getApplicationInfo } = require('@abtnode/auth/lib/auth');
39
- const {
40
- callFederated,
41
- getFederatedMaster,
42
- findFederatedSite,
43
- getUserAvatarUrl: getFederatedUserAvatarUrl,
44
- syncFederated,
45
- } = require('@abtnode/auth/lib/util/federated');
46
- const { hasActiveOwnerPassport } = require('@abtnode/util/lib/passport');
47
- const { getBlockletInfo } = require('@blocklet/meta/lib/info');
48
- const { getUserAvatarUrl, getAppAvatarUrl, extractUserAvatar, getAvatarByUrl } = require('@abtnode/util/lib/user');
49
- const { getChainClient } = require('@abtnode/util/lib/get-chain-client');
50
- const { getWalletDid } = require('@blocklet/meta/lib/did-utils');
51
- const { generateRandomString } = require('@abtnode/models/lib/util');
52
- const pMap = require('p-map');
7
+ const { PASSPORT_STATUS, USER_AVATAR_URL_PREFIX } = require('@abtnode/constant');
53
8
  const isUrl = require('is-url');
54
- const { PASSPORT_LOG_ACTION, PASSPORT_SOURCE, PASSPORT_ISSUE_ACTION } = require('@abtnode/constant');
55
- const ensureServerEndpoint = require('@abtnode/util/lib/ensure-server-endpoint');
56
- const { CustomError } = require('@blocklet/error');
57
- const { getEmailServiceProvider } = require('@abtnode/auth/lib/email');
58
- const md5 = require('@abtnode/util/lib/md5');
59
- const { sanitizeTag } = require('@abtnode/util/lib/sanitize');
60
- const { Joi } = require('@arcblock/validator');
61
- const { getBlockletAppIdList } = require('@blocklet/meta/lib/util');
62
-
63
- const { validateTrustedPassportIssuers } = require('../validators/trusted-passport');
64
- const { validateTrustedFactories } = require('../validators/trusted-factory');
65
- const { validateCreateRole, validateUpdateRole } = require('../validators/role');
66
- const { validateCreatePermission, validateUpdatePermission } = require('../validators/permission');
9
+ const { extractUserAvatar, getAvatarByUrl } = require('@abtnode/util/lib/user');
10
+ const { TeamEvents } = require('@blocklet/constant');
67
11
  const { BlockletRuntimeMonitor } = require('../monitor/blocklet-runtime-monitor');
68
12
  const { getBlocklet } = require('../util/blocklet');
69
- const StoreUtil = require('../util/store');
70
13
  const { profileSchema } = require('../validators/user');
71
- const { passportDisplaySchema } = require('../validators/util');
72
- const { validateUserRolePassport } = require('../util/validate-user-role-passport');
73
- const { getOrgInviteLink, createOrgValidators, isOrgOwner, isAdmingPath } = require('../util/org');
74
- const { createOrgInputSchema, updateOrgInputSchema } = require('../validators/org');
75
- const { checkPushChannelAvailable, getNotificationPushState } = require('../util/notification');
76
-
77
- const sanitizeUrl = (url) => {
78
- if (!url) {
79
- throw new Error('Registry URL should not be empty');
80
- }
81
-
82
- return url.trim();
83
- };
84
-
85
- const validateReservedRole = (role) => {
86
- if (Object.values(ROLES).includes(role)) {
87
- throw new Error(`The role ${role} is reserved`);
88
- }
89
- return true;
90
- };
91
-
92
- const sendPassportVcNotification = ({ userDid, appWallet: wallet, locale, vc, notification }) => {
93
- try {
94
- const receiver = userDid;
95
- const sender = { appDid: wallet.address, appSk: wallet.secretKey };
96
- const message = { ...notification };
97
- if (!message.title) {
98
- message.title = {
99
- en: 'Receive a Passport',
100
- zh: '获得通行证',
101
- }[locale];
102
- }
103
- if (!message.body) {
104
- message.body = {
105
- en: 'You got a passport',
106
- zh: '你获得了一张通行证',
107
- }[locale];
108
- }
109
- if (!message.attachments) {
110
- message.attachments = [
111
- {
112
- type: 'vc',
113
- data: {
114
- credential: vc,
115
- tag: vc.credentialSubject.passport.name,
116
- },
117
- },
118
- ];
119
- }
120
-
121
- sendToUser(receiver, message, sender, { channels: [NOTIFICATION_SEND_CHANNEL.WALLET] }).catch((error) => {
122
- logger.error('Failed send passport vc to wallet', { error });
123
- });
124
- } catch (error) {
125
- logger.error('Failed send passport vc to wallet', { error });
126
- }
127
- };
128
-
129
- const formatTransferData = (data) => ({
130
- transferId: data.id,
131
- remark: data.remark || '',
132
- expireDate: new Date(data.expireDate).toString(),
133
- appDid: data.appDid,
134
- status: data.status || '',
135
- });
136
-
137
- const taggingSchema = Joi.object({
138
- tagId: Joi.number().required(),
139
- taggableIds: Joi.array().items(Joi.string()).min(1).required().messages({
140
- 'any.required': 'Must specify tagging.taggableIds to create',
141
- 'array.base': 'taggableIds must be an array',
142
- 'array.min': 'taggableIds must contain at least one element',
143
- }),
144
- taggableType: Joi.string().required().messages({
145
- 'any.required': 'Must specify tagging.taggableType to create',
146
- 'string.base': 'taggableType must be a string',
147
- }),
148
- });
149
-
150
- const validatePassportDisplay = async (role, display) => {
151
- if (role.extra?.display === 'custom') {
152
- if (!display) {
153
- throw new Error(`display is required for custom passport: ${role.name}`);
154
- }
155
- const { error, value } = passportDisplaySchema.validate(display);
156
- if (error) {
157
- throw new Error(`display invalid for custom passport: ${formatError(error)}`);
158
- }
159
-
160
- if (value.type === 'url') {
161
- try {
162
- const res = await axios.head(value.content, {
163
- timeout: 8000,
164
- maxRedirects: 8,
165
- validateStatus: null,
166
- headers: {
167
- Accept: 'image/avif,image/webp,image/png,image/svg+xml,image/*,*/*;q=0.8',
168
- 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36', // prettier-ignore
169
- },
170
- });
171
- if (res.status >= 400) {
172
- throw new Error(`Passport display is not accessible: ${res.statusText}`);
173
- }
174
- const contentType = res.headers['content-type'];
175
- if (!contentType?.startsWith('image/')) {
176
- throw new Error(`Passport display does not return valid image: ${contentType}`);
177
- }
178
- } catch (err) {
179
- throw new Error(`Error validating passport display: ${err.message}`);
180
- }
181
- }
182
- }
183
- };
184
14
 
185
- const getUserSessionWhere = ({ status, blocklet }) => {
186
- const now = Date.now();
187
- const sessionTtl = blocklet.settings?.session?.ttl || SESSION_TTL;
188
- if (status === USER_SESSION_STATUS.ONLINE) {
189
- return {
190
- updatedAt: {
191
- [Op.gt]: new Date(now - sessionTtl * 1000),
192
- },
193
- status: {
194
- [Op.ne]: USER_SESSION_STATUS.OFFLINE,
195
- },
196
- };
197
- }
198
- if (status === USER_SESSION_STATUS.EXPIRED) {
199
- return {
200
- updatedAt: {
201
- [Op.lt]: new Date(now - sessionTtl * 1000),
202
- },
203
- status: {
204
- [Op.ne]: USER_SESSION_STATUS.OFFLINE,
205
- },
206
- };
207
- }
208
- if (status === USER_SESSION_STATUS.OFFLINE) {
209
- return {
210
- status: USER_SESSION_STATUS.OFFLINE,
211
- };
212
- }
213
- return {};
214
- };
215
-
216
- // 关注取消关注相关接口
217
- const USER_RELATION_ACTIONS = {
218
- follow: (state, followerDid, userDid, options) => state.followUser(followerDid, userDid, options),
219
- unfollow: (state, followerDid, userDid, options) => state.unfollowUser(followerDid, userDid, options),
220
- };
221
-
222
- // 获取用户相关接口
223
- const USER_RELATION_QUERIES = {
224
- followers: (state, userDid, options, context) => state.getFollowers(userDid, options, context),
225
- following: (state, userDid, options, context) => state.getFollowing(userDid, options, context),
226
- };
15
+ // Import split modules
16
+ const tagManager = require('./team/tag-manager');
17
+ const notificationManager = require('./team/notification-manager');
18
+ const rbacManager = require('./team/rbac-manager');
19
+ const accessKeyManager = require('./team/access-key-manager');
20
+ const oauthManager = require('./team/oauth-manager');
21
+ const verifyCodeManager = require('./team/verify-code-manager');
22
+ const storeManager = require('./team/store-manager');
23
+ const sessionManager = require('./team/session-manager');
24
+ const userManager = require('./team/user-manager');
25
+ const invitationManager = require('./team/invitation-manager');
26
+ const passportManager = require('./team/passport-manager');
27
+ const orgManager = require('./team/org-manager');
28
+
29
+ // Re-export helper functions
30
+ const { validatePassportDisplay, sendPassportVcNotification } = passportManager;
227
31
 
228
32
  class TeamAPI extends EventEmitter {
229
33
  /**
230
- *
231
34
  * @param {object} states abtnode core StateDB
232
35
  */
233
36
  constructor({ teamManager, states, dataDirs, nodeAPI, passportAPI, passportIssueQueue }) {
@@ -369,3308 +172,657 @@ class TeamAPI extends EventEmitter {
369
172
  };
370
173
  }
371
174
 
372
- // Tagging
373
- async createTagging({ teamDid, tagging }) {
374
- const state = await this.getTaggingState(teamDid);
375
-
376
- const { error, value } = taggingSchema.validate(tagging, { stripUnknown: true });
377
- if (error) {
378
- throw new Error(error.details.map((d) => d.message).join('; '));
379
- }
175
+ // ============ Tag Manager ============
176
+ createTagging({ teamDid, tagging }) {
177
+ return tagManager.createTagging(this, { teamDid, tagging });
178
+ }
380
179
 
381
- const fns = value.taggableIds.map(async (id) => {
382
- const found = await state.findOne({ tagId: tagging.tagId, taggableId: id });
383
- if (found) return found;
180
+ deleteTagging({ teamDid, tagging }) {
181
+ return tagManager.deleteTagging(this, { teamDid, tagging });
182
+ }
384
183
 
385
- return state.insert({ tagId: tagging.tagId, taggableId: id, taggableType: tagging.taggableType });
386
- });
184
+ getTag({ teamDid, tag }) {
185
+ return tagManager.getTag(this, { teamDid, tag });
186
+ }
387
187
 
388
- const docs = await Promise.all(fns);
389
- logger.info('tagging created successfully', { teamDid, tagging });
390
- return docs;
188
+ createTag({ teamDid, tag }, context = {}) {
189
+ return tagManager.createTag(this, { teamDid, tag }, context);
391
190
  }
392
191
 
393
- async deleteTagging({ teamDid, tagging }) {
394
- const { error, value } = taggingSchema.validate(tagging, { stripUnknown: true });
395
- if (error) {
396
- throw new Error(error.details.map((d) => d.message).join('; '));
397
- }
192
+ updateTag({ teamDid, tag }, context = {}) {
193
+ return tagManager.updateTag(this, { teamDid, tag }, context);
194
+ }
398
195
 
399
- const state = await this.getTaggingState(teamDid);
196
+ deleteTag({ teamDid, tag, moveTo }) {
197
+ return tagManager.deleteTag(this, { teamDid, tag, moveTo });
198
+ }
400
199
 
401
- const { tagId, taggableIds } = value;
402
- const fns = taggableIds.map((id) => {
403
- return state.remove({ tagId, taggableId: id });
404
- });
200
+ getTags({ teamDid, paging }) {
201
+ return tagManager.getTags(this, { teamDid, paging });
202
+ }
405
203
 
406
- const docs = await Promise.all(fns);
407
- logger.info('tagging deleted successfully', { teamDid, tagging });
204
+ // ============ Notification Manager ============
205
+ getNotification(params, context) {
206
+ return notificationManager.getNotification(this, params, context);
207
+ }
408
208
 
409
- return docs;
209
+ getNotificationById(params) {
210
+ return notificationManager.getNotificationById(this, params);
410
211
  }
411
212
 
412
- // Tags
413
- async getTag({ teamDid, tag }) {
414
- const state = await this.getTagState(teamDid);
415
- return state.findOne({ id: tag.id });
213
+ getNotificationsUnreadCount(params, context) {
214
+ return notificationManager.getNotificationsUnreadCount(this, params, context);
416
215
  }
417
216
 
418
- async createTag({ teamDid, tag }, context = {}) {
419
- if (!tag.title) {
420
- throw new Error('Must specify tag.title to create');
421
- }
422
- if (!tag.color || !tag.color.match(/^#[0-9a-f]{6}$/i)) {
423
- throw new Error('Must specify hex encoded tag.color to create');
424
- }
425
- if (!tag.slug) {
426
- throw new Error('Must specify tag.slug to create');
427
- }
217
+ createNotificationReceiver(params) {
218
+ return notificationManager.createNotificationReceiver(this, params);
219
+ }
428
220
 
429
- const state = await this.getTagState(teamDid);
430
- const tagData = pick(tag, ['title', 'description', 'color', 'slug', 'type', 'componentDid', 'parentId']);
431
- tagData.createdBy = context.user.did;
432
- tagData.updatedBy = context.user.did;
433
- const doc = await state.insert(tagData);
434
- logger.info('tags created successfully', { teamDid, tag });
435
- return doc;
221
+ markAllNotificationsAsRead(params, context) {
222
+ return notificationManager.markAllNotificationsAsRead(this, params, context);
436
223
  }
437
224
 
438
- async updateTag({ teamDid, tag }, context = {}) {
439
- if (!tag.id) {
440
- throw new Error('Must specify tag.id to update');
441
- }
225
+ updateNotificationStatus(params, context) {
226
+ return notificationManager.updateNotificationStatus(this, params, context);
227
+ }
442
228
 
443
- const keys = ['title', 'description', 'color', 'slug', 'type', 'componentDid', 'parentId'];
444
- const tagData = Object.fromEntries(keys.map((key) => [key, tag[key] ?? null]));
445
- tagData.updatedBy = context.user.did;
229
+ readNotifications(params, context) {
230
+ return notificationManager.readNotifications(this, params, context);
231
+ }
446
232
 
447
- const state = await this.getTagState(teamDid);
448
- const result = await state.updateById(tag.id, tagData);
449
- logger.info('tags updated successfully', { teamDid, tag, result });
450
- return state.findOne({ id: tag.id });
233
+ unreadNotifications(params, context) {
234
+ return notificationManager.unreadNotifications(this, params, context);
451
235
  }
452
236
 
453
- async deleteTag({ teamDid, tag, moveTo }) {
454
- if (!tag.id) {
455
- throw new Error('Must specify tag.id to delete');
456
- }
237
+ getNotificationSendLog(params) {
238
+ return notificationManager.getNotificationSendLog(this, params);
239
+ }
457
240
 
458
- const state = await this.getTagState(teamDid);
459
- const taggingState = await this.getTaggingState(teamDid);
241
+ getNotificationComponents(params) {
242
+ return notificationManager.getNotificationComponents(this, params);
243
+ }
460
244
 
461
- if (moveTo) {
462
- const records = await taggingState.find({ tagId: tag.id });
463
- const checkPairs = records.map((r) => ({ taggableId: r.taggableId, taggableType: r.taggableType }));
464
- const existing = await taggingState.find({ tagId: moveTo, $or: checkPairs });
245
+ getReceivers(params) {
246
+ return notificationManager.getReceivers(this, params);
247
+ }
465
248
 
466
- const existingSet = new Set(existing.map((e) => `${e.taggableId}:${e.taggableType}`));
467
- const toUpdate = [];
468
- const toDelete = [];
249
+ createNotification(payload) {
250
+ return this.teamManager.createNotification(payload);
251
+ }
469
252
 
470
- for (const record of records) {
471
- const key = `${record.taggableId}:${record.taggableType}`;
472
- if (existingSet.has(key)) {
473
- toDelete.push(record);
474
- logger.info('tag already at moveTo, will delete record', { teamDid, tag, record });
475
- } else {
476
- toUpdate.push(record);
477
- }
478
- }
253
+ // ============ RBAC Manager ============
254
+ getRole({ teamDid, role }) {
255
+ return rbacManager.getRole(this, { teamDid, role });
256
+ }
479
257
 
480
- if (toUpdate.length > 0) {
481
- await Promise.all(
482
- toUpdate.map((record) =>
483
- taggingState
484
- .update(
485
- { tagId: record.tagId, taggableId: record.taggableId, taggableType: record.taggableType },
486
- { $set: { tagId: moveTo } }
487
- )
488
- .then(() => {
489
- logger.info('tag moved successfully', { teamDid, tag, record, moveTo });
490
- })
491
- )
492
- );
493
- }
258
+ createRole({ teamDid, name, description, title, childName, permissions = [], extra, orgId }) {
259
+ return rbacManager.createRole(this, { teamDid, name, description, title, childName, permissions, extra, orgId });
260
+ }
494
261
 
495
- if (toDelete.length > 0) {
496
- await Promise.all(
497
- toDelete.map((record) =>
498
- taggingState.remove(record).then(() => {
499
- logger.info('tagging deleted successfully', { teamDid, tag, record });
500
- })
501
- )
502
- );
503
- }
504
- }
262
+ updateRole({ teamDid, role, orgId }) {
263
+ return rbacManager.updateRole(this, { teamDid, role, orgId });
264
+ }
505
265
 
506
- const doc = await state.remove({ id: tag.id });
507
- await taggingState.remove({ tagId: tag.id });
508
- logger.info('tags deleted successfully', { teamDid, tag });
509
- return doc;
266
+ getPermissions({ teamDid }) {
267
+ return rbacManager.getPermissions(this, { teamDid });
510
268
  }
511
269
 
512
- async getTags({ teamDid, paging }) {
513
- const state = await this.getTagState(teamDid);
514
- const result = await state.paginate({}, { createdAt: -1 }, { pageSize: 20, ...paging });
515
- return { tags: result.list, paging: result.paging };
516
- }
517
-
518
- async sendNewMemberNotification(teamManager, user, nodeInfo) {
519
- if (nodeInfo.nodeOwner && user.did !== nodeInfo.nodeOwner.did) {
520
- const actionPath = '/team/members';
521
- const action =
522
- process.env.NODE_ENV === 'production' ? joinURL(nodeInfo.routing.adminPath, actionPath) : actionPath;
523
- await teamManager.createNotification({
524
- title: 'New member join',
525
- description: `User with Name (${user.fullName}) and DID (${user.did}) has joined this server`,
526
- entityType: 'node',
527
- action,
528
- entityId: user.did,
529
- severity: 'success',
530
- });
531
- }
270
+ createPermission({ teamDid, name, description, extra }) {
271
+ return rbacManager.createPermission(this, { teamDid, name, description, extra });
532
272
  }
533
273
 
534
- // User && Invitation
535
- async loginUser({ teamDid, user, notify = true, force = false }) {
536
- const state = await this.getUserState(teamDid);
537
- const nodeInfo = await this.node.read();
274
+ updatePermission({ teamDid, permission }) {
275
+ return rbacManager.updatePermission(this, { teamDid, permission });
276
+ }
538
277
 
539
- if (user.role === ROLES.OWNER) {
540
- if (teamDid !== nodeInfo.did && !force) {
541
- throw new Error('Cannot add user of owner role');
542
- }
543
- if (await state.count({ role: ROLES.OWNER })) {
544
- throw new Error('The owner already exists');
545
- }
546
- }
278
+ grant({ teamDid, roleName, grantName }) {
279
+ return rbacManager.grant(this, { teamDid, roleName, grantName });
280
+ }
547
281
 
548
- const { _action, ...doc } = await state.loginUser(user);
549
- if (_action === 'update') {
550
- logger.info('user updated successfully', { teamDid, userDid: user.did });
551
- this.emit(TeamEvents.userUpdated, { teamDid, user: doc });
552
- } else if (_action === 'add') {
553
- this.createDefaultOrgForUser({ teamDid, user });
282
+ revoke({ teamDid, roleName, grantName }) {
283
+ return rbacManager.revoke(this, { teamDid, roleName, grantName });
284
+ }
554
285
 
555
- if (teamDid === nodeInfo.did && nodeInfo.nodeOwner && user.did !== nodeInfo.nodeOwner.did && notify) {
556
- await this.sendNewMemberNotification(this.teamManager, user, nodeInfo);
557
- }
286
+ updateGrants({ teamDid, roleName, grantNames }) {
287
+ return rbacManager.updateGrants(this, { teamDid, roleName, grantNames });
288
+ }
558
289
 
559
- logger.info('user added successfully', { teamDid, userDid: user.did, userPk: user.pk, userName: user.fullName });
560
- this.emit(TeamEvents.userAdded, { teamDid, user: doc });
561
- }
562
- return doc;
290
+ deleteRole({ teamDid, name }) {
291
+ return rbacManager.deleteRole(this, { teamDid, name });
563
292
  }
564
293
 
565
- async disconnectUserAccount({ teamDid, connectedAccount }) {
566
- const state = await this.getUserState(teamDid);
567
- const result = await state.disconnectUserAccount(connectedAccount);
294
+ deletePermission({ teamDid, name }) {
295
+ return rbacManager.deletePermission(this, { teamDid, name });
296
+ }
568
297
 
569
- logger.info('user disconnect account successfully', {
570
- teamDid,
571
- connectedAccountDid: connectedAccount.did,
572
- connectedAccountPk: connectedAccount.pk,
573
- connectedAccountProvider: connectedAccount.provider,
574
- });
575
- return result;
298
+ getPermissionsByRole({ teamDid, role }) {
299
+ return rbacManager.getPermissionsByRole(this, { teamDid, role });
576
300
  }
577
301
 
578
- async addUser({ teamDid, user, force = false }) {
579
- const state = await this.getUserState(teamDid);
302
+ hasPermission({ teamDid, role, permission }) {
303
+ return rbacManager.hasPermission(this, { teamDid, role, permission });
304
+ }
580
305
 
581
- if (user.role === ROLES.OWNER) {
582
- const nodeInfo = await this.node.read();
306
+ refreshBlockletInterfacePermissions(blockletMeta) {
307
+ return rbacManager.refreshBlockletInterfacePermissions(this, blockletMeta);
308
+ }
583
309
 
584
- if (teamDid !== nodeInfo.did && !force) {
585
- throw new Error('Cannot add user of owner role');
586
- }
310
+ // ============ Access Key Manager ============
311
+ getAccessKeys({ teamDid, paging }, context) {
312
+ return accessKeyManager.getAccessKeys(this, { teamDid, paging }, context);
313
+ }
587
314
 
588
- if (await state.count({ role: ROLES.OWNER })) {
589
- throw new Error('The owner already exists');
590
- }
591
- }
315
+ getAccessKey({ teamDid, accessKeyId }, context) {
316
+ return accessKeyManager.getAccessKey(this, { teamDid, accessKeyId }, context);
317
+ }
592
318
 
593
- const doc = await state.addUser(user);
319
+ createAccessKey({ teamDid, ...data }, context) {
320
+ return accessKeyManager.createAccessKey(this, { teamDid, ...data }, context);
321
+ }
594
322
 
595
- logger.info('user added successfully', { teamDid, userDid: user.did, userPk: user.pk, userName: user.fullName });
323
+ updateAccessKey({ teamDid, ...data }, context) {
324
+ return accessKeyManager.updateAccessKey(this, { teamDid, ...data }, context);
325
+ }
596
326
 
597
- const nodeInfo = await this.node.read();
598
- if (teamDid === nodeInfo.did && nodeInfo.nodeOwner && user.did !== nodeInfo.nodeOwner.did) {
599
- await this.sendNewMemberNotification(this.teamManager, user, nodeInfo);
600
- }
327
+ deleteAccessKey({ teamDid, accessKeyId }, context) {
328
+ return accessKeyManager.deleteAccessKey(this, { teamDid, accessKeyId }, context);
329
+ }
601
330
 
602
- this.emit(TeamEvents.userAdded, { teamDid, user: doc });
331
+ refreshLastUsed({ teamDid, accessKeyId }, context) {
332
+ return accessKeyManager.refreshLastUsed(this, { teamDid, accessKeyId }, context);
333
+ }
603
334
 
604
- return doc;
335
+ verifyAccessKey({ teamDid, accessKey }) {
336
+ return accessKeyManager.verifyAccessKey(this, { teamDid, accessKey });
605
337
  }
606
338
 
607
- async getUsers({ teamDid, query, paging: inputPaging, sort, dids }, context = {}) {
608
- const state = await this.getUserState(teamDid);
609
- const nodeInfo = await this.node.read();
339
+ // ============ OAuth Manager ============
340
+ getOAuthClients({ teamDid, paging }) {
341
+ return oauthManager.getOAuthClients(this, { teamDid, paging });
342
+ }
610
343
 
611
- // HACK: 如果查询的是 server 的数据库,则不应该查询 userSession 表
612
- if (teamDid === nodeInfo.did) {
613
- if (query) {
614
- delete query.includeUserSessions;
615
- }
616
- }
344
+ deleteOAuthClient({ teamDid, clientId }) {
345
+ return oauthManager.deleteOAuthClient(this, { teamDid, clientId });
346
+ }
617
347
 
618
- if (inputPaging?.pageSize > MAX_USER_PAGE_SIZE) {
619
- throw new Error(`Length of users should not exceed ${MAX_USER_PAGE_SIZE} per page`);
620
- }
348
+ createOAuthClient({ teamDid, input }, context) {
349
+ return oauthManager.createOAuthClient(this, { teamDid, input }, context);
350
+ }
621
351
 
622
- if (dids && dids.length > MAX_USER_PAGE_SIZE) {
623
- throw new Error(`Length of users should not exceed ${MAX_USER_PAGE_SIZE}`);
624
- }
352
+ updateOAuthClient({ teamDid, input }, context) {
353
+ return oauthManager.updateOAuthClient(this, { teamDid, input }, context);
354
+ }
625
355
 
626
- let list;
627
- let paging;
628
-
629
- if (dids) {
630
- list = await state.getUsersByDids({ query, dids }, context);
631
- paging = {
632
- total: list.length,
633
- pageSize: dids.length,
634
- pageCount: 1,
635
- page: 1,
636
- };
637
- } else {
638
- const doc = await state.getUsers({ query, sort, paging: { pageSize: 20, ...inputPaging } }, context);
639
- list = doc.list;
640
- paging = doc.paging;
641
- }
356
+ // ============ Verify Code Manager ============
357
+ createVerifyCode({ teamDid, subject, scope, purpose }) {
358
+ return verifyCodeManager.createVerifyCode(this, { teamDid, subject, scope, purpose });
359
+ }
642
360
 
643
- let userSessionsCountList = [];
644
- if (teamDid !== nodeInfo.did && query?.includeUserSessions) {
645
- userSessionsCountList = await pMap(
646
- list,
647
- async (x) => {
648
- const { count: userSessionsCount } = await this.getUserSessionsCount({
649
- teamDid,
650
- query: {
651
- userDid: x.did,
652
- appPid: teamDid,
653
- },
654
- });
655
- return userSessionsCount;
656
- },
657
- { concurrency: 10 }
658
- );
659
- }
660
- const users = list.map(
661
- (d, index) => {
662
- const pickData = pick(d, [
663
- 'did',
664
- 'pk',
665
- 'role',
666
- 'email',
667
- 'phone',
668
- 'fullName',
669
- 'approved',
670
- 'createdAt',
671
- 'updatedAt',
672
- 'passports',
673
- 'firstLoginAt',
674
- 'lastLoginAt',
675
- 'lastLoginIp',
676
- 'remark',
677
- 'avatar',
678
- 'locale',
679
- 'tags',
680
- 'url',
681
- 'inviter',
682
- 'generation',
683
- 'emailVerified',
684
- 'phoneVerified',
685
- // oauth relate fields
686
- 'sourceProvider',
687
- 'sourceAppPid',
688
- 'connectedAccounts',
689
-
690
- 'metadata',
691
- 'isFollowing',
692
- ]);
361
+ consumeVerifyCode({ teamDid, code }) {
362
+ return verifyCodeManager.consumeVerifyCode(this, { teamDid, code });
363
+ }
693
364
 
694
- return {
695
- ...pickData,
696
- userSessions: [],
697
- userSessionsCount: userSessionsCountList[index] || 0,
698
- };
699
- }
365
+ issueVerifyCode({ teamDid, code }) {
366
+ return verifyCodeManager.issueVerifyCode(this, { teamDid, code });
367
+ }
700
368
 
701
- // eslint-disable-next-line function-paren-newline
702
- );
703
- return {
704
- // FIXME: @zhanghan 这里做字段过滤的目的是?gql 本身已经对字段做了过滤了
705
- users,
706
- paging,
707
- };
369
+ sendVerifyCode({ teamDid, code }) {
370
+ return verifyCodeManager.sendVerifyCode(this, { teamDid, code });
708
371
  }
709
372
 
710
- async getUsersCount({ teamDid }) {
711
- const state = await this.getUserState(teamDid);
712
- return state.count();
373
+ isSubjectVerified({ teamDid, subject }) {
374
+ return verifyCodeManager.isSubjectVerified(this, { teamDid, subject });
713
375
  }
714
376
 
715
- async getUsersCountPerRole({ teamDid }) {
716
- const roles = await this.getRoles({ teamDid });
717
- const state = await this.getUserState(teamDid);
718
- const names = ['$all', ...roles.filter((x) => !x.orgId).map((x) => x.name), '$none', '$blocked'];
719
- return Promise.all(
720
- names.map(async (name) => {
721
- const count = await state.countByPassport({ name, status: PASSPORT_STATUS.VALID });
722
- return { key: name, value: count };
723
- })
724
- );
377
+ isSubjectIssued({ teamDid, userDid, subject }) {
378
+ return verifyCodeManager.isSubjectIssued(this, { teamDid, userDid, subject });
725
379
  }
726
380
 
727
- /**
728
- * @description
729
- * @param {{
730
- * teamDid: string,
731
- * user: {
732
- * did: string,
733
- * },
734
- * options: Record<string, any>
735
- * }} { teamDid, user, options = {} }
736
- * @return {Promise<import('@abtnode/models').UserState>}
737
- * @memberof TeamAPI
738
- */
739
- async getUser({ teamDid, user, options = {} }) {
740
- const state = await this.getUserState(teamDid);
741
- return state.getUser(user.did, options);
381
+ isSubjectSent({ teamDid, subject }) {
382
+ return verifyCodeManager.isSubjectSent(this, { teamDid, subject });
742
383
  }
743
384
 
744
- async getConnectedAccount({ teamDid, id }) {
745
- const state = await this.getUserState(teamDid);
746
- return state.getConnectedAccount(id);
385
+ getVerifyCode({ teamDid, code, id }) {
386
+ return verifyCodeManager.getVerifyCode(this, { teamDid, code, id });
747
387
  }
748
388
 
749
- async isEmailUsed({ teamDid, email, verified = false, sourceProvider = '' }) {
750
- const state = await this.getUserState(teamDid);
751
- return state.isEmailUsed(email, verified, sourceProvider);
389
+ rotateSessionKey({ teamDid }) {
390
+ return verifyCodeManager.rotateSessionKey(this, { teamDid });
752
391
  }
753
392
 
754
- async isPhoneUsed({ teamDid, phone, verified = false }) {
755
- const state = await this.getUserState(teamDid);
756
- return state.isPhoneUsed(phone, verified);
393
+ // ============ Store Manager ============
394
+ addStore({ teamDid, url, scope }, context) {
395
+ return storeManager.addStore(this, { teamDid, url, scope }, context);
757
396
  }
758
397
 
759
- async getUserByDid({ teamDid, userDid, attributes = [] }) {
760
- const state = await this.getUserState(teamDid);
761
- return state.getUserByDid(userDid, attributes);
398
+ addEndpoint({ teamDid, url }, context) {
399
+ return storeManager.addEndpoint(this, { teamDid, url }, context);
762
400
  }
763
401
 
764
- async isUserValid({ teamDid, userDid }) {
765
- const state = await this.getUserState(teamDid);
766
- return state.isUserValid(userDid);
402
+ deleteEndpoint({ teamDid, did, projectId }, context) {
403
+ return storeManager.deleteEndpoint(this, { teamDid, did, projectId }, context);
767
404
  }
768
405
 
769
- async isPassportValid({ teamDid, passportId }) {
770
- const state = await this.getUserState(teamDid);
771
- return state.isPassportValid(passportId);
406
+ existsStore({ teamDid, url, scope }, context) {
407
+ return storeManager.existsStore(this, { teamDid, url, scope }, context);
772
408
  }
773
409
 
774
- async isConnectedAccount({ teamDid, did }) {
775
- const state = await this.getUserState(teamDid);
776
- return state.isConnectedAccount(did);
410
+ deleteStore({ teamDid, url, projectId, scope }, context) {
411
+ return storeManager.deleteStore(this, { teamDid, url, projectId, scope }, context);
777
412
  }
778
413
 
779
- async getOwner({ teamDid }) {
780
- let owner = await this.teamManager.getOwner(teamDid);
781
- const state = await this.getUserState(teamDid);
782
- if (!owner) {
783
- const ownerDids = await state.getOwnerDids();
784
- if (ownerDids.length > 0) {
785
- owner = { did: ownerDids[0] };
786
- }
787
- }
414
+ // ============ Session Manager ============
415
+ getUserSession(params) {
416
+ return sessionManager.getUserSession(this, params);
417
+ }
788
418
 
789
- if (!owner) {
790
- return null;
791
- }
419
+ getUserSessions(params) {
420
+ return sessionManager.getUserSessions(this, params);
421
+ }
792
422
 
793
- // NOTICE: 目前 owner 只能为 did-wallet,无需查询 connectedAccount
794
- const full = await state.getUser(owner.did);
795
- return full || owner;
423
+ getUserSessionsCount(params) {
424
+ return sessionManager.getUserSessionsCount(this, params);
796
425
  }
797
426
 
798
- async userFollowAction({ teamDid, userDid, followerDid, action = 'follow', options = {} }, context = {}) {
799
- const targetDid = followerDid || context.userDid;
427
+ getPassportById(params) {
428
+ return sessionManager.getPassportById(this, params);
429
+ }
800
430
 
801
- if (!USER_RELATION_ACTIONS[action]) {
802
- throw new CustomError(
803
- 400,
804
- `Invalid action: ${action}. Supported: ${Object.keys(USER_RELATION_ACTIONS).join(', ')}`
805
- );
806
- }
807
- let result;
808
- try {
809
- const state = await this.getUserState(teamDid);
810
- result = await USER_RELATION_ACTIONS[action](state, targetDid, userDid, options);
811
- } catch (error) {
812
- logger.error(`Failed to ${action} user`, {
813
- followerDid: targetDid,
814
- userDid,
815
- error,
816
- });
817
- throw error;
818
- }
431
+ getPassportFromFederated(params) {
432
+ return sessionManager.getPassportFromFederated(this, params);
433
+ }
819
434
 
820
- try {
821
- if (action === 'follow' && !options.skipNotification) {
822
- const currentUser = await this.getUser({ teamDid, user: { did: targetDid } });
823
- await this.createNotification({
824
- teamDid,
825
- receiver: userDid,
826
- title: 'Followed',
827
- description: `<${currentUser.fullName}(did:abt:${currentUser.did})> followed you`,
828
- activity: {
829
- type: 'follow',
830
- actor: followerDid,
831
- target: {
832
- type: 'user',
833
- id: followerDid,
834
- },
835
- },
836
- entityId: teamDid,
837
- source: 'system',
838
- severity: 'success',
839
- });
840
- }
841
- } catch (error) {
842
- // 消息创建失败,不影响 follow 操作
843
- logger.error('Failed to create notification', { error });
844
- }
435
+ upsertUserSession(params) {
436
+ return sessionManager.upsertUserSession(this, params);
437
+ }
845
438
 
846
- return result;
439
+ syncUserSession(params) {
440
+ return sessionManager.syncUserSession(this, params);
847
441
  }
848
442
 
849
- async getUserFollows({ teamDid, userDid, type = 'following', paging, sort, options = {} }, context = {}) {
850
- const state = await this.getUserState(teamDid);
443
+ logoutUser(params) {
444
+ return sessionManager.logoutUser(this, params);
445
+ }
851
446
 
852
- const blocklet = await getBlocklet({ did: teamDid, states: this.states, dataDirs: this.dataDirs });
447
+ // ============ User Manager ============
448
+ loginUser(params, context) {
449
+ return userManager.loginUser(this, params, context);
450
+ }
853
451
 
854
- if (paging?.pageSize > MAX_USER_PAGE_SIZE) {
855
- throw new Error(`Length of users should not exceed ${MAX_USER_PAGE_SIZE} per page`);
856
- }
452
+ disconnectUserAccount(params, context) {
453
+ return userManager.disconnectUserAccount(this, params, context);
454
+ }
857
455
 
858
- if (!USER_RELATION_QUERIES[type]) {
859
- throw new CustomError(400, `Invalid type: ${type}. Supported: ${Object.keys(USER_RELATION_QUERIES).join(', ')}`);
860
- }
456
+ addUser(params, context) {
457
+ return userManager.addUser(this, params, context);
458
+ }
861
459
 
862
- try {
863
- const { list, paging: resultPaging } = await USER_RELATION_QUERIES[type](
864
- state,
865
- userDid,
866
- {
867
- paging,
868
- sort,
869
- ...options,
870
- },
871
- context
872
- );
873
-
874
- list.forEach((item) => {
875
- const { user } = item;
876
- if (user && user?.avatar && user?.avatar?.startsWith(USER_AVATAR_URL_PREFIX)) {
877
- user.avatar = `${getFederatedUserAvatarUrl(user.avatar, blocklet)}?imageFilter=resize&w=48&h=48`;
878
- }
879
- });
880
-
881
- return {
882
- data: list,
883
- paging: resultPaging,
884
- };
885
- } catch (error) {
886
- logger.error('Failed to get user follows', { error });
887
- throw error;
888
- }
460
+ getUsers(params, context) {
461
+ return userManager.getUsers(this, params, context);
889
462
  }
890
463
 
891
- async getUserFollowStats({ teamDid, userDids, options = {} }, context = {}) {
892
- const state = await this.getUserState(teamDid);
893
- const info = await this.node.read();
894
- const prefix = process.env.NODE_ENV === 'production' ? info.routing.adminPath : '';
895
- return state.getFollowStats({ userDids, teamDid, prefix, options }, context);
464
+ getUsersCount(params) {
465
+ return userManager.getUsersCount(this, params);
896
466
  }
897
467
 
898
- async checkFollowing({ teamDid, userDids = [], followerDid }) {
899
- if (!followerDid) {
900
- throw new CustomError(400, 'Follower DID is required');
901
- }
902
- const state = await this.getUserState(teamDid);
903
- return state.isFollowing(followerDid, userDids);
468
+ getUsersCountPerRole(params) {
469
+ return userManager.getUsersCountPerRole(this, params);
904
470
  }
905
471
 
906
- async getUserInvites({ teamDid, userDid, paging, sort, options = {} }, context = {}) {
907
- try {
908
- if (paging?.pageSize > MAX_USER_PAGE_SIZE) {
909
- throw new CustomError(400, `Length of users should not exceed ${MAX_USER_PAGE_SIZE} per page`);
910
- }
911
- const state = await this.getUserState(teamDid);
912
- const { users, paging: resultPaging } = await state.getInvitees(userDid, { paging, sort, ...options }, context);
913
- // 处理头像
914
- const blocklet = await getBlocklet({ did: teamDid, states: this.states, dataDirs: this.dataDirs });
915
- users.forEach((user) => {
916
- if (user && user?.avatar && user?.avatar?.startsWith(USER_AVATAR_URL_PREFIX)) {
917
- user.avatar = `${getFederatedUserAvatarUrl(user.avatar, blocklet)}?imageFilter=resize&w=48&h=48`;
918
- }
919
- });
920
- return {
921
- users,
922
- paging: resultPaging,
923
- };
924
- } catch (error) {
925
- logger.error('Failed to get user invites', { teamDid, userDid, error });
926
- throw error;
927
- }
472
+ getUser(params, context) {
473
+ return userManager.getUser(this, params, context);
928
474
  }
929
475
 
930
- async createWebhookDisabledNotification({ teamDid, userDid, webhook, action = '', locale = 'en' }) {
931
- const notification = {
932
- en: {
933
- title: 'Your webhook has been temporarily disabled',
934
- description: `Your Webhook has been temporarily disabled after ${WEBHOOK_CONSECUTIVE_FAILURES_THRESHOLD} consecutive failed message delivery attempts. Please check its availability and you can click the button below to reactivate it if needed.`,
935
- failureCount: 'Number of failures',
936
- failureUrl: 'Failed Webhook URL',
937
- },
938
- zh: {
939
- title: '您的 webhook 已被暂时禁用',
940
- description: `您的 Webhook 在连续 ${WEBHOOK_CONSECUTIVE_FAILURES_THRESHOLD} 次消息投递失败后已被暂时禁用。请检查其可用性,如果需要重新激活,请点击下方按钮。`,
941
- failureCount: '失败次数',
942
- failureUrl: '失败的 Webhook URL',
943
- },
944
- };
476
+ getConnectedAccount(params, context) {
477
+ return userManager.getConnectedAccount(this, params, context);
478
+ }
945
479
 
946
- try {
947
- const nodeInfo = await this.node.read();
948
- // server 的 webhook 页面地址
949
- const actionPath = '/settings/integration';
950
- let _action =
951
- process.env.NODE_ENV === 'production' ? joinURL(nodeInfo.routing.adminPath, actionPath) : actionPath;
952
- const channels = [NOTIFICATION_SEND_CHANNEL.WALLET, NOTIFICATION_SEND_CHANNEL.PUSH];
953
- if (teamDid && !this.teamManager.isNodeTeam(teamDid)) {
954
- channels.push(NOTIFICATION_SEND_CHANNEL.EMAIL);
955
- _action = action || `${WELLKNOWN_SERVICE_PATH_PREFIX}/user/settings`; // 默认返回用户中心页面设置页面
956
- }
957
- await this.teamManager.createNotification({
958
- title: notification[locale].title,
959
- description: notification[locale].description,
960
- attachments: [
961
- {
962
- type: 'section',
963
- fields: [
964
- {
965
- type: 'text',
966
- data: {
967
- type: 'plain',
968
- color: '#9397A1',
969
- text: notification[locale].failureUrl,
970
- },
971
- },
972
- {
973
- type: 'text',
974
- data: {
975
- type: 'plain',
976
- color: '#9397A1',
977
- text: webhook.url,
978
- },
979
- },
980
- {
981
- type: 'text',
982
- data: {
983
- type: 'plain',
984
- color: '#9397A1',
985
- text: notification[locale].failureCount,
986
- },
987
- },
988
- {
989
- type: 'text',
990
- data: {
991
- type: 'plain',
992
- color: '#9397A1',
993
- text: `${WEBHOOK_CONSECUTIVE_FAILURES_THRESHOLD}`,
994
- },
995
- },
996
- ],
997
- },
998
- ],
999
- source: 'system',
1000
- severity: 'warning',
1001
- action: _action,
1002
- channels,
1003
- ...(teamDid ? { teamDid } : {}),
1004
- ...(userDid ? { receiver: userDid } : {}),
1005
- });
1006
- } catch (error) {
1007
- logger.error('Failed to create webhook disabled notification', { teamDid, userDid, webhook, error });
1008
- }
480
+ isEmailUsed(params) {
481
+ return userManager.isEmailUsed(this, params);
1009
482
  }
1010
483
 
1011
- async updateWebHookState({ teamDid, userDids, webhook, enabled, consecutiveFailures }) {
1012
- const state = await this.getUserState(teamDid);
1013
- return Promise.all(
1014
- userDids.map(async (userDid) => {
1015
- const webhookParams =
1016
- consecutiveFailures === undefined
1017
- ? omit(
1018
- {
1019
- ...webhook,
1020
- enabled: enabled ?? webhook.enabled,
1021
- },
1022
- ['consecutiveFailures']
1023
- )
1024
- : {
1025
- ...webhook,
1026
- enabled: enabled ?? webhook.enabled,
1027
- consecutiveFailures,
1028
- };
1029
- await state.updateWebhook(userDid, webhookParams, (updated) => {
1030
- this.createWebhookDisabledNotification({ teamDid, userDid, webhook: updated });
1031
- });
1032
- })
1033
- );
484
+ isPhoneUsed(params) {
485
+ return userManager.isPhoneUsed(this, params);
1034
486
  }
1035
487
 
1036
- async updateUser({ teamDid, user }) {
1037
- const state = await this.getUserState(teamDid);
1038
- const exist = await state.getUserByDid(user.did);
1039
- const updated = await state.updateUser(user.did, user);
1040
- logger.info('user updated successfully', { teamDid, userDid: user.did });
1041
-
1042
- this.emit(TeamEvents.userUpdated, { teamDid, user: updated });
1043
- const kycChanged =
1044
- (typeof user.emailVerified === 'boolean' && user.emailVerified !== exist.emailVerified) ||
1045
- (typeof user.phoneVerified === 'boolean' && user.phoneVerified !== exist.phoneVerified);
1046
- if (kycChanged) {
1047
- logger.info('user updated with kycChanged', { teamDid, userDid: user.did });
1048
- this.emit(TeamEvents.userPermissionUpdated, { teamDid, user: updated });
1049
- }
488
+ getUserByDid(params) {
489
+ return userManager.getUserByDid(this, params);
490
+ }
1050
491
 
1051
- return updated;
492
+ isUserValid(params) {
493
+ return userManager.isUserValid(this, params);
1052
494
  }
1053
495
 
1054
- updateUserAddress(args) {
1055
- if (!args.address) {
1056
- throw new Error('address should not be empty');
1057
- }
496
+ isPassportValid(params) {
497
+ return userManager.isPassportValid(this, params);
498
+ }
1058
499
 
1059
- return this.updateUser({ teamDid: args.teamDid, user: pick(args, ['did', 'address']) });
500
+ isConnectedAccount(params) {
501
+ return userManager.isConnectedAccount(this, params);
1060
502
  }
1061
503
 
1062
- async updateUserTags({ teamDid, did, tags }) {
1063
- const state = await this.getUserState(teamDid);
1064
- const doc = await state.updateTags(did, tags);
1065
- logger.info('user tags updated successfully', { teamDid, userDid: did, tags });
1066
- this.emit(TeamEvents.userUpdated, { teamDid, user: doc });
1067
- return doc;
504
+ getOwner(params) {
505
+ return userManager.getOwner(this, params);
1068
506
  }
1069
507
 
1070
- async removeUser({ teamDid, user }) {
1071
- const { did } = user;
508
+ userFollowAction(params, context) {
509
+ return userManager.userFollowAction(this, params, context);
510
+ }
1072
511
 
1073
- if (!did) {
1074
- throw new Error('did does not exist');
1075
- }
512
+ getUserFollows(params, context) {
513
+ return userManager.getUserFollows(this, params, context);
514
+ }
1076
515
 
1077
- if (user.role === ROLES.OWNER) {
1078
- throw new Error('Cannot delete owner');
1079
- }
516
+ getUserFollowStats(params, context) {
517
+ return userManager.getUserFollowStats(this, params, context);
518
+ }
1080
519
 
1081
- const state = await this.getUserState(teamDid);
1082
- await state.remove({ did });
1083
- logger.info('user removed successfully', { teamDid, userDid: did });
1084
- this.emit(TeamEvents.userRemoved, { teamDid, user: { did } });
520
+ checkFollowing(params) {
521
+ return userManager.checkFollowing(this, params);
522
+ }
1085
523
 
1086
- return { did };
524
+ getUserInvites(params) {
525
+ return userManager.getUserInvites(this, params);
1087
526
  }
1088
527
 
1089
- async updateUserApproval({ teamDid, user, options: { includeFederated = true } = {} }) {
1090
- const state = await this.getUserState(teamDid);
1091
- const userDoc = await state.getUser(user.did);
528
+ updateUser(params, context) {
529
+ return userManager.updateUser(this, params, context);
530
+ }
1092
531
 
1093
- if (!userDoc) {
1094
- throw new Error('User does not exist');
1095
- }
532
+ updateUserAddress(params, context) {
533
+ return userManager.updateUserAddress(this, params, context);
534
+ }
1096
535
 
1097
- if (hasActiveOwnerPassport(userDoc)) {
1098
- throw new Error('Cannot update owner\'s approval'); // prettier-ignore
1099
- }
536
+ updateUserTags(params, context) {
537
+ return userManager.updateUserTags(this, params, context);
538
+ }
1100
539
 
1101
- const changeData = pick(user, ['did', 'approved']);
540
+ removeUser(params, context) {
541
+ return userManager.removeUser(this, params, context);
542
+ }
1102
543
 
1103
- const result = await state.updateApproved(changeData);
544
+ updateUserApproval(params, context) {
545
+ return userManager.updateUserApproval(this, params, context);
546
+ }
1104
547
 
1105
- logger.info('user approval updated successfully', { teamDid, userDid: user.did, approved: user.approved });
548
+ updateWebHookState(params) {
549
+ return userManager.updateWebHookState(this, params);
550
+ }
1106
551
 
1107
- if (changeData?.approved === false) {
1108
- // 需要注销指定 userDid 在当前 member 和 master 中的所有 userSession
1109
- await this.logoutUser({ teamDid, userDid: user.did });
552
+ createWebhookDisabledNotification(params) {
553
+ return userManager.createWebhookDisabledNotification(this, params);
554
+ }
1110
555
 
1111
- // 需要禁用用户的所有 passport
1112
- await state.revokePassportByUserDid({ did: user.did });
556
+ async switchProfile({ teamDid, userDid, profile }) {
557
+ const state = await this.getUserState(teamDid);
558
+ // NOTICE: 这个 schema 没有对数据做任何 default 操作,所以没必要用 validate 之后的值
559
+ const { error } = profileSchema.validate({ ...profile, did: userDid });
560
+ if (error) {
561
+ throw new Error(error);
1113
562
  }
1114
- if (includeFederated) {
1115
- const nodeInfo = await this.node.read();
1116
- // 只有 blocklet 需要执行这个操作
1117
- if (nodeInfo.did !== teamDid) {
1118
- const blocklet = await getBlocklet({
1119
- did: teamDid,
1120
- states: this.states,
1121
- dataDirs: this.dataDirs,
1122
- useCache: true,
1123
- });
1124
- syncFederated({
1125
- nodeInfo,
1126
- blocklet,
1127
- data: {
1128
- users: [
1129
- {
1130
- action: user.approved ? 'unban' : 'ban',
1131
- did: user.did,
1132
- sourceAppPid: userDoc.sourceAppPid,
1133
- },
1134
- ],
1135
- },
1136
- });
563
+ const oldUser = await state.getUser(userDid);
564
+ if (!oldUser) {
565
+ throw new Error('User is not exist', { userDid, teamDid });
566
+ }
567
+
568
+ const mergeData = { ...profile };
569
+
570
+ if (mergeData.avatar) {
571
+ if (mergeData.avatar.startsWith(USER_AVATAR_URL_PREFIX)) {
572
+ // pass
573
+ } else if (isUrl(mergeData.avatar)) {
574
+ try {
575
+ mergeData.avatar = await getAvatarByUrl(mergeData.avatar);
576
+ const blocklet = await getBlocklet({ did: teamDid, states: this.states, dataDirs: this.dataDirs });
577
+ mergeData.avatar = await extractUserAvatar(mergeData.avatar, { dataDir: blocklet.env.dataDir });
578
+ } catch {
579
+ logger.error('Failed to convert external avatar', { teamDid, userDid, avatar: mergeData.avatar });
580
+ throw new Error('Failed to convert external avatar');
581
+ }
582
+ } else {
583
+ // 仅当 avatar 是 base64 时,对 avatar 进行处理
584
+ const regex = /^data:image\/(\w+);base64,/;
585
+ const match = regex.exec(mergeData.avatar);
586
+ if (match) {
587
+ const blocklet = await getBlocklet({ did: teamDid, states: this.states, dataDirs: this.dataDirs });
588
+ mergeData.avatar = await extractUserAvatar(mergeData.avatar, { dataDir: blocklet.env.dataDir });
589
+ } else {
590
+ logger.error('Profile avatar is invalid', { teamDid, userDid, profile });
591
+ throw new Error('Profile avatar is invalid');
592
+ }
1137
593
  }
1138
594
  }
1139
- this.emit(TeamEvents.userUpdated, { teamDid, user: result });
1140
- this.emit(TeamEvents.userPermissionUpdated, { teamDid, user: result });
595
+ const doc = await state.updateUser(userDid, mergeData);
596
+ logger.info('User switch-profile successfully', { teamDid, userDid });
597
+ this.emit(TeamEvents.userUpdated, { teamDid, user: doc });
598
+ this.emit(TeamEvents.userProfileUpdated, { teamDid, user: doc });
599
+ return doc;
600
+ }
1141
601
 
1142
- return result;
602
+ // ============ Invitation Manager ============
603
+ createMemberInvitation(params, context) {
604
+ return invitationManager.createMemberInvitation(this, params, context, { validatePassportDisplay });
1143
605
  }
1144
606
 
1145
- /**
1146
- * @param {object} options
1147
- * @param {string} options.teamDid
1148
- * @param {string} options.userDid
1149
- * @param {string} options.role
1150
- * @param {object} [options.display]
1151
- * @param {string} options.display.type
1152
- * @param {string} options.display.content
1153
- * @param {boolean} [options.notify=true]
1154
- * @param {object} [options.notification]
1155
- */
1156
- // 前端用上了,目前是在后端使用
1157
- async issuePassportToUser(
1158
- { teamDid, userDid, role: roleName, display = null, notify = true, notification, action = '' },
1159
- context = {}
1160
- ) {
1161
- try {
1162
- // eslint-disable-next-line no-param-reassign
1163
- notification = JSON.parse(notification);
1164
- } catch {
1165
- logger.error('Failed to parse notification, use notification as undefined', { notification });
1166
- }
1167
- const { locale = 'en' } = context;
607
+ getInvitation({ teamDid, inviteId }) {
608
+ return invitationManager.getInvitation(this, { teamDid, inviteId });
609
+ }
1168
610
 
1169
- const userState = await this.getUserState(teamDid);
1170
- // NOTICE: issuePassportToUser 必须传入 wallet did,无需查询 connectedAccount
1171
- const user = await userState.getUser(userDid);
611
+ getInvitations({ teamDid, filter, orgId }, context) {
612
+ return invitationManager.getInvitations(this, { teamDid, filter, orgId }, context);
613
+ }
1172
614
 
1173
- if (!user) {
1174
- throw new Error(`user does not exist: ${userDid}`);
1175
- }
615
+ deleteInvitation({ teamDid, inviteId }) {
616
+ return invitationManager.deleteInvitation(this, { teamDid, inviteId });
617
+ }
1176
618
 
1177
- if (!user.approved) {
1178
- throw new Error(`the user is revoked: ${userDid}`);
1179
- }
619
+ checkInvitation({ teamDid, inviteId }) {
620
+ return invitationManager.checkInvitation(this, { teamDid, inviteId });
621
+ }
1180
622
 
1181
- const rbac = await this.getRBAC(teamDid);
1182
- const role = await rbac.getRole(roleName);
623
+ closeInvitation(params) {
624
+ return invitationManager.closeInvitation(this, params);
625
+ }
1183
626
 
1184
- if (!role) {
1185
- throw new Error(`passport does not exist: ${roleName}`);
1186
- }
627
+ createTransferInvitation({ teamDid, remark }, context = {}) {
628
+ return invitationManager.createTransferInvitation(this, { teamDid, remark }, context, { validatePassportDisplay });
629
+ }
1187
630
 
1188
- await validatePassportDisplay(role, display);
1189
-
1190
- // create vc
1191
- const nodeInfo = await this.node.read();
1192
- const {
1193
- wallet,
1194
- passportColor,
1195
- appUrl,
1196
- name: issuerName,
1197
- } = await getApplicationInfo({
1198
- teamDid,
1199
- nodeInfo,
1200
- node: {
1201
- dataDirs: this.dataDirs,
1202
- getSessionSecret: this.nodeAPI?.getSessionSecret?.bind(this.nodeAPI) || (() => 'abc'),
1203
- getBlocklet: () => getBlocklet({ did: teamDid, states: this.states, dataDirs: this.dataDirs }),
1204
- },
1205
- });
631
+ createTransferAppOwnerSession({ appDid, remark }) {
632
+ return invitationManager.createTransferAppOwnerSession(this, { appDid, remark });
633
+ }
1206
634
 
1207
- const result = await createPassport({ role });
1208
- const vc = await createPassportVC({
1209
- issuerName,
1210
- issuerWallet: wallet,
1211
- issuerAvatarUrl: getAppAvatarUrl(appUrl),
1212
- ownerDid: userDid,
1213
- endpoint: getPassportStatusEndpoint({
1214
- baseUrl: joinURL(appUrl, WELLKNOWN_SERVICE_PATH_PREFIX),
1215
- userDid: user.did,
1216
- teamDid,
1217
- }),
1218
- ownerProfile: {
1219
- ...user,
1220
- avatar: getUserAvatarUrl(appUrl, user.avatar),
1221
- },
1222
- preferredColor: passportColor,
1223
- passport: result.passport,
1224
- types: result.types,
1225
- purpose: teamDid === nodeInfo.did || isEmpty(result.types) ? 'login' : 'verification',
1226
- display,
1227
- });
1228
-
1229
- // write passport to db
1230
- const passport = createUserPassport(vc, { role: role.name, display, source: PASSPORT_SOURCE.ISSUE });
1231
-
1232
- user.passports = upsertToPassports(user.passports || [], passport);
1233
- await this.updateUser({
1234
- teamDid,
1235
- user: {
1236
- did: user.did,
1237
- pk: user.pk,
1238
- passports: user.passports,
1239
- },
1240
- });
1241
-
1242
- if (passport && this.passportAPI) {
1243
- await this.passportAPI.createPassportLog(teamDid, {
1244
- passportId: passport.id,
1245
- action: PASSPORT_LOG_ACTION.ISSUE,
1246
- operatorDid: userDid,
1247
- metadata: {
1248
- action: action || 'issue-passport-to-user',
1249
- },
1250
- operatorIp: context?.ip || '',
1251
- operatorUa: context?.ua || '',
1252
- });
1253
- }
1254
-
1255
- // send vc to wallet
1256
- if (notify) {
1257
- sendPassportVcNotification({ userDid, appWallet: wallet, locale, vc, notification });
1258
- }
1259
-
1260
- return user;
1261
- }
1262
-
1263
- async revokeUserPassport({ teamDid, userDid, passportId } = {}, context = {}) {
1264
- if (!passportId) {
1265
- throw new Error('Revoked passport should not be empty');
1266
- }
1267
-
1268
- const state = await this.getUserState(teamDid);
1269
-
1270
- const doc = await state.revokePassportById({ did: userDid, id: passportId });
1271
-
1272
- // create passport log
1273
- if (this.passportAPI) {
1274
- await this.passportAPI.createPassportLog(teamDid, {
1275
- passportId,
1276
- action: PASSPORT_LOG_ACTION.REVOKE,
1277
- operatorDid: userDid,
1278
- metadata: {
1279
- operator: await state.getUser(userDid).catch(() => null),
1280
- },
1281
- operatorIp: context?.ip || '',
1282
- operatorUa: context?.ua || '',
1283
- });
1284
- }
1285
-
1286
- logger.info('user passport revoked successfully', { teamDid, userDid, passportId });
1287
-
1288
- this.emit(TeamEvents.userUpdated, { teamDid, user: doc });
1289
- this.emit(TeamEvents.userPermissionUpdated, { teamDid, user: doc });
1290
-
1291
- return doc;
1292
- }
1293
-
1294
- async enableUserPassport({ teamDid, userDid, passportId } = {}, context) {
1295
- if (!passportId) {
1296
- throw new Error('Passport should not be empty');
1297
- }
1298
-
1299
- const state = await this.getUserState(teamDid);
1300
-
1301
- const doc = await state.enablePassportById({ did: userDid, id: passportId });
1302
-
1303
- if (this.passportAPI) {
1304
- await this.passportAPI.createPassportLog(teamDid, {
1305
- passportId,
1306
- action: PASSPORT_LOG_ACTION.APPROVE,
1307
- operatorDid: userDid,
1308
- metadata: {
1309
- operator: await state.getUser(userDid).catch(() => null),
1310
- },
1311
- operatorIp: context?.ip || '',
1312
- operatorUa: context?.ua || '',
1313
- });
1314
- }
1315
-
1316
- logger.info('user passport enabled successfully', { teamDid, userDid, passportId });
1317
-
1318
- this.emit(TeamEvents.userUpdated, { teamDid, user: doc });
1319
- this.emit(TeamEvents.userPermissionUpdated, { teamDid, user: doc });
1320
-
1321
- return doc;
1322
- }
1323
-
1324
- async removeUserPassport({ passportId, userDid, teamDid }) {
1325
- if (!passportId) {
1326
- throw new Error('Revoked passport should not be empty');
1327
- }
1328
-
1329
- const state = await this.getUserState(teamDid);
1330
-
1331
- const doc = await state.removePassportById({ id: passportId });
1332
-
1333
- logger.info('user passport remove successfully', { teamDid, userDid, passportId });
1334
-
1335
- this.emit(TeamEvents.userUpdated, { teamDid, user: doc });
1336
- this.emit(TeamEvents.userPermissionUpdated, { teamDid, user: doc });
1337
-
1338
- return doc;
1339
- }
1340
-
1341
- async switchProfile({ teamDid, userDid, profile }) {
1342
- const state = await this.getUserState(teamDid);
1343
- // NOTICE: 这个 schema 没有对数据做任何 default 操作,所以没必要用 validate 之后的值
1344
- const { error } = profileSchema.validate({ ...profile, did: userDid });
1345
- if (error) {
1346
- throw new Error(error);
1347
- }
1348
- const oldUser = await state.getUser(userDid);
1349
- if (!oldUser) {
1350
- throw new Error('User is not exist', { userDid, teamDid });
1351
- }
1352
-
1353
- const mergeData = { ...profile };
1354
-
1355
- if (mergeData.avatar) {
1356
- if (mergeData.avatar.startsWith(USER_AVATAR_URL_PREFIX)) {
1357
- // pass
1358
- } else if (isUrl(mergeData.avatar)) {
1359
- try {
1360
- mergeData.avatar = await getAvatarByUrl(mergeData.avatar);
1361
- const blocklet = await getBlocklet({ did: teamDid, states: this.states, dataDirs: this.dataDirs });
1362
- mergeData.avatar = await extractUserAvatar(mergeData.avatar, { dataDir: blocklet.env.dataDir });
1363
- } catch {
1364
- logger.error('Failed to convert external avatar', { teamDid, userDid, avatar: mergeData.avatar });
1365
- throw new Error('Failed to convert external avatar');
1366
- }
1367
- } else {
1368
- // 仅当 avatar 是 base64 时,对 avatar 进行处理
1369
- const regex = /^data:image\/(\w+);base64,/;
1370
- const match = regex.exec(mergeData.avatar);
1371
- if (match) {
1372
- const blocklet = await getBlocklet({ did: teamDid, states: this.states, dataDirs: this.dataDirs });
1373
- mergeData.avatar = await extractUserAvatar(mergeData.avatar, { dataDir: blocklet.env.dataDir });
1374
- } else {
1375
- logger.error('Profile avatar is invalid', { teamDid, userDid, profile });
1376
- throw new Error('Profile avatar is invalid');
1377
- }
1378
- }
1379
- }
1380
- const doc = await state.updateUser(userDid, mergeData);
1381
- logger.info('User switch-profile successfully', { teamDid, userDid });
1382
- this.emit(TeamEvents.userUpdated, { teamDid, user: doc });
1383
- this.emit(TeamEvents.userProfileUpdated, { teamDid, user: doc });
1384
- return doc;
1385
- }
1386
-
1387
- // Invite member
1388
-
1389
- /**
1390
- * @type InvitationSession {
1391
- * type: 'invite';
1392
- * role: string;
1393
- * remark: string;
1394
- * expireDate: number;
1395
- * inviter: {
1396
- * did: string;
1397
- * email?: string;
1398
- * fullName?: string;
1399
- * role?: string;
1400
- * };
1401
- * teamDid: string;
1402
- * status: '' | 'success';
1403
- * receiver: {
1404
- * did: string;
1405
- * };
1406
- * sourceAppPid: string;
1407
- * display: {
1408
- * type: string;
1409
- * tag: string;
1410
- * };
1411
- * }
1412
- * @returns
1413
- */
1414
- async createMemberInvitation(
1415
- {
1416
- teamDid,
1417
- role: roleName,
1418
- expireTime,
1419
- remark,
1420
- sourceAppPid,
1421
- display = null,
1422
- passportExpireTime = null,
1423
- orgId = '', // 是否是邀请加入组织
1424
- inviteUserDids = [], // 邀请用户列表
1425
- },
1426
- context
1427
- ) {
1428
- await this.teamManager.checkEnablePassportIssuance(teamDid);
1429
-
1430
- if (expireTime && expireTime <= 0) {
1431
- throw new Error('Expire time must be greater than 0');
1432
- }
1433
- if (!roleName) {
1434
- throw new Error('Role cannot be empty');
1435
- }
1436
-
1437
- if (passportExpireTime && dayjs(passportExpireTime).isBefore(dayjs())) {
1438
- throw new Error('Passport expire time must be greater than current time');
1439
- }
1440
-
1441
- const roles = await this.getRoles({ teamDid, orgId });
1442
- const role = roles.find((r) => r.name === roleName);
1443
- if (!role) {
1444
- throw new Error(`Role does not exist: ${roleName}`);
1445
- }
1446
- await validatePassportDisplay(role, display);
1447
-
1448
- if (remark && remark.length > 50) {
1449
- throw new Error('Remark length should NOT be more than 50 characters');
1450
- }
1451
-
1452
- const { user } = context;
1453
- if (!user || !user.did) {
1454
- throw new Error('Inviter does not exist');
1455
- }
1456
-
1457
- // 如果邀请加入组织,则不验证 role 和 passport
1458
- if (!orgId && (user?.role || '').startsWith('blocklet-')) {
1459
- const userInfo = await this.getUser({ teamDid, user });
1460
- validateUserRolePassport({
1461
- role: (user?.role || '').replace('blocklet-', ''),
1462
- passports: userInfo?.passports || [],
1463
- });
1464
- }
1465
-
1466
- const expireDate = Date.now() + (expireTime || this.memberInviteExpireTime);
1467
- const state = await this.getSessionState(teamDid);
1468
- const { id: inviteId } = await state.start({
1469
- type: 'invite',
1470
- role: roleName,
1471
- remark,
1472
- expireDate,
1473
- inviter: user,
1474
- teamDid,
1475
- orgId,
1476
- inviteUserDids,
1477
- sourceAppPid,
1478
- display,
1479
- passportExpireTime,
1480
- });
1481
-
1482
- logger.info('Create invite member', { role: roleName, user, inviteId, display });
1483
-
1484
- return {
1485
- inviteId,
1486
- role: roleName,
1487
- remark,
1488
- expireDate: new Date(expireDate).toString(),
1489
- inviter: user,
1490
- teamDid,
1491
- sourceAppPid,
1492
- display,
1493
- };
1494
- }
1495
-
1496
- async getInvitation({ teamDid, inviteId }) {
1497
- if (!teamDid || !inviteId) {
1498
- return null;
1499
- }
1500
-
1501
- const state = await this.getSessionState(teamDid);
1502
- const invitation = await state.read(inviteId);
1503
- if (!invitation) {
1504
- return null;
1505
- }
1506
-
1507
- return {
1508
- // eslint-disable-next-line no-underscore-dangle
1509
- inviteId: invitation.id,
1510
- role: invitation.role,
1511
- remark: invitation.remark,
1512
- expireDate: new Date(invitation.expireDate).toString(),
1513
- inviter: invitation.inviter,
1514
- teamDid: invitation.teamDid,
1515
- status: invitation.status,
1516
- receiver: invitation.receiver,
1517
- sourceAppPid: invitation.sourceAppPid || null,
1518
- display: invitation.display || null,
1519
- orgId: invitation.orgId || null,
1520
- inviteUserDids: invitation.inviteUserDids || [],
1521
- passportExpireTime: invitation.passportExpireTime || null,
1522
- };
1523
- }
1524
-
1525
- async getInvitations({ teamDid, filter, orgId = '' }, context = {}) {
1526
- const state = await this.getSessionState(teamDid);
1527
- let invitations = [];
1528
- if (orgId) {
1529
- const org = await this.getOrg({ teamDid, id: orgId }, context);
1530
- const { user } = context;
1531
- if (!org) {
1532
- throw new CustomError(404, `Org does not exist: ${orgId}`);
1533
- }
1534
- if (org.ownerDid !== user?.did) {
1535
- throw new CustomError(403, `You are not the owner of the org: ${orgId}`);
1536
- }
1537
- }
1538
- const query = { type: 'invite' };
1539
- if (orgId) {
1540
- query['__data.orgId'] = orgId;
1541
- } else {
1542
- query.$or = [{ '__data.orgId': { $exists: false } }, { '__data.orgId': '' }];
1543
- }
1544
- invitations = await state.find(query);
1545
-
1546
- return invitations.filter(filter || ((x) => x.status !== 'success')).map((d) => ({
1547
- // eslint-disable-next-line no-underscore-dangle
1548
- inviteId: d.id,
1549
- role: d.role,
1550
- remark: d.remark,
1551
- expireDate: new Date(d.expireDate).toString(),
1552
- inviter: d.inviter,
1553
- teamDid: d.teamDid,
1554
- status: d.status,
1555
- receiver: d.receiver,
1556
- sourceAppPid: d.sourceAppPid || null,
1557
- display: d.display || null,
1558
- orgId: d.orgId || null,
1559
- inviteUserDids: d.inviteUserDids || [],
1560
- passportExpireTime: d.passportExpireTime || null,
1561
- }));
1562
- }
1563
-
1564
- async deleteInvitation({ teamDid, inviteId }) {
1565
- if (!inviteId) {
1566
- throw new Error('InviteId cannot be empty');
1567
- }
1568
-
1569
- const state = await this.getSessionState(teamDid);
1570
- await state.end(inviteId);
1571
-
1572
- logger.info('Delete invite session', { inviteId });
1573
-
1574
- return true;
1575
- }
1576
-
1577
- async checkInvitation({ teamDid, inviteId }) {
1578
- const state = await this.getSessionState(teamDid);
1579
-
1580
- const invitation = await state.read(inviteId);
1581
-
1582
- if (!invitation) {
1583
- throw new Error(`The invitation does not exist: ${inviteId}`);
1584
- }
1585
-
1586
- const { role, expireDate, remark } = invitation;
1587
-
1588
- if (Date.now() > expireDate) {
1589
- logger.error('Invite id has expired', { inviteId, expireAt: new Date(expireDate) });
1590
-
1591
- throw new Error(`The invitation has expired: ${inviteId}`);
1592
- }
1593
-
1594
- const roles = await this.getRoles({ teamDid });
1595
- const orgRoles = await this.getRoles({ teamDid, orgId: invitation.orgId });
1596
- const allRoles = [...roles, ...orgRoles];
1597
- if (!allRoles.some((r) => r.name === role)) {
1598
- throw new Error(`Role does not exist: ${role}`);
1599
- }
1600
-
1601
- return {
1602
- role,
1603
- remark,
1604
- };
1605
- }
1606
-
1607
- async closeInvitation({ teamDid, inviteId, status, receiver, isOrgInvite, timeout = 30 * 1000 }) {
1608
- const state = await this.getSessionState(teamDid);
1609
-
1610
- const invitation = await state.read(inviteId);
1611
-
1612
- if (!invitation) {
1613
- throw new Error(`The invitation does not exist: ${inviteId}`);
1614
- }
1615
- let shouldRemoveInviteSession = false;
1616
- if (isOrgInvite && invitation?.inviteUserDids?.length > 0) {
1617
- const unReceiverUserDids = invitation.inviteUserDids.filter((did) => did !== receiver.did);
1618
- shouldRemoveInviteSession = unReceiverUserDids.length === 0;
1619
- await state.update(inviteId, {
1620
- receiver,
1621
- inviteUserDids: unReceiverUserDids,
1622
- ...(shouldRemoveInviteSession ? { status } : {}),
1623
- });
1624
- } else {
1625
- await state.update(inviteId, { status, receiver });
1626
- shouldRemoveInviteSession = true;
1627
- }
1628
-
1629
- if (shouldRemoveInviteSession) {
1630
- setTimeout(async () => {
1631
- try {
1632
- logger.info('Invitation session closed', { inviteId });
1633
- await state.end(inviteId);
1634
- } catch (error) {
1635
- logger.error('close invitation failed', { error });
1636
- }
1637
- }, timeout);
1638
- }
1639
- }
1640
-
1641
- // transfer
1642
-
1643
- /**
1644
- * this function is only used to create server owner transfer invitation
1645
- */
1646
- createTransferInvitation({ teamDid, remark }, context = {}) {
1647
- return this.createMemberInvitation(
1648
- { teamDid, expireTime: this.transferOwnerExpireTime, remark, role: 'owner' },
1649
- context
1650
- );
1651
- }
1652
-
1653
- /**
1654
- * this function is only used to create application owner transfer
1655
- */
1656
- async createTransferAppOwnerSession({ appDid, remark = '' }) {
1657
- const expireTime = this.transferOwnerExpireTime;
1658
-
1659
- await this.teamManager.checkEnableTransferAppOwner(appDid);
1660
-
1661
- if (remark && remark.length > 50) {
1662
- throw new Error('Remark length should NOT be more than 50 characters');
1663
- }
1664
-
1665
- const expireDate = Date.now() + expireTime;
1666
- const state = await this.getSessionState(appDid);
1667
- const { id: transferId } = await state.start({
1668
- type: 'transfer-app-owner',
1669
- remark,
1670
- expireDate,
1671
- appDid,
1672
- });
1673
-
1674
- logger.info('createTransferAppOwnerSession', { appDid, transferId });
1675
-
1676
- return {
1677
- transferId,
1678
- remark,
1679
- expireDate: new Date(expireDate).toString(),
1680
- appDid,
1681
- status: 'created',
1682
- };
1683
- }
1684
-
1685
- async getTransferAppOwnerSession({ appDid, transferId }) {
1686
- if (!appDid || !transferId) {
1687
- return null;
1688
- }
1689
-
1690
- const state = await this.getSessionState(appDid);
1691
-
1692
- const transfer = await state.read(transferId);
1693
- if (!transfer) {
1694
- return null;
1695
- }
1696
-
1697
- return formatTransferData(transfer);
1698
- }
1699
-
1700
- async checkTransferAppOwnerSession({ appDid, transferId }) {
1701
- const state = await this.getSessionState(appDid);
1702
-
1703
- const transfer = await state.read(transferId);
1704
-
1705
- if (!transfer) {
1706
- throw new Error(`The transfer session does not exist: ${transferId}`);
1707
- }
1708
-
1709
- const { expireDate } = transfer;
1710
-
1711
- if (Date.now() > expireDate) {
1712
- logger.error('Transfer session has expired', { transferId, expireAt: new Date(expireDate) });
1713
-
1714
- throw new Error(`The transfer session has expired: ${transferId}`);
1715
- }
1716
-
1717
- return formatTransferData(transfer);
1718
- }
1719
-
1720
- async closeTransferAppOwnerSession({ appPid, transferId, status, timeout = 30 * 1000 }) {
1721
- const state = await this.getSessionState(appPid);
1722
-
1723
- const session = await state.read(transferId);
1724
-
1725
- if (!session) {
1726
- throw new Error(`The transfer app owner session does not exist: ${transferId}`);
1727
- }
1728
-
1729
- await state.update(transferId, { status });
1730
-
1731
- setTimeout(async () => {
1732
- try {
1733
- await state.end(transferId);
1734
- logger.info('Transfer session closed', { transferId });
1735
- } catch (error) {
1736
- logger.error('close transfer session failed', { transferId, error });
1737
- }
1738
- }, timeout);
1739
- }
1740
-
1741
- // Issue Passport
1742
- /**
1743
- * 为指定用户颁发 passport
1744
- * @param {string} param.teamDid - 应用的 did
1745
- * @param {string} param.ownerDid - 指定用户的 did
1746
- * @param {string} param.name - passport 的 role
1747
- * @returns {object} passport-info
1748
- */
1749
- async createPassportIssuance({ teamDid, ownerDid, name, display = null, passportExpireTime = null }, context = {}) {
1750
- if (!name) {
1751
- throw new Error('Passport cannot be empty');
1752
- }
1753
-
1754
- if (!isValidDid(ownerDid)) {
1755
- throw new Error(`ownerDid is invalid: ${ownerDid}`);
1756
- }
1757
-
1758
- await this.teamManager.checkEnablePassportIssuance(teamDid);
1759
-
1760
- // passport name is same as role
1761
- const roles = await this.getRoles({ teamDid });
1762
- const role = roles.find((r) => r.name === name);
1763
- if (!role) {
1764
- throw new Error(`Passport does not exist: ${name}`);
1765
- }
1766
- if (role.extra?.display === 'custom') {
1767
- if (!display) {
1768
- throw new Error(`display is required for custom passport: ${name}`);
1769
- }
1770
- const { error } = passportDisplaySchema.validate(display);
1771
- if (error) {
1772
- throw new Error(`display invalid for custom passport: ${formatError(error)}`);
1773
- }
1774
- }
1775
-
1776
- await validatePassportDisplay(role, display);
1777
-
1778
- const expireDate = Date.now() + this.memberInviteExpireTime;
1779
-
1780
- const state = await this.getSessionState(teamDid);
1781
-
1782
- // 为指定用户颁发通行证,拿到的 did 是永久 did,无需查询 connectedAccount
1783
- const currentUser = await this.getUser({ teamDid, user: { did: ownerDid } });
1784
- const walletDid = getWalletDid(currentUser);
1785
- const userDid = walletDid || ownerDid;
1786
- // 这里可能是为一个不存在的用户创建一个 passport,所以需要额外判断一下
1787
- if (currentUser) {
1788
- // auth0 账户不需要手动领取
1789
- if (walletDid !== ownerDid) {
1790
- const blocklet = await getBlocklet({ did: teamDid, states: this.states, dataDirs: this.dataDirs });
1791
- const nodeInfo = await this.node.read();
1792
- const blockletInfo = getBlockletInfo(blocklet, nodeInfo.sk);
1793
- const { wallet, passportColor, appUrl, name: issuerName } = blockletInfo;
1794
- const result = await createPassport({ role });
1795
- const vc = await createPassportVC({
1796
- issuerName,
1797
- issuerWallet: wallet,
1798
- issuerAvatarUrl: getAppAvatarUrl(appUrl),
1799
- ownerDid: userDid,
1800
- endpoint: getPassportStatusEndpoint({
1801
- baseUrl: joinURL(appUrl, WELLKNOWN_SERVICE_PATH_PREFIX),
1802
- userDid: ownerDid,
1803
- teamDid,
1804
- }),
1805
- ownerProfile: {
1806
- ...currentUser,
1807
- avatar: getUserAvatarUrl(appUrl, currentUser.avatar),
1808
- },
1809
- preferredColor: passportColor,
1810
- display,
1811
- passport: result.passport,
1812
- types: result.types,
1813
- purpose: teamDid === nodeInfo.did || isEmpty(result.types) ? 'login' : 'verification',
1814
- });
1815
-
1816
- if (walletDid) {
1817
- sendPassportVcNotification({ userDid, appWallet: wallet, locale: 'en', vc });
1818
- }
1819
-
1820
- // write passport to db
1821
- const passport = createUserPassport(vc, { role: role.name, display, source: PASSPORT_SOURCE.ISSUE });
1822
-
1823
- const passports = upsertToPassports(currentUser.passports || [], passport);
1824
- await this.updateUser({
1825
- teamDid,
1826
- user: {
1827
- did: currentUser.did,
1828
- pk: currentUser.pk,
1829
- passports,
1830
- },
1831
- });
1832
-
1833
- if (passport && this.passportAPI) {
1834
- await this.passportAPI.createPassportLog(teamDid, {
1835
- passportId: passport.id,
1836
- action: PASSPORT_LOG_ACTION.ISSUE,
1837
- operatorIp: context?.ip || '',
1838
- operatorUa: context?.ua || '',
1839
- operatorDid: ownerDid,
1840
- metadata: {
1841
- action: PASSPORT_ISSUE_ACTION.ISSUE_ON_AUTH0,
1842
- },
1843
- });
1844
- }
1845
-
1846
- return undefined;
1847
- }
1848
- }
1849
- const { id } = await state.start({
1850
- type: 'passport-issuance',
1851
- key: userDid,
1852
- expireDate, // session expireDate
1853
- name: role.name,
1854
- title: role.title,
1855
- ownerDid: userDid,
1856
- teamDid,
1857
- display,
1858
- inviter: context?.user,
1859
- passportExpireTime,
1860
- });
1861
-
1862
- logger.info('Create issuing passport session', { id, passport: name, ownerDid, teamDid });
1863
-
1864
- return {
1865
- id,
1866
- name: role.name,
1867
- title: role.title,
1868
- expireDate: new Date(expireDate).toString(),
1869
- ownerDid: userDid,
1870
- teamDid,
1871
- display,
1872
- };
1873
- }
1874
-
1875
- async getPassportIssuances({ teamDid, ownerDid }) {
1876
- const state = await this.getSessionState(teamDid);
1877
-
1878
- const query = { type: 'passport-issuance' };
1879
-
1880
- if (ownerDid) {
1881
- query.key = ownerDid;
1882
- }
1883
-
1884
- const list = await state.find(query);
1885
-
1886
- return list.map((d) => ({
1887
- // eslint-disable-next-line no-underscore-dangle
1888
- id: d.id,
1889
- name: d.name,
1890
- title: d.title,
1891
- expireDate: new Date(d.expireDate).toString(),
1892
- ownerDid: d.ownerDid,
1893
- teamDid: d.teamDid,
1894
- display: d.display,
1895
- }));
1896
- }
1897
-
1898
- async getPassportIssuance({ teamDid, sessionId }) {
1899
- const state = await this.getSessionState(teamDid);
1900
- const doc = await state.read(sessionId);
1901
- // FIXME: @zhanghan 指定被邀请者的邀请需要查询被邀请者的详细信息
1902
- return doc;
1903
- }
1904
-
1905
- async processPassportIssuance({ teamDid, sessionId }) {
1906
- const state = await this.getSessionState(teamDid);
1907
-
1908
- const session = await state.read(sessionId);
1909
-
1910
- if (!session) {
1911
- throw new Error(`The passport issuance session does not exist: ${sessionId}`);
1912
- }
1913
-
1914
- const { name, ownerDid, expireDate } = session;
1915
-
1916
- if (Date.now() > expireDate) {
1917
- logger.error('The passport issuance session has expired', { sessionId, expireAt: new Date(expireDate) });
1918
- throw new Error(`The passport issuance session has expired: ${sessionId}`);
1919
- }
1920
-
1921
- const roles = await this.getRoles({ teamDid });
1922
- if (!roles.find((x) => x.name === name)) {
1923
- throw new Error(`Passport does not exist: ${name}`);
1924
- }
1925
-
1926
- await state.end(sessionId);
1927
-
1928
- logger.info('The passport issuance session completed successfully', { sessionId, name, ownerDid });
1929
-
1930
- return {
1931
- name,
1932
- ownerDid,
1933
- };
1934
- }
1935
-
1936
- async deletePassportIssuance({ teamDid, sessionId }) {
1937
- if (!sessionId) {
1938
- throw new Error('SessionId cannot be empty');
1939
- }
1940
-
1941
- const state = await this.getSessionState(teamDid);
1942
- await state.end(sessionId);
1943
-
1944
- logger.info('Delete passport issuance session', { sessionId });
1945
-
1946
- return true;
1947
- }
1948
-
1949
- async configTrustedPassports({ teamDid, trustedPassports }) {
1950
- const value = await validateTrustedPassportIssuers(trustedPassports);
1951
- await this.teamManager.configTrustedPassports(teamDid, value);
1952
- if (!this.teamManager.isNodeTeam(teamDid)) {
1953
- this.emit(BlockletEvents.updated, { meta: { did: teamDid } });
1954
- }
1955
- }
1956
-
1957
- async configTrustedFactories({ teamDid, trustedFactories }) {
1958
- const value = await validateTrustedFactories(trustedFactories);
1959
-
1960
- const client = getChainClient(MAIN_CHAIN_ENDPOINT);
1961
- await Promise.all(
1962
- value.map(async (x) => {
1963
- const { state } = await client.getFactoryState({ address: x.factoryAddress });
1964
- if (!state) {
1965
- throw new Error(`NFT Collection ${x.factoryAddress} not found on ${MAIN_CHAIN_ENDPOINT}`);
1966
- }
1967
- })
1968
- );
1969
-
1970
- await this.teamManager.configTrustedFactories(teamDid, value);
1971
- if (!this.teamManager.isNodeTeam(teamDid)) {
1972
- this.emit(BlockletEvents.updated, { meta: { did: teamDid } });
1973
- }
1974
- }
1975
-
1976
- async configPassportIssuance({ teamDid, enable }) {
1977
- await this.teamManager.configPassportIssuance(teamDid, !!enable);
1978
-
1979
- if (!this.teamManager.isNodeTeam(teamDid)) {
1980
- this.emit(BlockletEvents.updated, { meta: { did: teamDid } });
1981
- }
1982
- }
1983
-
1984
- // Access Control
1985
-
1986
- getRoles({ teamDid, orgId }) {
1987
- return this.teamManager.getRoles(teamDid, orgId);
1988
- }
1989
-
1990
- async getRole({ teamDid, role: { name } = {} }) {
1991
- if (!name) {
1992
- throw new Error('role name is invalid');
1993
- }
1994
- const rbac = await this.getRBAC(teamDid);
1995
- const role = await rbac.getRole(name);
1996
- return role ? pick(role, ['name', 'grants', 'title', 'description', 'extra', 'orgId']) : null;
1997
- }
1998
-
1999
- async createRole({ teamDid, name, description, title, childName, permissions = [], extra: raw, orgId }) {
2000
- logger.info('create role', { teamDid, name, description, childName, permissions, raw });
2001
- const attrs = { name, title, description, childName, permissions, orgId };
2002
-
2003
- if (raw) {
2004
- try {
2005
- attrs.extra = JSON.parse(raw);
2006
- } catch (err) {
2007
- throw new Error('extra should be a valid json string');
2008
- }
2009
- }
2010
-
2011
- await validateCreateRole(pick(attrs, ['name', 'title', 'description', 'extra']));
2012
-
2013
- validateReservedRole(name);
2014
-
2015
- const rbac = await this.getRBAC(teamDid);
2016
-
2017
- let role;
2018
- try {
2019
- role = await rbac.createRole(attrs);
2020
- return pick(role, ['name', 'title', 'grants', 'description', 'extra', 'orgId']);
2021
- } catch (err) {
2022
- if (new RegExp(`Item ${name} already exists`).test(err.message)) {
2023
- throw new Error(`Id ${name} already exists`);
2024
- }
2025
- throw err;
2026
- }
2027
- }
2028
-
2029
- async updateRole({ teamDid, role: { name, title, description, extra: raw } = {}, orgId }) {
2030
- logger.info('update role', { teamDid, name, title, description, raw });
2031
-
2032
- const attrs = { name, title, description, orgId };
2033
-
2034
- if (raw) {
2035
- try {
2036
- attrs.extra = JSON.parse(raw);
2037
- } catch (err) {
2038
- throw new Error('extra should be a valid json string');
2039
- }
2040
- }
2041
-
2042
- await validateUpdateRole(pick(attrs, ['name', 'title', 'description', 'extra']));
2043
- const rbac = await this.getRBAC(teamDid);
2044
- const state = await rbac.updateRole(attrs);
2045
- return pick(state, ['name', 'title', 'grants', 'description', 'extra', 'orgId']);
2046
- }
2047
-
2048
- async getPermissions({ teamDid }) {
2049
- const rbac = await this.getRBAC(teamDid);
2050
- const permissions = await rbac.getPermissions();
2051
- return permissions.map((d) => {
2052
- d.isProtected = !!(d.extra && d.extra.isProtected);
2053
- return pick(d, ['name', 'description', 'isProtected']);
2054
- });
2055
- }
2056
-
2057
- async createPermission({ teamDid, name, description }) {
2058
- logger.info('create permissions', { teamDid, name });
2059
-
2060
- await validateCreatePermission({ name, description });
2061
-
2062
- const rbac = await this.getRBAC(teamDid);
2063
- const added = await rbac.createPermission({ name, description });
2064
-
2065
- return pick(added, ['name', 'description']);
2066
- }
2067
-
2068
- async updatePermission({ teamDid, permission: { name, description } = {} }) {
2069
- logger.info('update permission', { teamDid, name, description });
2070
-
2071
- await validateUpdatePermission({ name, description });
2072
-
2073
- const rbac = await this.getRBAC(teamDid);
2074
- const state = await rbac.updatePermission({ name, description });
2075
-
2076
- return pick(state, ['name', 'description']);
2077
- }
2078
-
2079
- async grant({ teamDid, roleName, grantName }) {
2080
- logger.info('grant', { teamDid, roleName, grantName });
2081
-
2082
- const rbac = await this.getRBAC(teamDid);
2083
- await rbac.grant(roleName, grantName);
2084
-
2085
- return true;
2086
- }
2087
-
2088
- async revoke({ teamDid, roleName, grantName }) {
2089
- logger.info('revoke', { teamDid, roleName, grantName });
2090
-
2091
- const rbac = await this.getRBAC(teamDid);
2092
- await rbac.revoke(roleName, grantName);
2093
-
2094
- return true;
2095
- }
2096
-
2097
- async updateGrants({ teamDid, roleName, grantNames }) {
2098
- logger.info('update grants', { teamDid, roleName, grantNames });
2099
-
2100
- const rbac = await this.getRBAC(teamDid);
2101
- const role = await rbac.updateGrants(roleName, grantNames);
2102
-
2103
- return pick(role, ['name', 'grants', 'title', 'description']);
2104
- }
2105
-
2106
- async deleteRole({ teamDid, name }) {
2107
- logger.info('delete role', { teamDid, name });
2108
-
2109
- validateReservedRole(name);
2110
-
2111
- const rbac = await this.getRBAC(teamDid);
2112
- await rbac.removeRole(name);
2113
-
2114
- return true;
2115
- }
2116
-
2117
- async deletePermission({ teamDid, name }) {
2118
- logger.info('delete permission', { teamDid, name });
2119
-
2120
- const rbac = await this.getRBAC(teamDid);
2121
-
2122
- const permission = await rbac.getPermission(name);
2123
- if (permission.extra && permission.extra.isProtected) {
2124
- throw new Error(`The permission ${name} is reserved`);
2125
- }
2126
-
2127
- await rbac.removePermission(name);
2128
-
2129
- return true;
2130
- }
2131
-
2132
- async getPermissionsByRole({ teamDid, role }) {
2133
- const rbac = await this.getRBAC(teamDid);
2134
-
2135
- const permissions = await rbac.getScope(role.name, true);
2136
-
2137
- return permissions.map((d) => pick(d, ['name', 'description']));
2138
- }
2139
-
2140
- async hasPermission({ teamDid, role, permission }) {
2141
- const rbac = await this.getRBAC(teamDid);
2142
-
2143
- const has = await rbac.can(role, ...permission.split('_'));
2144
-
2145
- return has;
2146
- }
2147
-
2148
- async refreshBlockletInterfacePermissions(blockletMeta) {
2149
- const { did, interfaces } = blockletMeta;
2150
-
2151
- const rbac = await this.getRBAC(did);
2152
-
2153
- const oldPermissions = await this.getPermissions({ teamDid: did });
2154
-
2155
- await Promise.all(
2156
- (interfaces || []).map(async ({ name, type }) => {
2157
- const permissionName = genPermissionName(name);
2158
- if (type === 'web') {
2159
- if (!oldPermissions.some((x) => x.name === permissionName)) {
2160
- await rbac.createPermission({
2161
- name: permissionName,
2162
- description: `Access resources under the ${name} interface`,
2163
- extra: {
2164
- isProtected: true,
2165
- },
2166
- });
2167
- }
2168
- }
2169
- })
2170
- );
2171
- }
2172
-
2173
- // eslint-disable-next-line no-unused-vars
2174
- async addStore({ teamDid, url, scope = '' }, context) {
2175
- logger.info('add store', { teamDid, url, scope });
2176
-
2177
- const sanitized = withoutTrailingSlash(await StoreUtil.getStoreUrl(url));
2178
- const storeList = await this.teamManager.getStoreList(teamDid);
2179
- const exists = storeList.some((x) => {
2180
- if (x.url === sanitized) {
2181
- if (!x.scope) {
2182
- return true;
2183
- }
2184
-
2185
- return x.scope === scope;
2186
- }
2187
-
2188
- return false;
2189
- });
2190
-
2191
- if (exists) {
2192
- throw new Error(`Blocklet Store already exist: ${sanitized}`);
2193
- }
2194
-
2195
- const store = await StoreUtil.getStoreMeta(sanitized);
2196
-
2197
- storeList.push({ ...store, scope, url: sanitized, protected: false });
2198
-
2199
- return this.teamManager.updateStoreList(teamDid, storeList);
2200
- }
2201
-
2202
- // 获取更多信息
2203
- async addEndpoint({ teamDid, url }, context) {
2204
- const userDid = context?.user?.did;
2205
- const { endpoint, appPid, appName, appDescription } = await ensureServerEndpoint(url);
2206
- const endpointList = await this.teamManager.getEndpointList(teamDid);
2207
-
2208
- if (!appPid) {
2209
- throw new Error('current url not blocklet url');
2210
- }
2211
-
2212
- if (!endpoint) {
2213
- throw new Error('current url not blocklet url');
2214
- }
2215
-
2216
- const index = endpointList.findIndex((x) => x.id === appPid);
2217
- if (index !== -1) {
2218
- endpointList[index].url = url;
2219
- } else {
2220
- endpointList.push({ id: appPid, appName, appDescription, scope: userDid, url, endpoint, protected: false });
2221
- }
2222
-
2223
- return this.teamManager.updateEndpointList(teamDid, endpointList);
2224
- }
2225
-
2226
- async deleteEndpoint({ teamDid, did, projectId }, context) {
2227
- const endpointList = await this.teamManager.getEndpointList(teamDid);
2228
-
2229
- const index = endpointList.findIndex((x) => x.id === did);
2230
- if (index === -1) {
2231
- throw new Error(`Blocklet Endpoint does not exist: ${did}`);
2232
- }
2233
- endpointList.splice(index, 1);
2234
-
2235
- if (projectId) {
2236
- const { projectState } = await this.teamManager.getProjectState(teamDid);
2237
- await projectState.deleteConnectedEndpoint({ projectId, endpointId: did, createdBy: context?.user?.did });
2238
- }
2239
-
2240
- return this.teamManager.updateEndpointList(teamDid, endpointList);
2241
- }
2242
-
2243
- async existsStore({ teamDid, url, scope }, context) {
2244
- const sanitized = withoutTrailingSlash(await StoreUtil.getStoreUrl(url));
2245
- const storeList = await this.teamManager.getStoreList(teamDid);
2246
- const did = context?.user?.did;
2247
- const exists = storeList.some((x) => {
2248
- if (x.url !== sanitized) {
2249
- return false;
2250
- }
2251
- if (x.scope && x.scope === did) {
2252
- return true;
2253
- }
2254
- if (!x.scope || x.scope === 'studio') {
2255
- return true;
2256
- }
2257
- return x.scope === scope;
2258
- });
2259
-
2260
- return exists;
2261
- }
2262
-
2263
- // eslint-disable-next-line no-unused-vars
2264
- async deleteStore({ teamDid, url, projectId, scope }, context) {
2265
- logger.info('delete registry', { url });
2266
- const sanitized = sanitizeUrl(url);
2267
-
2268
- const storeList = await this.teamManager.getStoreList(teamDid);
2269
- let storeIndex;
2270
- if (scope) {
2271
- storeIndex = storeList.findIndex((x) => {
2272
- if (x.protected) {
2273
- return false;
2274
- }
2275
- return x.url === sanitized && x.scope === scope;
2276
- });
2277
- if (storeIndex === -1) {
2278
- throw new Error(`No permission to delete the Store registry: ${sanitized}`);
2279
- }
2280
- } else {
2281
- storeIndex = storeList.findIndex((x) => x.url === sanitized && !x.scope);
2282
- if (storeIndex === -1) {
2283
- throw new Error(`Store registry does not exist: ${sanitized}`);
2284
- }
2285
- }
2286
-
2287
- if (projectId && scope) {
2288
- const { projectState } = await this.teamManager.getProjectState(teamDid);
2289
- const store = await StoreUtil.getStoreMeta(sanitized);
2290
- await projectState.deleteConnectedStore({
2291
- projectId,
2292
- storeId: store.id,
2293
- createdBy: scope === 'studio' ? null : context?.user?.did,
2294
- });
2295
- }
2296
- storeList.splice(storeIndex, 1);
2297
- return this.teamManager.updateStoreList(teamDid, storeList);
2298
- }
2299
-
2300
- createNotification = (payload) => {
2301
- return this.teamManager.createNotification(payload);
2302
- };
2303
-
2304
- async getNotification({ teamDid, ...rest }, context) {
2305
- const notificationState = await this.getNotificationState(teamDid);
2306
- return notificationState.findPaginated({ teamDid, ...rest }, context);
2307
- }
2308
-
2309
- async getNotificationById({ teamDid, id }) {
2310
- const notificationState = await this.getNotificationState(teamDid);
2311
- const notification = await notificationState.findNotification({ id });
2312
- return notification;
2313
- }
2314
-
2315
- async getNotificationsUnreadCount({ teamDid, receiver }, context) {
2316
- const notificationState = await this.getNotificationState(teamDid);
2317
- return notificationState.getUnreadCount({ receiver }, context);
2318
- }
2319
-
2320
- async createNotificationReceiver({ teamDid, receiverInstance, ...rest }, context) {
2321
- const notificationState = await this.getNotificationState(teamDid);
2322
- return notificationState.createNotificationReceiver({ receiverInstance, ...rest }, context);
2323
- }
2324
-
2325
- async markAllNotificationsAsRead({ teamDid, ...rest }, context) {
2326
- const notificationState = await this.getNotificationState(teamDid);
2327
- const result = await notificationState.makeAllAsRead(rest, context);
2328
- const { numAffected, notificationIds, effectRows } = result;
2329
- this.teamManager.emitReadNotification(teamDid, {
2330
- readCount: numAffected,
2331
- notificationIds,
2332
- receiver: rest.receiver,
2333
- effectRows,
2334
- });
2335
- return result;
2336
- }
2337
-
2338
- async updateNotificationStatus({ teamDid, receivers, notificationId, updates }) {
2339
- const notificationState = await this.getNotificationState(teamDid);
2340
- const result = await notificationState.updateStatus({ receivers, notificationId, updates, teamDid });
2341
- // 发送状态更新的通知给前端,使用通用节流函数
2342
- if (result > 0 && teamDid && !this.teamManager.isNodeTeam(teamDid)) {
2343
- const throttledEmit = this.getThrottledEmit(teamDid);
2344
- if (throttledEmit) {
2345
- throttledEmit(EVENTS.NOTIFICATION_BLOCKLET_UPDATE, {
2346
- teamDid,
2347
- notificationId,
2348
- });
2349
- }
2350
- }
2351
- return result;
635
+ getTransferAppOwnerSession({ appDid, transferId }) {
636
+ return invitationManager.getTransferAppOwnerSession(this, { appDid, transferId });
2352
637
  }
2353
638
 
2354
- async readNotifications({ teamDid, ...rest }, context) {
2355
- const notificationState = await this.getNotificationState(teamDid);
2356
- const { numAffected, effectRows } = await notificationState.read({ teamDid, ...rest }, context);
2357
- this.teamManager.emitReadNotification(teamDid, {
2358
- readCount: numAffected,
2359
- receiver: rest.receiver,
2360
- notificationIds: rest.notificationIds ?? [],
2361
- effectRows,
2362
- });
2363
- return numAffected;
2364
- }
2365
-
2366
- async unreadNotifications({ teamDid, ...rest }, context) {
2367
- const notificationState = await this.getNotificationState(teamDid);
2368
- return notificationState.unread({ teamDid, ...rest }, context);
2369
- }
2370
-
2371
- async getNotificationSendLog({ teamDid, ...rest }, context) {
2372
- const notificationState = await this.getNotificationState(teamDid);
2373
- return notificationState.getNotificationSendLog({ ...rest }, context);
2374
- }
2375
-
2376
- async getNotificationComponents({ teamDid, ...rest }, context) {
2377
- const notificationState = await this.getNotificationState(teamDid);
2378
- return notificationState.getNotificationComponents({ ...rest }, context);
2379
- }
2380
-
2381
- async getReceivers({ teamDid, ...rest }, context) {
2382
- const notificationState = await this.getNotificationState(teamDid);
2383
- return notificationState.getNotificationReceivers({ ...rest }, context);
2384
- }
2385
-
2386
- // =======
2387
- // Private
2388
- // =======
2389
-
2390
- getRBAC(did) {
2391
- return this.teamManager.getRBAC(did);
2392
- }
2393
-
2394
- getUserState(did) {
2395
- return this.teamManager.getUserState(did);
2396
- }
2397
-
2398
- getVerifyCodeState(did) {
2399
- return this.teamManager.getVerifyCodeState(did);
2400
- }
2401
-
2402
- getUserSessionState(did) {
2403
- return this.teamManager.getUserSessionState(did);
2404
- }
2405
-
2406
- getAccessKeyState(did) {
2407
- return this.teamManager.getAccessKeyState(did);
2408
- }
2409
-
2410
- getTagState(did) {
2411
- return this.teamManager.getTagState(did);
2412
- }
2413
-
2414
- getTaggingState(did) {
2415
- return this.teamManager.getTaggingState(did);
2416
- }
2417
-
2418
- getSessionState(did) {
2419
- return this.teamManager.getSessionState(did);
2420
- }
2421
-
2422
- getNotificationState(did) {
2423
- return this.teamManager.getNotificationState(did);
2424
- }
2425
-
2426
- getOrgState(did) {
2427
- return this.teamManager.getOrgState(did);
2428
- }
2429
-
2430
- // =============
2431
- // Just for test
2432
- // =============
2433
- setInviteExpireTime(ms) {
2434
- this.memberInviteExpireTime = ms;
2435
- }
2436
-
2437
- // user-session management
2438
- async getUserSession({ teamDid, id, includeUser = false }) {
2439
- const state = await this.getUserSessionState(teamDid);
2440
- const userState = await this.getUserState(teamDid);
2441
-
2442
- const where = {
2443
- status: USER_SESSION_STATUS.ONLINE,
2444
- id,
2445
- };
2446
- const include = [];
2447
- if (includeUser) {
2448
- include.push({
2449
- model: userState.model,
2450
- as: 'user',
2451
- attributes: [
2452
- 'did',
2453
- 'pk',
2454
- 'sourceProvider',
2455
- 'fullName',
2456
- 'email',
2457
- 'avatar',
2458
- 'remark',
2459
- 'sourceAppPid',
2460
- 'approved',
2461
- ],
2462
- });
2463
- }
2464
-
2465
- const userSession = await state.model.findOne({
2466
- where,
2467
- attributes: [
2468
- 'id',
2469
- 'appPid',
2470
- 'userDid',
2471
- 'visitorId',
2472
- 'passportId',
2473
- 'createdAt',
2474
- 'updatedAt',
2475
- 'extra',
2476
- 'ua',
2477
- 'lastLoginIp',
2478
- 'status',
2479
- ],
2480
- include,
2481
- });
2482
- return userSession?.toJSON?.();
2483
- }
2484
-
2485
- // query, paging: inputPaging, sort
2486
- async getUserSessions({ teamDid, query = {}, paging, sort }) {
2487
- const nodeInfo = await this.node.read({ useCache: true });
2488
- if (teamDid === nodeInfo.did) {
2489
- return {
2490
- list: [],
2491
- paging: {
2492
- page: 1,
2493
- pageCount: 0,
2494
- pageSize: 10,
2495
- total: 0,
2496
- },
2497
- };
2498
- }
2499
-
2500
- const { userDid, visitorId, appPid, includeUser = false, status = USER_SESSION_STATUS.ONLINE } = query;
2501
- const state = await this.getUserSessionState(teamDid);
2502
- const userState = await this.getUserState(teamDid);
2503
- const where = {};
2504
- if (userDid) where.userDid = userDid;
2505
- if (visitorId) where.visitorId = visitorId;
2506
- if (appPid) where.appPid = appPid;
2507
-
2508
- const blocklet = await getBlocklet({ did: teamDid, states: this.states, dataDirs: this.dataDirs, useCache: true });
2509
- const whereStatus = getUserSessionWhere({ status, blocklet });
2510
- Object.assign(where, whereStatus);
2511
- const include = [];
2512
- if (includeUser) {
2513
- include.push({
2514
- model: userState.model,
2515
- as: 'user',
2516
- attributes: [
2517
- 'did',
2518
- 'pk',
2519
- 'sourceProvider',
2520
- 'fullName',
2521
- 'email',
2522
- 'avatar',
2523
- 'remark',
2524
- 'sourceAppPid',
2525
- 'approved',
2526
- ],
2527
- });
2528
- }
2529
-
2530
- const result = await state.paginate(
2531
- {
2532
- where,
2533
- attributes: [
2534
- 'id',
2535
- 'appPid',
2536
- 'userDid',
2537
- 'visitorId',
2538
- 'passportId',
2539
- 'createdAt',
2540
- 'updatedAt',
2541
- 'extra',
2542
- 'ua',
2543
- 'lastLoginIp',
2544
- 'status',
2545
- ],
2546
- include,
2547
- },
2548
- { updatedAt: -1, ...sort },
2549
- { pageSize: 10, ...paging }
2550
- );
2551
-
2552
- // NOTICE: 保留结构,方便理解
2553
- return {
2554
- list: result.list,
2555
- paging: result.paging,
2556
- };
639
+ checkTransferAppOwnerSession({ appDid, transferId }) {
640
+ return invitationManager.checkTransferAppOwnerSession(this, { appDid, transferId });
2557
641
  }
2558
642
 
2559
- async getUserSessionsCount({ teamDid, query = {} }) {
2560
- const nodeInfo = await this.node.read();
2561
- if (teamDid === nodeInfo.did) {
2562
- return {
2563
- count: 0,
2564
- };
2565
- }
2566
- const { userDid, visitorId, appPid, status = USER_SESSION_STATUS.ONLINE } = query;
2567
- const state = await this.getUserSessionState(teamDid);
2568
- const where = {};
2569
- if (userDid) where.userDid = userDid;
2570
- if (visitorId) where.visitorId = visitorId;
2571
- if (appPid) where.appPid = appPid;
2572
-
2573
- const blocklet = await getBlocklet({ did: teamDid, states: this.states, dataDirs: this.dataDirs, useCache: true });
2574
- const whereStatus = getUserSessionWhere({ status, blocklet });
2575
- Object.assign(where, whereStatus);
2576
-
2577
- // HACK: 使用 state.count 并不能按预期工作,先改为使用 state.model.count 来实现
2578
- const result = await state.model.count({ where });
2579
- return {
2580
- count: result,
2581
- };
643
+ closeTransferAppOwnerSession(params) {
644
+ return invitationManager.closeTransferAppOwnerSession(this, params);
2582
645
  }
2583
646
 
2584
- async getPassportById({ teamDid, passportId }) {
2585
- const userState = await this.getUserState(teamDid);
2586
- const passport = await userState.getPassportById(passportId);
2587
- return passport;
2588
- }
2589
-
2590
- async getPassportFromFederated({ site, passportId, teamDid }) {
2591
- try {
2592
- const blocklet = await getBlocklet({
2593
- did: teamDid,
2594
- states: this.states,
2595
- dataDirs: this.dataDirs,
2596
- useCache: true,
2597
- });
2598
- const nodeInfo = await this.node.read();
2599
- const { permanentWallet } = getBlockletInfo(blocklet, nodeInfo.sk);
2600
-
2601
- const result = await callFederated({
2602
- action: 'getPassport',
2603
- data: {
2604
- passportId,
2605
- },
2606
- permanentWallet,
2607
- site,
2608
- requestOptions: {
2609
- // 缩短查询通行证的请求时间,这个请求不会很复杂
2610
- timeout: 3 * 1000,
2611
- },
2612
- });
2613
-
2614
- return result;
2615
- } catch (error) {
2616
- // 吞没错误,查询失败也不会影响整个快捷登录流程
2617
- logger.error('Failed to getPassportFromFederated', { site, passportId, teamDid, error });
2618
- return null;
2619
- }
647
+ // ============ Passport Manager ============
648
+ issuePassportToUser(params, context) {
649
+ return passportManager.issuePassportToUser(this, params, context);
2620
650
  }
2621
651
 
2622
- /**
2623
- * 新增|更新一个 userSession
2624
- * @param {string} params.teamDid - 当前更新的这个应用的 teamDid
2625
- * @param {string} params.appPid - 记录的 userSession 中应用的 appPid
2626
- * @param {string} params.userDid - 记录的 userSession 中当前用户的 did
2627
- * @param {string} params.visitorId - 记录的 userSession 中当前用户所使用设备的 visitorId
2628
- * @param {string} params.ua - 记录的 userSession 中当前用户所使用设备的 ua
2629
- * @param {string} params.lastLoginIp - 记录的 userSession 中当前用户的最后登录 ip
2630
- * @param {string} params.passportId - 记录的 userSession 中当前用户登录所使用的 passportId
2631
- * @param {string} params.status - 记录的 userSession 的状态
2632
- * @param {Object} params.extra - 记录的 userSession 额外的数据
2633
- * @returns
2634
- */
2635
-
2636
- async upsertUserSession(
2637
- { teamDid, appPid, userDid, visitorId, ua, lastLoginIp, passportId, status, extra, locale, origin },
2638
- { skipNotification = false } = {}
2639
- ) {
2640
- if (!userDid) {
2641
- throw new Error('userDid is required');
2642
- }
2643
- if (visitorId) {
2644
- if (visitorId.length > 80) {
2645
- throw new Error('visitorId should be less than 80 characters');
2646
- }
2647
- }
2648
-
2649
- const state = await this.getUserSessionState(teamDid);
2650
- let data;
2651
- if (!visitorId) {
2652
- data = await state.insert({
2653
- userDid,
2654
- ua,
2655
- lastLoginIp,
2656
- appPid,
2657
- passportId,
2658
- extra,
2659
- });
2660
- logger.info('insert userSession successfully', { userDid, ua, lastLoginIp, appPid, passportId, extra });
2661
- this.emit(BlockletEvents.addUserSession, {
2662
- userSession: data,
2663
- teamDid,
2664
- userDid,
2665
- locale,
2666
- skipNotification,
2667
- origin,
2668
- });
2669
- } else {
2670
- const exist = await state.findOne({ userDid, visitorId, appPid });
2671
- if (exist) {
2672
- const mergeExtra = defaults({}, extra || {}, exist.extra || {});
2673
- [, [data]] = await state.update(exist.id, {
2674
- passportId,
2675
- lastLoginIp,
2676
- ua,
2677
- status,
2678
- extra: mergeExtra,
2679
- });
2680
- logger.info('update userSession successfully', {
2681
- id: exist.id,
2682
- ua,
2683
- lastLoginIp,
2684
- passportId,
2685
- status,
2686
- extra: mergeExtra,
2687
- });
2688
- if (Date.now() - new Date(exist.createdAt).getTime() < 1000 * 10) {
2689
- // HACK: 此处是为了矫正首次创建 userSession 没有 ua 的情况,会通过 /api/did/session 接口来更新 ua 信息,这个时候才能当成新增 userSession 来发送通知
2690
- this.emit(BlockletEvents.addUserSession, {
2691
- userSession: data,
2692
- teamDid,
2693
- userDid,
2694
- locale,
2695
- skipNotification,
2696
- origin,
2697
- });
2698
- } else {
2699
- this.emit(BlockletEvents.updateUserSession, {
2700
- userSession: data,
2701
- teamDid,
2702
- userDid,
2703
- skipNotification,
2704
- origin,
2705
- });
2706
- }
2707
- } else {
2708
- data = await state.insert({
2709
- visitorId,
2710
- userDid,
2711
- ua,
2712
- lastLoginIp,
2713
- appPid,
2714
- passportId,
2715
- extra,
2716
- });
2717
- logger.info('insert userSession successfully', {
2718
- visitorId,
2719
- userDid,
2720
- ua,
2721
- lastLoginIp,
2722
- appPid,
2723
- passportId,
2724
- extra,
2725
- });
2726
- this.emit(BlockletEvents.addUserSession, {
2727
- userSession: data,
2728
- teamDid,
2729
- userDid,
2730
- locale,
2731
- skipNotification,
2732
- origin,
2733
- });
2734
- }
2735
- }
2736
-
2737
- return data;
652
+ revokeUserPassport(params, context) {
653
+ return passportManager.revokeUserPassport(this, params, context);
2738
654
  }
2739
655
 
2740
- /**
2741
- * 同步一个 userSession(userSession 只能向 master 同步,或 master 向 member 同步,不能由 member 向 member 同步)
2742
- * @param {string} params.teamDid - 当前更新的这个应用的 teamDid
2743
- * @param {string} params.targetAppPid - 同步的 userSession 的目标站点 appPid
2744
- * @param {string} params.userDid - 同步的 userSession 中当前用户的 did
2745
- * @param {string} params.visitorId - 同步的 userSession 中当前用户所使用设备的 visitorId
2746
- * @param {string} params.ua - 同步的 userSession 中当前用户所使用设备的 ua
2747
- * @param {string} params.lastLoginIp - 同步的 userSession 中当前用户的最后登录 ip
2748
- * @param {string} params.passportId - 同步的 userSession 中当前用户登录所使用的 passportId
2749
- * @param {object} params.extra - 同步的 userSession 中当前用户登录额外的数据
2750
- * @returns
2751
- */
2752
- async syncUserSession({ teamDid, targetAppPid, userDid, visitorId, ua, lastLoginIp, passportId, extra }) {
2753
- if (!targetAppPid) return;
2754
-
2755
- const blocklet = await getBlocklet({ did: teamDid, states: this.states, dataDirs: this.dataDirs, useCache: true });
2756
- const nodeInfo = await this.node.read();
2757
- const { permanentWallet } = getBlockletInfo(blocklet, nodeInfo.sk);
2758
-
2759
- const targetSite = findFederatedSite(blocklet, targetAppPid);
2760
- const masterSite = getFederatedMaster(blocklet);
2761
-
2762
- let syncSite = null;
2763
- let appPid;
2764
- if (masterSite && targetSite) {
2765
- if (targetSite.appPid === masterSite.appPid) {
2766
- if (teamDid !== masterSite.appPid) {
2767
- syncSite = masterSite;
2768
- appPid = teamDid;
2769
- }
2770
- } else if (teamDid === masterSite.appPid) {
2771
- syncSite = targetSite;
2772
- appPid = targetAppPid;
2773
- }
2774
-
2775
- if (syncSite) {
2776
- try {
2777
- const userSession = {
2778
- action: 'login',
2779
- userDid,
2780
- visitorId,
2781
- ua,
2782
- lastLoginIp,
2783
- appPid,
2784
- passportId,
2785
- extra,
2786
- };
2787
- await callFederated({
2788
- action: 'sync',
2789
- permanentWallet,
2790
- site: targetSite,
2791
- data: {
2792
- userSessions: [userSession],
2793
- },
2794
- });
2795
- logger.debug('Sync userSession to federated site successfully', {
2796
- userDid,
2797
- visitorId,
2798
- ua,
2799
- lastLoginIp,
2800
- appPid,
2801
- passportId,
2802
- targetSite,
2803
- extra,
2804
- });
2805
- } catch (err) {
2806
- logger.error(
2807
- 'Sync userSession to federated site failed',
2808
- {
2809
- userDid,
2810
- visitorId,
2811
- ua,
2812
- lastLoginIp,
2813
- appPid,
2814
- passportId,
2815
- targetSite,
2816
- extra,
2817
- },
2818
- err
2819
- );
2820
- }
2821
- }
2822
- }
656
+ enableUserPassport(params, context) {
657
+ return passportManager.enableUserPassport(this, params, context);
2823
658
  }
2824
659
 
2825
- async logoutUser({ teamDid, userDid, visitorId = '', appPid = '', status = '', remove = false }) {
2826
- if (!userDid) {
2827
- throw new Error('userDid is required');
2828
- }
2829
- if (!teamDid) {
2830
- throw new Error('teamDid is required');
2831
- }
2832
-
2833
- const nodeInfo = await this.node.read();
2834
- if (nodeInfo.did === teamDid) {
2835
- return 0;
2836
- }
2837
- const userSessionState = await this.getUserSessionState(teamDid);
2838
- const where = { userDid };
2839
- if (visitorId) where.visitorId = visitorId;
2840
- if (appPid) where.appPid = appPid;
2841
-
2842
- const blocklet = await getBlocklet({ did: teamDid, states: this.states, dataDirs: this.dataDirs, useCache: true });
2843
- const whereStatus = getUserSessionWhere({ status, blocklet });
2844
- Object.assign(where, whereStatus);
2845
- let result;
2846
- if (remove) {
2847
- result = await userSessionState.remove(where);
2848
- } else {
2849
- result = await userSessionState.update(where, { $set: { status: USER_SESSION_STATUS.OFFLINE } });
2850
- }
2851
- const { permanentWallet } = getBlockletInfo(blocklet, nodeInfo.sk);
2852
- const masterSite = getFederatedMaster(blocklet);
2853
- if (masterSite && masterSite.isMaster !== false && masterSite.appPid !== teamDid) {
2854
- callFederated({
2855
- action: 'sync',
2856
- permanentWallet,
2857
- // FIXME: @zhanghan 是否还需要通知其他 Member 站点,执行退出登录的操作
2858
- site: masterSite,
2859
- data: {
2860
- userSessions: [
2861
- {
2862
- action: 'logout',
2863
- userDid,
2864
- visitorId,
2865
- // HACK: 如果未传入 appPid,代表要注销当前用户在 master 中所有登录状态,所以传入 undefined;否则就只注销 master 中记录的当前 member 的登录状态
2866
- appPid: appPid ? teamDid : undefined,
2867
- remove,
2868
- },
2869
- ],
2870
- },
2871
- });
2872
- }
2873
- return result;
660
+ removeUserPassport(params) {
661
+ return passportManager.removeUserPassport(this, params);
2874
662
  }
2875
663
 
2876
- // KYC related
2877
- async createVerifyCode({ teamDid, subject, scope = 'email', purpose = 'kyc' }) {
2878
- const state = await this.getVerifyCodeState(teamDid);
2879
- return state.create(subject, scope, purpose);
664
+ createPassportIssuance(params, context) {
665
+ return passportManager.createPassportIssuance(this, params, context);
2880
666
  }
2881
667
 
2882
- async consumeVerifyCode({ teamDid, code }) {
2883
- const state = await this.getVerifyCodeState(teamDid);
2884
- return state.verify(code);
668
+ getPassportIssuances({ teamDid, ownerDid }) {
669
+ return passportManager.getPassportIssuances(this, { teamDid, ownerDid });
2885
670
  }
2886
671
 
2887
- async issueVerifyCode({ teamDid, code }) {
2888
- const state = await this.getVerifyCodeState(teamDid);
2889
- return state.issue(code);
672
+ getPassportIssuance({ teamDid, sessionId }) {
673
+ return passportManager.getPassportIssuance(this, { teamDid, sessionId });
2890
674
  }
2891
675
 
2892
- async sendVerifyCode({ teamDid, code }) {
2893
- const state = await this.getVerifyCodeState(teamDid);
2894
- return state.send(code);
676
+ processPassportIssuance({ teamDid, sessionId }) {
677
+ return passportManager.processPassportIssuance(this, { teamDid, sessionId });
2895
678
  }
2896
679
 
2897
- async isSubjectVerified({ teamDid, subject }) {
2898
- const state = await this.getVerifyCodeState(teamDid);
2899
- return state.isVerified(subject);
680
+ deletePassportIssuance({ teamDid, sessionId }) {
681
+ return passportManager.deletePassportIssuance(this, { teamDid, sessionId });
2900
682
  }
2901
683
 
2902
- async isSubjectIssued({ teamDid, userDid, subject }) {
2903
- const state = await this.getUserState(teamDid);
2904
- return state.isSubjectIssued(userDid, subject);
684
+ configTrustedPassports({ teamDid, trustedPassports }) {
685
+ return passportManager.configTrustedPassports(this, { teamDid, trustedPassports });
2905
686
  }
2906
687
 
2907
- async isSubjectSent({ teamDid, subject }) {
2908
- const state = await this.getVerifyCodeState(teamDid);
2909
- return state.isSent(subject);
688
+ configTrustedFactories({ teamDid, trustedFactories }) {
689
+ return passportManager.configTrustedFactories(this, { teamDid, trustedFactories });
2910
690
  }
2911
691
 
2912
- async getVerifyCode({ teamDid, code, id }) {
2913
- const state = await this.getVerifyCodeState(teamDid);
2914
- const where = {};
2915
- if (code) where.code = code;
2916
- if (id) where.id = id;
2917
- return state.findOne(where);
692
+ configPassportIssuance({ teamDid, enable }) {
693
+ return passportManager.configPassportIssuance(this, { teamDid, enable });
2918
694
  }
2919
695
 
2920
- async rotateSessionKey({ teamDid }) {
2921
- if (!teamDid) {
2922
- throw new Error('teamDid is required');
2923
- }
2924
-
2925
- const info = await this.node.read();
2926
- if (info.did === teamDid) {
2927
- await this.node.updateNodeInfo({ sessionSalt: generateRandomString(16) });
2928
- return 0;
2929
- }
2930
-
2931
- const blocklet = await getBlocklet({ did: teamDid, states: this.states, dataDirs: this.dataDirs, useCache: true });
2932
- const sessionConfig = cloneDeep(blocklet.settings.session || {});
2933
- sessionConfig.salt = generateRandomString(16);
2934
- await this.states.blockletExtras.setSettings(blocklet.meta.did, { session: sessionConfig });
2935
- this.emit(BlockletEvents.updated, { meta: { did: teamDid } });
2936
-
2937
- // NOTE: we need emit appConfigChanged event to update sessionSalt in blocklet-sdk
2938
- this.emit(BlockletInternalEvents.appConfigChanged, {
2939
- appDid: teamDid,
2940
- configs: [{ key: 'BLOCKLET_APP_SALT', value: sessionConfig.salt }],
2941
- });
2942
-
2943
- return 1;
696
+ // ============ Org Manager ============
697
+ getOrgs(params, context) {
698
+ return orgManager.getOrgs(this, params, context);
2944
699
  }
2945
700
 
2946
- // AccessKey related
2947
- async getAccessKeys({ teamDid, ...data }, context) {
2948
- const state = await this.getAccessKeyState(teamDid);
2949
- return state.findPaginated(data, context);
701
+ getOrg({ teamDid, id }, context) {
702
+ return orgManager.getOrg(this, { teamDid, id }, context);
2950
703
  }
2951
704
 
2952
- async getAccessKey({ teamDid, accessKeyId }, context) {
2953
- const state = await this.getAccessKeyState(teamDid);
2954
- return state.detail({ accessKeyId }, context);
705
+ createDefaultOrgForUser({ teamDid, user }) {
706
+ return orgManager.createDefaultOrgForUser(this, { teamDid, user });
2955
707
  }
2956
708
 
2957
- async createAccessKey({ teamDid, ...data }, context) {
2958
- const state = await this.getAccessKeyState(teamDid);
2959
- return state.create(Object.assign({ componentDid: teamDid }, data), context);
709
+ issueOrgOwnerPassport({ teamDid, org }) {
710
+ return orgManager.issueOrgOwnerPassport(this, { teamDid, org });
2960
711
  }
2961
712
 
2962
- async updateAccessKey({ teamDid, ...data }, context) {
2963
- const state = await this.getAccessKeyState(teamDid);
2964
- return state.update(data, context);
713
+ createOrg(params, context) {
714
+ return orgManager.createOrg(this, params, context);
2965
715
  }
2966
716
 
2967
- async deleteAccessKey({ teamDid, accessKeyId }, context) {
2968
- const state = await this.getAccessKeyState(teamDid);
2969
- return state.remove({ accessKeyId }, context);
717
+ updateOrg({ teamDid, org }, context) {
718
+ return orgManager.updateOrg(this, { teamDid, org }, context);
2970
719
  }
2971
720
 
2972
- async refreshLastUsed({ teamDid, accessKeyId }, context) {
2973
- const state = await this.getAccessKeyState(teamDid);
2974
- return state.refreshLastUsed(accessKeyId, context);
721
+ deleteOrg({ teamDid, id }, context) {
722
+ return orgManager.deleteOrg(this, { teamDid, id }, context);
2975
723
  }
2976
724
 
2977
- async verifyAccessKey({ teamDid, ...data }, context) {
2978
- const state = await this.getAccessKeyState(teamDid);
2979
- const accessKey = await state.verify(data, context);
2980
- return accessKey;
725
+ addOrgMember({ teamDid, orgId, userDid }, context) {
726
+ return orgManager.addOrgMember(this, { teamDid, orgId, userDid }, context);
2981
727
  }
2982
728
 
2983
- async getOAuthClients({ teamDid, paging }) {
2984
- const { oauthClientState } = await this.teamManager.getOAuthState(teamDid);
2985
- return oauthClientState.list(paging);
729
+ updateOrgMember({ teamDid, orgId, userDid, status }, context) {
730
+ return orgManager.updateOrgMember(this, { teamDid, orgId, userDid, status }, context);
2986
731
  }
2987
732
 
2988
- async deleteOAuthClient({ teamDid, clientId }) {
2989
- const { oauthClientState } = await this.teamManager.getOAuthState(teamDid);
2990
- return oauthClientState.remove({ clientId });
733
+ sendInvitationNotification(params) {
734
+ return orgManager.sendInvitationNotification(this, params);
2991
735
  }
2992
736
 
2993
- async createOAuthClient({ teamDid, input }, context) {
2994
- const { oauthClientState } = await this.teamManager.getOAuthState(teamDid);
2995
- return oauthClientState.create(input, context);
737
+ getFederatedMasterBlockletInfo({ blocklet }) {
738
+ return orgManager.getFederatedMasterBlockletInfo({ blocklet });
2996
739
  }
2997
740
 
2998
- async updateOAuthClient({ teamDid, input }, context) {
2999
- const { oauthClientState } = await this.teamManager.getOAuthState(teamDid);
3000
- return oauthClientState.update(input, context);
741
+ inviteMembersToOrg(params, context) {
742
+ return orgManager.inviteMembersToOrg(this, params, context);
3001
743
  }
3002
744
 
3003
- async getOrgs({ teamDid, ...payload }, context) {
3004
- try {
3005
- const state = await this.getOrgState(teamDid);
3006
- const { passports, orgs, ...rest } = await state.list(payload, context);
3007
- const { includePassports = true } = payload.options || {};
3008
- if (includePassports) {
3009
- // 获取每个组织的 passports
3010
- const orgPassports = await Promise.all(orgs.map((o) => this.getRoles({ teamDid, orgId: o.id })));
3011
-
3012
- orgs.forEach((o, index) => {
3013
- const roles = orgPassports[index]; // 获取每个组织的角色
3014
- // 过滤 passports
3015
- o.passports = passports.filter((p) => roles.some((r) => r.name === p.name));
3016
- });
3017
- } else {
3018
- orgs.forEach((o) => {
3019
- o.passports = [];
3020
- });
3021
- }
3022
-
3023
- return {
3024
- ...rest,
3025
- orgs,
3026
- };
3027
- } catch (err) {
3028
- logger.error('Failed to get orgs', { err, teamDid });
3029
- throw err;
3030
- }
745
+ removeOrgMember({ teamDid, orgId, userDid }, context) {
746
+ return orgManager.removeOrgMember(this, { teamDid, orgId, userDid }, context);
3031
747
  }
3032
748
 
3033
- async getOrg({ teamDid, id }, context) {
3034
- try {
3035
- const state = await this.getOrgState(teamDid);
3036
- return state.get({ id }, context);
3037
- } catch (err) {
3038
- logger.error('Failed to get org', { err, teamDid, id });
3039
- throw err;
3040
- }
749
+ getOrgMembers({ teamDid, orgId, paging }, context) {
750
+ return orgManager.getOrgMembers(this, { teamDid, orgId, paging }, context);
3041
751
  }
3042
752
 
3043
- async createDefaultOrgForUser({ teamDid, user }) {
3044
- try {
3045
- // 创建失败不要影响主流程
3046
- await this.createOrg(
3047
- {
3048
- teamDid,
3049
- name: user.fullName,
3050
- description: `this is a default org for ${user.fullName}`,
3051
- throwOnValidationError: false,
3052
- },
3053
- { user }
3054
- );
3055
- } catch (err) {
3056
- logger.error('Failed to create default org for user', { err, teamDid, user });
3057
- }
753
+ getOrgInvitableUsers({ teamDid, id, query, paging }, context) {
754
+ return orgManager.getOrgInvitableUsers(this, { teamDid, id, query, paging }, context);
3058
755
  }
3059
756
 
3060
- async issueOrgOwnerPassport({ teamDid, org }) {
3061
- try {
3062
- // 创建 org 的 owner passport, 并赋值给 owner
3063
- const roleName = md5(`${org.id}-owner`); // 避免 name 重复
3064
- await this.createRole({ teamDid, name: roleName, title: org.name, description: 'Owner', orgId: org.id });
3065
- await this.issuePassportToUser({
3066
- teamDid,
3067
- userDid: org.ownerDid,
3068
- role: roleName,
3069
- notification: {},
3070
- });
3071
- } catch (err) {
3072
- logger.error('Failed to create passport to org owner', { err, teamDid, org });
3073
- }
757
+ getOrgResource({ teamDid, orgId, resourceId }, context) {
758
+ return orgManager.getOrgResource(this, { teamDid, orgId, resourceId }, context);
3074
759
  }
3075
760
 
3076
- async createOrg({ teamDid, deferPassport = false, throwOnValidationError = true, ...rest }, context) {
3077
- try {
3078
- // 1. 对输入进行转义
3079
- const sanitizedOrg = {
3080
- ...rest,
3081
- description: sanitizeTag(rest.description || ''),
3082
- name: sanitizeTag(rest.name || ''),
3083
- };
3084
-
3085
- const { error } = createOrgInputSchema.validate(sanitizedOrg);
3086
- if (error) {
3087
- throw new CustomError(400, error.message);
3088
- }
3089
-
3090
- const state = await this.getOrgState(teamDid);
3091
- const blocklet = await getBlocklet({
3092
- did: teamDid,
3093
- states: this.states,
3094
- dataDirs: this.dataDirs,
3095
- useCache: true,
3096
- });
3097
- const checkedUserDid = rest.ownerDid || context.user.did || '';
3098
- const orgCount = await state.getOrgCountByUser(checkedUserDid);
3099
-
3100
- const { veriftMaxOrgPerUser } = createOrgValidators(blocklet);
3101
-
3102
- try {
3103
- veriftMaxOrgPerUser(orgCount); // 验证用户创建的 org 数量是否超过最大限制, 内部已经验证 org 是否开启
3104
- } catch (_error) {
3105
- if (throwOnValidationError) {
3106
- throw _error;
3107
- }
3108
- logger.warn('Failed to validate org creation', { error: _error, teamDid, orgCount });
3109
- return undefined;
3110
- }
3111
-
3112
- const result = await state.create({ ...sanitizedOrg }, context);
3113
-
3114
- // 创建 org 的 owner passport, 并赋值给 owner
3115
- if (!deferPassport) {
3116
- await this.issueOrgOwnerPassport({ teamDid, org: result });
3117
- } else {
3118
- const jobId = md5(`${teamDid}-${result.id}-${checkedUserDid}`);
3119
- this.passportIssueQueue.push(
3120
- {
3121
- action: 'issueOrgOwnerPassport',
3122
- entity: 'blocklet',
3123
- entityId: teamDid,
3124
- params: {
3125
- teamDid,
3126
- org: result,
3127
- },
3128
- },
3129
- jobId,
3130
- true
3131
- );
3132
- }
3133
-
3134
- return result;
3135
- } catch (err) {
3136
- logger.error('Failed to create org', err, {
3137
- teamDid,
3138
- name: rest.name,
3139
- userDid: rest.ownerDid || context.user.did || '',
3140
- });
3141
- throw err;
3142
- }
761
+ addOrgResource({ teamDid, orgId, resourceIds, type, metadata }, context) {
762
+ return orgManager.addOrgResource(this, { teamDid, orgId, resourceIds, type, metadata }, context);
3143
763
  }
3144
764
 
3145
- async updateOrg({ teamDid, org }, context) {
3146
- try {
3147
- const sanitizedOrg = {
3148
- ...org,
3149
- description: sanitizeTag(org.description),
3150
- name: sanitizeTag(org.name),
3151
- };
3152
-
3153
- const { error } = updateOrgInputSchema.validate(sanitizedOrg);
3154
- if (error) {
3155
- throw new CustomError(400, error.message);
3156
- }
3157
-
3158
- const state = await this.getOrgState(teamDid);
3159
- return state.updateOrg({ org: sanitizedOrg }, context);
3160
- } catch (err) {
3161
- logger.error('Failed to update org', { err, teamDid });
3162
- throw err;
3163
- }
765
+ removeOrgResource({ teamDid, orgId, resourceIds }, context) {
766
+ return orgManager.removeOrgResource(this, { teamDid, orgId, resourceIds }, context);
3164
767
  }
3165
768
 
3166
- async deleteOrg({ teamDid, id }, context) {
3167
- try {
3168
- const state = await this.getOrgState(teamDid);
3169
- // 要同时删除与 org 相关的 passport 和 邀请链接
3170
- const roles = await this.getRoles({ teamDid, orgId: id });
3171
- const result = await state.deleteOrg({ id }, context);
3172
- try {
3173
- await state.removeOrgRelatedData({ roles, orgId: id });
3174
- } catch (err) {
3175
- logger.error('Failed to remove org related data', { err, teamDid, roles, id });
3176
- }
3177
- return result;
3178
- } catch (err) {
3179
- logger.error('Failed to delete org', { err, teamDid, id });
3180
- throw err;
3181
- }
769
+ migrateOrgResource({ teamDid, from, to, resourceIds }, context) {
770
+ return orgManager.migrateOrgResource(this, { teamDid, from, to, resourceIds }, context);
3182
771
  }
3183
772
 
3184
- async addOrgMember({ teamDid, orgId, userDid }, context) {
3185
- try {
3186
- const state = await this.getOrgState(teamDid);
3187
- return state.addOrgMember({ orgId, userDid }, context);
3188
- } catch (err) {
3189
- logger.error('Add member to org failed', { err, teamDid, orgId, userDid });
3190
- throw err;
3191
- }
773
+ getNotificationStats({ teamDid, since }) {
774
+ return orgManager.getNotificationStats(this, { teamDid, since });
3192
775
  }
3193
776
 
3194
- async updateOrgMember({ teamDid, orgId, userDid, status }, context) {
3195
- try {
3196
- const state = await this.getOrgState(teamDid);
3197
- return state.updateOrgMember({ orgId, userDid, status }, context);
3198
- } catch (err) {
3199
- logger.error('Update member in org failed', { err, teamDid, orgId, userDid, status });
3200
- throw err;
3201
- }
777
+ // ============ Delegated Methods (from teamManager) ============
778
+ getRoles({ teamDid, orgId }) {
779
+ return this.teamManager.getRoles(teamDid, orgId);
3202
780
  }
3203
781
 
3204
- /**
3205
- * 发送邀请通知
3206
- * @param {*} param0
3207
- */
3208
- async sendInvitationNotification({
3209
- teamDid,
3210
- invitor,
3211
- org,
3212
- role,
3213
- successUserDids,
3214
- inviteLink,
3215
- email,
3216
- inviteType,
3217
- blocklet,
3218
- }) {
3219
- try {
3220
- const userInfo = await this.getUser({
3221
- teamDid,
3222
- user: { did: invitor.did },
3223
- options: { enableConnectedAccount: true },
3224
- });
3225
-
3226
- // 检测是否开启了 email 服务
3227
- const provider = getEmailServiceProvider(blocklet);
3228
-
3229
- const translate = {
3230
- en: {
3231
- title: 'Inviting you to join the organization',
3232
- description: `<${userInfo.fullName}(did:abt:${userInfo.did})> invites you to join the ${org.name} organization, role is ${role}.<br/>After accepting, you will be able to access the resources and collaboration content of the organization.<br/><br/>Please click the button below to handle the invitation.<br/><br/>If you don't want to join, you can ignore this notification.`,
3233
- accept: 'Accept',
3234
- },
3235
- zh: {
3236
- title: '邀请您加入组织',
3237
- description: `<${userInfo.fullName}(did:abt:${userInfo.did})> 邀请您加入 ${org.name} 组织,角色为 ${role}。<br/>接受后,你将能够访问该组织的资源和协作内容。<br/><br/>请点击下方按钮处理邀请。<br/><br/>如果你不想加入,可以忽略此通知。`,
3238
- accept: '接受',
3239
- },
3240
- };
3241
- const content = translate[userInfo.locale || 'en'] || translate.en;
3242
-
3243
- const message = {
3244
- title: content.title,
3245
- body: content.description,
3246
- actions: [
3247
- {
3248
- name: content.accept,
3249
- title: content.accept,
3250
- link: inviteLink,
3251
- },
3252
- ],
3253
- };
3254
-
3255
- if (inviteType === 'internal') {
3256
- await this.createNotification({
3257
- teamDid,
3258
- receiver: successUserDids,
3259
- entityId: teamDid,
3260
- source: 'system',
3261
- severity: 'info',
3262
- ...message,
3263
- });
3264
- logger.info('Invite notification sent successfully', {
3265
- teamDid,
3266
- orgId: org.id,
3267
- sentToUsers: successUserDids,
3268
- sentCount: successUserDids.length,
3269
- });
3270
- } else if (inviteType === 'external' && provider && email) {
3271
- // 当 service 开启 email 服务时才会发送邮件通知
3272
- const emailInputSchema = Joi.string().email().required();
3273
- const { error } = emailInputSchema.validate(email);
3274
- if (error) {
3275
- throw new CustomError(400, error.message);
3276
- }
3277
- const nodeInfo = await this.node.read();
3278
- const blockletInfo = getBlockletInfo(blocklet, nodeInfo.sk);
3279
- const sender = {
3280
- appDid: blockletInfo.wallet.address,
3281
- appSk: blockletInfo.wallet.secretKey,
3282
- };
3283
-
3284
- await sendToUser(email, message, sender, undefined, 'send-to-mail');
3285
- logger.info('Send invitation notification to email completed', {
3286
- teamDid,
3287
- orgId: org.id,
3288
- email,
3289
- });
3290
- }
3291
- } catch (notificationErr) {
3292
- // 通知发送失败不影响邀请的成功,只记录警告
3293
- logger.warn('Failed to send invitation notification, but invitations were created successfully', {
3294
- notificationErr,
3295
- teamDid,
3296
- orgId: org.id,
3297
- successUserDids,
3298
- });
3299
- }
782
+ getRBAC(teamDid) {
783
+ return this.teamManager.getRBAC(teamDid);
3300
784
  }
3301
785
 
3302
- getFederatedMasterBlockletInfo({ blocklet }) {
3303
- const sites = blocklet.settings?.federated?.sites || [];
3304
- const federatedMaster = sites.find((item) => item.isMaster !== false);
3305
- const federatedCurrent = sites.find((item) => item.appId === blocklet.appDid);
3306
- const isFederated = !!federatedMaster || !!federatedCurrent;
3307
- if (!isFederated) {
3308
- return undefined;
3309
- }
3310
-
3311
- const formatBlockletInfo = (federateBlocklet) => ({
3312
- appId: federateBlocklet?.appId,
3313
- appName: federateBlocklet?.appName,
3314
- appDescription: federateBlocklet?.appDescription,
3315
- appLogo: federateBlocklet?.appLogo,
3316
- appPid: federateBlocklet?.appPid,
3317
- appUrl: federateBlocklet?.appUrl,
3318
- version: federateBlocklet?.version,
3319
- sourceAppPid: federateBlocklet?.appPid,
3320
- provider: 'wallet',
3321
- });
3322
-
3323
- const blocklets = [];
3324
- if (federatedCurrent?.status === 'approved') {
3325
- blocklets.push(formatBlockletInfo(federatedMaster));
3326
- }
3327
- if (federatedCurrent) {
3328
- blocklets.push({
3329
- ...formatBlockletInfo(federatedCurrent),
3330
- sourceAppPid: null,
3331
- });
3332
- } else {
3333
- const blockletInfo = getBlockletInfo(blocklet, undefined, { returnWallet: false });
3334
- blocklets.push({
3335
- ...formatBlockletInfo(blockletInfo),
3336
- appPid: blocklet?.appPid,
3337
- appLogo: joinURL(blockletInfo.appUrl, WELLKNOWN_SERVICE_PATH_PREFIX, '/blocklet/logo'),
3338
- sourceAppPid: null,
3339
- });
3340
- }
3341
-
3342
- return blocklets[0];
786
+ // State getters (delegated to teamManager)
787
+ getUserState(teamDid) {
788
+ return this.teamManager.getUserState(teamDid);
3343
789
  }
3344
790
 
3345
- /**
3346
- * 邀请成员到组织
3347
- * 1. 批量添加成员到组织 - 记录添加成功和添加失败的用户 DID
3348
- * 2. 创建邀请链接,只创建添加成功的用户邀请链接
3349
- * 3. 发送邀请通知,只发送添加成功的用户邀请通知
3350
- * @param {*} param0
3351
- * @param {*} param0.inviteType 邀请类型,internal 内部邀请,external 外部邀请
3352
- * @param {*} context
3353
- * @returns 返回邀请成功的用户和失败的用户 did 列表
3354
- */
3355
- async inviteMembersToOrg({ teamDid, orgId, userDids, role, inviteType = 'internal', email }, context) {
3356
- try {
3357
- const state = await this.getOrgState(teamDid);
3358
- const { user } = context || {};
3359
- if (!user) {
3360
- throw new CustomError(400, 'User is required');
3361
- }
3362
- const org = await this.getOrg({ teamDid, id: orgId }, context);
3363
- if (!org) {
3364
- throw new CustomError(400, 'Org not found');
3365
- }
3366
-
3367
- // dashboard 或者非 org owner 无法邀请成员
3368
- if (isAdmingPath(context) || !isOrgOwner(user, org)) {
3369
- throw new CustomError(403, "You cannot invite members to other users' org");
3370
- }
3371
-
3372
- if (inviteType === 'internal' && userDids.length === 0) {
3373
- throw new CustomError(400, 'You must invite at least one user');
3374
- }
3375
-
3376
- // Step 1: 批量添加成员到组织 - 记录添加成功和添加失败的用户 DID
3377
- const successUserDids = [];
3378
- const failedUserDids = [];
3379
- const skipInviteUserDids = [];
3380
-
3381
- // 内部邀请
3382
- if (inviteType === 'internal') {
3383
- for (const userDid of userDids) {
3384
- try {
3385
- // eslint-disable-next-line no-await-in-loop
3386
- const currentUser = await this.getUser({
3387
- teamDid,
3388
- user: { did: userDid },
3389
- });
3390
- const walletDid = getWalletDid(currentUser);
3391
- // 如果当前用户不是钱包用户,则跳过邀请 (参考颁发通行证逻辑)
3392
- const skipInvite = walletDid !== userDid;
3393
- if (skipInvite) {
3394
- skipInviteUserDids.push(userDid);
3395
- }
3396
- const status = skipInvite ? 'active' : 'inviting';
3397
- // eslint-disable-next-line no-await-in-loop
3398
- await state.addOrgMember({ orgId, userDid, status }, context);
3399
- successUserDids.push(userDid);
3400
- } catch (addErr) {
3401
- failedUserDids.push(userDid);
3402
- logger.warn('Failed to add user to org', { userDid, orgId, error: addErr.message });
3403
- }
3404
- }
3405
-
3406
- logger.info('Batch add members to org completed', {
3407
- teamDid,
3408
- orgId,
3409
- totalUsers: userDids.length,
3410
- successCount: successUserDids.length,
3411
- failedCount: failedUserDids.length,
3412
- });
3413
-
3414
- // 如果没有成功添加的用户,直接返回结果
3415
- if (successUserDids.length === 0) {
3416
- logger.warn('No users were successfully added to org', { teamDid, orgId, failedUserDids });
3417
- throw new CustomError(500, 'No users were successfully added to org');
3418
- }
3419
- }
3420
-
3421
- // 要排除 OAuth 用户
3422
- const inviteUserDids = successUserDids.filter((did) => !skipInviteUserDids.includes(did));
3423
-
3424
- if (skipInviteUserDids.length > 0) {
3425
- logger.info('OAuth users were successfully added to org', { teamDid, orgId, skipInviteUserDids });
3426
- // 向 OAuth 用户颁发通行证
3427
- for (const userDid of skipInviteUserDids) {
3428
- // eslint-disable-next-line no-await-in-loop
3429
- await this.issuePassportToUser({
3430
- teamDid,
3431
- userDid,
3432
- role,
3433
- notification: {},
3434
- });
3435
- }
3436
- }
3437
-
3438
- // 如果没有其他用户,那么直接返回即可
3439
- if (inviteType === 'internal' && inviteUserDids.length === 0) {
3440
- return {
3441
- successDids: successUserDids,
3442
- failedDids: failedUserDids,
3443
- };
3444
- }
3445
-
3446
- // Previous Step 2: 要判断站点是否是站点群内,如果是站点群中,需要使用 master 的 appPid 作为 sourceAppPid 创建邀请链接
3447
-
3448
- const blocklet = await getBlocklet({ did: teamDid, states: this.states, dataDirs: this.dataDirs });
3449
- const masterBlockletInfo = this.getFederatedMasterBlockletInfo({ blocklet });
3450
-
3451
- // Step 2: 创建邀请链接,只创建添加成功的用户邀请链接
3452
-
3453
- const inviteInfo = await this.createMemberInvitation(
3454
- {
3455
- teamDid,
3456
- role,
3457
- expireTime: this.memberInvitationExpireTime,
3458
- orgId,
3459
- inviteUserDids: inviteType === 'internal' ? inviteUserDids : [],
3460
- sourceAppPid: masterBlockletInfo?.appPid,
3461
- },
3462
- context
3463
- );
3464
-
3465
- const inviteLink = getOrgInviteLink(inviteInfo, blocklet);
3466
-
3467
- logger.info('Invite link created for successful users', {
3468
- teamDid,
3469
- orgId,
3470
- inviteId: inviteInfo.inviteId,
3471
- userCount: successUserDids.length,
3472
- });
3473
-
3474
- // Step 3: 发送邀请通知,只发送添加成功的用户邀请通知
3475
-
3476
- this.sendInvitationNotification({
3477
- teamDid,
3478
- invitor: user,
3479
- org,
3480
- role,
3481
- successUserDids,
3482
- email,
3483
- inviteType,
3484
- inviteLink,
3485
- blocklet,
3486
- });
3487
-
3488
- // 返回成功和失败的用户列表
3489
- return {
3490
- successDids: successUserDids,
3491
- failedDids: failedUserDids,
3492
- inviteLink,
3493
- };
3494
- } catch (err) {
3495
- logger.error('Invite users to org failed', { err, teamDid, orgId, userDids, role });
3496
- throw err;
3497
- }
791
+ getSessionState(teamDid) {
792
+ return this.teamManager.getSessionState(teamDid);
3498
793
  }
3499
794
 
3500
- async removeOrgMember({ teamDid, orgId, userDid }, context) {
3501
- try {
3502
- const state = await this.getOrgState(teamDid);
3503
- const roles = await this.getRoles({ teamDid, orgId });
3504
- const result = await state.removeOrgMember({ orgId, userDid }, context);
3505
- try {
3506
- await state.removeOrgRelatedData({ roles, orgId, userDid });
3507
- } catch (err) {
3508
- logger.error('Failed to remove user related passports', { err, teamDid, roles, userDid });
3509
- }
3510
- return result;
3511
- } catch (err) {
3512
- logger.error('Remove member from org failed', { err, teamDid, orgId, userDid });
3513
- throw err;
3514
- }
795
+ getNotificationState(teamDid) {
796
+ return this.teamManager.getNotificationState(teamDid);
3515
797
  }
3516
798
 
3517
- async getOrgMembers({ teamDid, orgId, paging }, context) {
3518
- try {
3519
- const state = await this.getOrgState(teamDid);
3520
- const result = await state.getOrgMembers({ orgId, paging, options: { includePassport: true } }, context);
3521
- const info = await this.node.read();
3522
- const isServer = this.teamManager.isNodeTeam(teamDid);
3523
- const blocklet = await getBlocklet({ did: teamDid, states: this.states, dataDirs: this.dataDirs });
3524
- const baseUrl = blocklet.environmentObj.BLOCKLET_APP_URL;
3525
- const roles = await this.getRoles({ teamDid, orgId });
3526
- result.users.forEach((item) => {
3527
- if (item?.user?.avatar) {
3528
- item.user.avatar = getUserAvatarUrl(baseUrl, item.user.avatar, info, isServer);
3529
- }
3530
- if (item?.user?.passports?.length > 0) {
3531
- item.user.passports = item.user.passports.filter((passport) =>
3532
- roles.some((role) => role.name === passport.name)
3533
- );
3534
- }
3535
- });
3536
-
3537
- return result;
3538
- } catch (err) {
3539
- logger.error('Get org members failed', { err, teamDid, orgId });
3540
- throw err;
3541
- }
799
+ getTagState(teamDid) {
800
+ return this.teamManager.getTagState(teamDid);
3542
801
  }
3543
802
 
3544
- async getOrgInvitableUsers({ teamDid, id, query, paging }, context) {
3545
- try {
3546
- const state = await this.getOrgState(teamDid);
3547
- return state.getOrgInvitableUsers({ id, query, paging }, context);
3548
- } catch (err) {
3549
- logger.error('Get org invitable users failed', { err, teamDid, id });
3550
- throw err;
3551
- }
803
+ getTaggingState(teamDid) {
804
+ return this.teamManager.getTaggingState(teamDid);
3552
805
  }
3553
806
 
3554
- async getOrgResource({ teamDid, orgId, resourceId }, context) {
3555
- try {
3556
- const state = await this.getOrgState(teamDid);
3557
- return state.getOrgResource({ orgId, resourceId }, context);
3558
- } catch (err) {
3559
- logger.error('Get org resource failed', { err, teamDid, orgId, resourceId });
3560
- throw err;
3561
- }
807
+ getVerifyCodeState(teamDid) {
808
+ return this.teamManager.getVerifyCodeState(teamDid);
3562
809
  }
3563
810
 
3564
- async addOrgResource({ teamDid, orgId, resourceIds, type, metadata }, context) {
3565
- try {
3566
- const state = await this.getOrgState(teamDid);
3567
- return state.addOrgResource({ orgId, resourceIds, type, metadata }, context);
3568
- } catch (err) {
3569
- logger.error('Add org resource failed', { err, teamDid, orgId, resourceIds, type, metadata });
3570
- throw err;
3571
- }
811
+ getAccessKeyState(teamDid) {
812
+ return this.teamManager.getAccessKeyState(teamDid);
3572
813
  }
3573
814
 
3574
- async removeOrgResource({ teamDid, orgId, resourceIds }, context) {
3575
- try {
3576
- const state = await this.getOrgState(teamDid);
3577
- return state.removeOrgResource({ orgId, resourceIds }, context);
3578
- } catch (err) {
3579
- logger.error('Remove org resource failed', { err, teamDid, orgId, resourceIds });
3580
- throw err;
3581
- }
815
+ getOrgState(teamDid) {
816
+ return this.teamManager.getOrgState(teamDid);
3582
817
  }
3583
818
 
3584
- async migrateOrgResource({ teamDid, from, to, resourceIds }, context) {
3585
- try {
3586
- const state = await this.getOrgState(teamDid);
3587
- return state.migrateOrgResource({ from, to, resourceIds }, context);
3588
- } catch (err) {
3589
- logger.error('Migrate org resource failed', { err, teamDid, from, to, resourceIds });
3590
- throw err;
3591
- }
819
+ getUserSessionState(teamDid) {
820
+ return this.teamManager.getUserSessionState(teamDid);
3592
821
  }
3593
822
 
3594
- async getNotificationStats({ teamDid, since = '1h' }) {
3595
- let startTime = dayjs().subtract(1, 'hours').toDate();
3596
-
3597
- if (since && typeof since === 'string') {
3598
- const sinceMatch = since.match(/^(\d+)h$/);
3599
- if (sinceMatch) {
3600
- let hours = parseInt(sinceMatch[1], 10);
3601
- if (hours < 1 || hours > 24) {
3602
- hours = Math.min(Math.max(hours, 1), 24);
3603
- }
3604
- startTime = dayjs().subtract(hours, 'hours').toDate();
3605
- }
3606
- }
3607
-
3608
- try {
3609
- const state = await this.getNotificationState(teamDid);
3610
- const isServer = this.teamManager.isNodeTeam(teamDid);
3611
- const blocklet = isServer
3612
- ? {}
3613
- : await getBlocklet({ did: teamDid, states: this.states, dataDirs: this.dataDirs });
3614
- const channelsAvailable = checkPushChannelAvailable(blocklet, isServer);
3615
- const results = await state.getNotificationsBySince({ since });
3616
-
3617
- if (results.length === 0) {
3618
- return {
3619
- healthy: true,
3620
- message: `There have been no push records since ${startTime}. Please choose another time range`,
3621
- since: startTime,
3622
- channels: channelsAvailable,
3623
- };
3624
- }
3625
-
3626
- const pushState = getNotificationPushState(results, channelsAvailable, isServer);
3627
-
3628
- let teamDids = [teamDid];
3629
- if (isServer) {
3630
- const nodeInfo = await this.node.read();
3631
- teamDids = [nodeInfo.did];
3632
- } else {
3633
- teamDids = getBlockletAppIdList(blocklet);
3634
- }
3635
-
3636
- const pendingResult = await this.states.job.getPendingNotifications({
3637
- teamDids,
3638
- isServer,
3639
- channels: Object.keys(channelsAvailable),
3640
- createdAt: startTime,
3641
- });
3642
-
3643
- // pushState 的 key 与 pendingResult 的 key 映射关系
3644
- const channelKeyMap = {
3645
- pushKit: NOTIFICATION_SEND_CHANNEL.PUSH,
3646
- wallet: NOTIFICATION_SEND_CHANNEL.WALLET,
3647
- email: NOTIFICATION_SEND_CHANNEL.EMAIL,
3648
- webhook: NOTIFICATION_SEND_CHANNEL.WEBHOOK,
3649
- };
3650
-
3651
- // 合并 pending 数量到对应的 channel
3652
- const channels = Object.entries(pushState).reduce((acc, [key, value]) => {
3653
- const pendingKey = channelKeyMap[key] || key;
3654
- acc[key] = {
3655
- ...value,
3656
- pending: pendingResult[pendingKey] || 0,
3657
- };
3658
- return acc;
3659
- }, {});
3660
-
3661
- return {
3662
- healthy: true,
3663
- since: startTime,
3664
- channels,
3665
- };
3666
- } catch (err) {
3667
- logger.error('Get notification service health failed', err, { teamDid });
3668
- return {
3669
- healthy: false,
3670
- error: err.message,
3671
- since: startTime,
3672
- };
3673
- }
823
+ // ============ Just for test ============
824
+ setInviteExpireTime(ms) {
825
+ this.memberInviteExpireTime = ms;
3674
826
  }
3675
827
  }
3676
828