@abtnode/core 1.16.52-beta-20251002-030549-0f91dab2 → 1.16.52-beta-20251005-235515-42ad5caf

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/api/team.js CHANGED
@@ -8,7 +8,6 @@ const cloneDeep = require('@abtnode/util/lib/deep-clone');
8
8
  const { joinURL, withoutTrailingSlash } = require('ufo');
9
9
  const { Op } = require('sequelize');
10
10
  const dayjs = require('@abtnode/util/lib/dayjs');
11
- const Joi = require('joi');
12
11
  const logger = require('@abtnode/logger')('@abtnode/core:api:team');
13
12
  const {
14
13
  ROLES,
@@ -20,6 +19,7 @@ const {
20
19
  USER_AVATAR_URL_PREFIX,
21
20
  SESSION_TTL,
22
21
  EVENTS,
22
+ NOTIFICATION_SEND_CHANNEL,
23
23
  } = require('@abtnode/constant');
24
24
  const { isValid: isValidDid } = require('@arcblock/did');
25
25
  const { BlockletEvents, TeamEvents, BlockletInternalEvents } = require('@blocklet/constant');
@@ -49,6 +49,10 @@ const isUrl = require('is-url');
49
49
  const { PASSPORT_LOG_ACTION, PASSPORT_SOURCE, PASSPORT_ISSUE_ACTION } = require('@abtnode/constant');
50
50
  const ensureServerEndpoint = require('@abtnode/util/lib/ensure-server-endpoint');
51
51
  const { CustomError } = require('@blocklet/error');
52
+ const { getEmailServiceProvider } = require('@abtnode/auth/lib/email');
53
+ const md5 = require('@abtnode/util/lib/md5');
54
+ const { sanitizeTag } = require('@abtnode/util/lib/sanitize');
55
+ const { Joi } = require('@arcblock/validator');
52
56
 
53
57
  const { validateTrustedPassportIssuers } = require('../validators/trusted-passport');
54
58
  const { validateTrustedFactories } = require('../validators/trusted-factory');
@@ -60,6 +64,8 @@ const StoreUtil = require('../util/store');
60
64
  const { profileSchema } = require('../validators/user');
61
65
  const { passportDisplaySchema } = require('../validators/util');
62
66
  const { validateUserRolePassport } = require('../util/validate-user-role-passport');
67
+ const { getOrgInviteLink, createOrgValidators, isOrgOwner, isAdmingPath } = require('../util/org');
68
+ const { createOrgInputSchema, updateOrgInputSchema } = require('../validators/org');
63
69
 
64
70
  const sanitizeUrl = (url) => {
65
71
  if (!url) {
@@ -105,7 +111,7 @@ const sendPassportVcNotification = ({ userDid, appWallet: wallet, locale, vc, no
105
111
  ];
106
112
  }
107
113
 
108
- sendToUser(receiver, message, sender, { channels: ['wallet'] }).catch((error) => {
114
+ sendToUser(receiver, message, sender, { channels: [NOTIFICATION_SEND_CHANNEL.WALLET] }).catch((error) => {
109
115
  logger.error('Failed send passport vc to wallet', { error });
110
116
  });
111
117
  } catch (error) {
@@ -374,23 +380,47 @@ class TeamAPI extends EventEmitter {
374
380
 
375
381
  if (moveTo) {
376
382
  const records = await taggingState.find({ tagId: tag.id });
383
+ const checkPairs = records.map((r) => ({ taggableId: r.taggableId, taggableType: r.taggableType }));
384
+ const existing = await taggingState.find({ tagId: moveTo, $or: checkPairs });
385
+
386
+ const existingSet = new Set(existing.map((e) => `${e.taggableId}:${e.taggableType}`));
387
+ const toUpdate = [];
388
+ const toDelete = [];
389
+
390
+ for (const record of records) {
391
+ const key = `${record.taggableId}:${record.taggableType}`;
392
+ if (existingSet.has(key)) {
393
+ toDelete.push(record);
394
+ logger.info('tag already at moveTo, will delete record', { teamDid, tag, record });
395
+ } else {
396
+ toUpdate.push(record);
397
+ }
398
+ }
377
399
 
378
- await Promise.all(
379
- records.map(async (record) => {
380
- const found = await taggingState.findOne({
381
- tagId: moveTo,
382
- taggableId: record.taggableId,
383
- taggableType: record.taggableType,
384
- });
385
- if (found) {
386
- await taggingState.remove(record);
387
- logger.info('tag already at moveTo, deleted record', { teamDid, tag, record });
388
- } else {
389
- await taggingState.update({ tagId: record.tagId }, { $set: { tagId: moveTo } });
390
- logger.info('tag moved successfully', { teamDid, tag, record, moveTo });
391
- }
392
- })
393
- );
400
+ if (toUpdate.length > 0) {
401
+ await Promise.all(
402
+ toUpdate.map((record) =>
403
+ taggingState
404
+ .update(
405
+ { tagId: record.tagId, taggableId: record.taggableId, taggableType: record.taggableType },
406
+ { $set: { tagId: moveTo } }
407
+ )
408
+ .then(() => {
409
+ logger.info('tag moved successfully', { teamDid, tag, record, moveTo });
410
+ })
411
+ )
412
+ );
413
+ }
414
+
415
+ if (toDelete.length > 0) {
416
+ await Promise.all(
417
+ toDelete.map((record) =>
418
+ taggingState.remove(record).then(() => {
419
+ logger.info('tagging deleted successfully', { teamDid, tag, record });
420
+ })
421
+ )
422
+ );
423
+ }
394
424
  }
395
425
 
396
426
  const doc = await state.remove({ id: tag.id });
@@ -440,6 +470,8 @@ class TeamAPI extends EventEmitter {
440
470
  logger.info('user updated successfully', { teamDid, userDid: user.did });
441
471
  this.emit(TeamEvents.userUpdated, { teamDid, user: doc });
442
472
  } else if (_action === 'add') {
473
+ this.crateDefaultOrgForUser({ teamDid, user });
474
+
443
475
  if (teamDid === nodeInfo.did && nodeInfo.nodeOwner && user.did !== nodeInfo.nodeOwner.did && notify) {
444
476
  await this.sendNewMemberNotification(this.teamManager, user, nodeInfo);
445
477
  }
@@ -603,7 +635,7 @@ class TeamAPI extends EventEmitter {
603
635
  async getUsersCountPerRole({ teamDid }) {
604
636
  const roles = await this.getRoles({ teamDid });
605
637
  const state = await this.getUserState(teamDid);
606
- const names = ['$all', ...roles.map((x) => x.name), '$none', '$blocked'];
638
+ const names = ['$all', ...roles.filter((x) => !x.orgId).map((x) => x.name), '$none', '$blocked'];
607
639
  return Promise.all(
608
640
  names.map(async (name) => {
609
641
  const count = await state.countByPassport({ name, status: PASSPORT_STATUS.VALID });
@@ -1181,7 +1213,17 @@ class TeamAPI extends EventEmitter {
1181
1213
  * @returns
1182
1214
  */
1183
1215
  async createMemberInvitation(
1184
- { teamDid, role: roleName, expireTime, remark, sourceAppPid, display = null, passportExpireTime = null },
1216
+ {
1217
+ teamDid,
1218
+ role: roleName,
1219
+ expireTime,
1220
+ remark,
1221
+ sourceAppPid,
1222
+ display = null,
1223
+ passportExpireTime = null,
1224
+ orgId = '', // 是否是邀请加入组织
1225
+ inviteUserDids = [], // 邀请用户列表
1226
+ },
1185
1227
  context
1186
1228
  ) {
1187
1229
  await this.teamManager.checkEnablePassportIssuance(teamDid);
@@ -1197,7 +1239,7 @@ class TeamAPI extends EventEmitter {
1197
1239
  throw new Error('Passport expire time must be greater than current time');
1198
1240
  }
1199
1241
 
1200
- const roles = await this.getRoles({ teamDid });
1242
+ const roles = await this.getRoles({ teamDid, orgId });
1201
1243
  const role = roles.find((r) => r.name === roleName);
1202
1244
  if (!role) {
1203
1245
  throw new Error(`Role does not exist: ${roleName}`);
@@ -1213,7 +1255,8 @@ class TeamAPI extends EventEmitter {
1213
1255
  throw new Error('Inviter does not exist');
1214
1256
  }
1215
1257
 
1216
- if ((user?.role || '').startsWith('blocklet-')) {
1258
+ // 如果邀请加入组织,则不验证 role passport
1259
+ if (!orgId && (user?.role || '').startsWith('blocklet-')) {
1217
1260
  const userInfo = await this.getUser({ teamDid, user });
1218
1261
  validateUserRolePassport({
1219
1262
  role: (user?.role || '').replace('blocklet-', ''),
@@ -1230,6 +1273,8 @@ class TeamAPI extends EventEmitter {
1230
1273
  expireDate,
1231
1274
  inviter: user,
1232
1275
  teamDid,
1276
+ orgId,
1277
+ inviteUserDids,
1233
1278
  sourceAppPid,
1234
1279
  display,
1235
1280
  passportExpireTime,
@@ -1272,14 +1317,32 @@ class TeamAPI extends EventEmitter {
1272
1317
  receiver: invitation.receiver,
1273
1318
  sourceAppPid: invitation.sourceAppPid || null,
1274
1319
  display: invitation.display || null,
1320
+ orgId: invitation.orgId || null,
1321
+ inviteUserDids: invitation.inviteUserDids || [],
1275
1322
  passportExpireTime: invitation.passportExpireTime || null,
1276
1323
  };
1277
1324
  }
1278
1325
 
1279
- async getInvitations({ teamDid, filter }) {
1326
+ async getInvitations({ teamDid, filter, orgId = '' }, context = {}) {
1280
1327
  const state = await this.getSessionState(teamDid);
1281
-
1282
- const invitations = await state.find({ type: 'invite' });
1328
+ let invitations = [];
1329
+ if (orgId) {
1330
+ const org = await this.getOrg({ teamDid, id: orgId }, context);
1331
+ const { user } = context;
1332
+ if (!org) {
1333
+ throw new CustomError(404, `Org does not exist: ${orgId}`);
1334
+ }
1335
+ if (org.ownerDid !== user?.did) {
1336
+ throw new CustomError(403, `You are not the owner of the org: ${orgId}`);
1337
+ }
1338
+ }
1339
+ const query = { type: 'invite' };
1340
+ if (orgId) {
1341
+ query['__data.orgId'] = orgId;
1342
+ } else {
1343
+ query.$or = [{ '__data.orgId': { $exists: false } }, { '__data.orgId': '' }];
1344
+ }
1345
+ invitations = await state.find(query);
1283
1346
 
1284
1347
  return invitations.filter(filter || ((x) => x.status !== 'success')).map((d) => ({
1285
1348
  // eslint-disable-next-line no-underscore-dangle
@@ -1293,6 +1356,8 @@ class TeamAPI extends EventEmitter {
1293
1356
  receiver: d.receiver,
1294
1357
  sourceAppPid: d.sourceAppPid || null,
1295
1358
  display: d.display || null,
1359
+ orgId: d.orgId || null,
1360
+ inviteUserDids: d.inviteUserDids || [],
1296
1361
  passportExpireTime: d.passportExpireTime || null,
1297
1362
  }));
1298
1363
  }
@@ -1328,7 +1393,9 @@ class TeamAPI extends EventEmitter {
1328
1393
  }
1329
1394
 
1330
1395
  const roles = await this.getRoles({ teamDid });
1331
- if (!roles.some((r) => r.name === role)) {
1396
+ const orgRoles = await this.getRoles({ teamDid, orgId: invitation.orgId });
1397
+ const allRoles = [...roles, ...orgRoles];
1398
+ if (!allRoles.some((r) => r.name === role)) {
1332
1399
  throw new Error(`Role does not exist: ${role}`);
1333
1400
  }
1334
1401
 
@@ -1338,7 +1405,7 @@ class TeamAPI extends EventEmitter {
1338
1405
  };
1339
1406
  }
1340
1407
 
1341
- async closeInvitation({ teamDid, inviteId, status, receiver, timeout = 30 * 1000 }) {
1408
+ async closeInvitation({ teamDid, inviteId, status, receiver, isOrgInvite, timeout = 30 * 1000 }) {
1342
1409
  const state = await this.getSessionState(teamDid);
1343
1410
 
1344
1411
  const invitation = await state.read(inviteId);
@@ -1346,17 +1413,30 @@ class TeamAPI extends EventEmitter {
1346
1413
  if (!invitation) {
1347
1414
  throw new Error(`The invitation does not exist: ${inviteId}`);
1348
1415
  }
1416
+ let shouldRemoveInviteSession = false;
1417
+ if (isOrgInvite && invitation?.inviteUserDids?.length > 0) {
1418
+ const unReceiverUserDids = invitation.inviteUserDids.filter((did) => did !== receiver.did);
1419
+ shouldRemoveInviteSession = unReceiverUserDids.length === 0;
1420
+ await state.update(inviteId, {
1421
+ receiver,
1422
+ inviteUserDids: unReceiverUserDids,
1423
+ ...(shouldRemoveInviteSession ? { status } : {}),
1424
+ });
1425
+ } else {
1426
+ await state.update(inviteId, { status, receiver });
1427
+ shouldRemoveInviteSession = true;
1428
+ }
1349
1429
 
1350
- await state.update(inviteId, { status, receiver });
1351
-
1352
- setTimeout(async () => {
1353
- try {
1354
- logger.info('Invitation session closed', { inviteId });
1355
- await state.end(inviteId);
1356
- } catch (error) {
1357
- logger.error('close invitation failed', { error });
1358
- }
1359
- }, timeout);
1430
+ if (shouldRemoveInviteSession) {
1431
+ setTimeout(async () => {
1432
+ try {
1433
+ logger.info('Invitation session closed', { inviteId });
1434
+ await state.end(inviteId);
1435
+ } catch (error) {
1436
+ logger.error('close invitation failed', { error });
1437
+ }
1438
+ }, timeout);
1439
+ }
1360
1440
  }
1361
1441
 
1362
1442
  // transfer
@@ -1704,8 +1784,8 @@ class TeamAPI extends EventEmitter {
1704
1784
 
1705
1785
  // Access Control
1706
1786
 
1707
- getRoles({ teamDid }) {
1708
- return this.teamManager.getRoles(teamDid);
1787
+ getRoles({ teamDid, orgId }) {
1788
+ return this.teamManager.getRoles(teamDid, orgId);
1709
1789
  }
1710
1790
 
1711
1791
  async getRole({ teamDid, role: { name } = {} }) {
@@ -1714,12 +1794,12 @@ class TeamAPI extends EventEmitter {
1714
1794
  }
1715
1795
  const rbac = await this.getRBAC(teamDid);
1716
1796
  const role = await rbac.getRole(name);
1717
- return role ? pick(role, ['name', 'grants', 'title', 'description', 'extra']) : null;
1797
+ return role ? pick(role, ['name', 'grants', 'title', 'description', 'extra', 'orgId']) : null;
1718
1798
  }
1719
1799
 
1720
- async createRole({ teamDid, name, description, title, childName, permissions = [], extra: raw }) {
1800
+ async createRole({ teamDid, name, description, title, childName, permissions = [], extra: raw, orgId }) {
1721
1801
  logger.info('create role', { teamDid, name, description, childName, permissions, raw });
1722
- const attrs = { name, title, description, childName, permissions };
1802
+ const attrs = { name, title, description, childName, permissions, orgId };
1723
1803
 
1724
1804
  if (raw) {
1725
1805
  try {
@@ -1738,7 +1818,7 @@ class TeamAPI extends EventEmitter {
1738
1818
  let role;
1739
1819
  try {
1740
1820
  role = await rbac.createRole(attrs);
1741
- return pick(role, ['name', 'title', 'grants', 'description', 'extra']);
1821
+ return pick(role, ['name', 'title', 'grants', 'description', 'extra', 'orgId']);
1742
1822
  } catch (err) {
1743
1823
  if (new RegExp(`Item ${name} already exists`).test(err.message)) {
1744
1824
  throw new Error(`Id ${name} already exists`);
@@ -1747,10 +1827,10 @@ class TeamAPI extends EventEmitter {
1747
1827
  }
1748
1828
  }
1749
1829
 
1750
- async updateRole({ teamDid, role: { name, title, description, extra: raw } = {} }) {
1830
+ async updateRole({ teamDid, role: { name, title, description, extra: raw } = {}, orgId }) {
1751
1831
  logger.info('update role', { teamDid, name, title, description, raw });
1752
1832
 
1753
- const attrs = { name, title, description };
1833
+ const attrs = { name, title, description, orgId };
1754
1834
 
1755
1835
  if (raw) {
1756
1836
  try {
@@ -1760,10 +1840,10 @@ class TeamAPI extends EventEmitter {
1760
1840
  }
1761
1841
  }
1762
1842
 
1763
- await validateUpdateRole(attrs);
1843
+ await validateUpdateRole(pick(attrs, ['name', 'title', 'description', 'extra']));
1764
1844
  const rbac = await this.getRBAC(teamDid);
1765
1845
  const state = await rbac.updateRole(attrs);
1766
- return pick(state, ['name', 'title', 'grants', 'description', 'extra']);
1846
+ return pick(state, ['name', 'title', 'grants', 'description', 'extra', 'orgId']);
1767
1847
  }
1768
1848
 
1769
1849
  async getPermissions({ teamDid }) {
@@ -2146,6 +2226,10 @@ class TeamAPI extends EventEmitter {
2146
2226
  return this.teamManager.getNotificationState(did);
2147
2227
  }
2148
2228
 
2229
+ getOrgState(did) {
2230
+ return this.teamManager.getOrgState(did);
2231
+ }
2232
+
2149
2233
  // =============
2150
2234
  // Just for test
2151
2235
  // =============
@@ -2703,6 +2787,454 @@ class TeamAPI extends EventEmitter {
2703
2787
  const { oauthClientState } = await this.teamManager.getOAuthState(teamDid);
2704
2788
  return oauthClientState.update(input, context);
2705
2789
  }
2790
+
2791
+ async getOrgs({ teamDid, ...payload }, context) {
2792
+ try {
2793
+ const state = await this.getOrgState(teamDid);
2794
+ const { passports, orgs, ...rest } = await state.list(payload, context);
2795
+ // 获取每个组织的 passports
2796
+ const orgPassports = await Promise.all(orgs.map((o) => this.getRoles({ teamDid, orgId: o.id })));
2797
+
2798
+ orgs.forEach((o, index) => {
2799
+ const roles = orgPassports[index]; // 获取每个组织的角色
2800
+ // 过滤 passports
2801
+ o.passports = passports.filter((p) => roles.some((r) => r.name === p.name));
2802
+ });
2803
+
2804
+ return {
2805
+ ...rest,
2806
+ orgs,
2807
+ };
2808
+ } catch (err) {
2809
+ logger.error('Failed to get orgs', { err, teamDid });
2810
+ throw err;
2811
+ }
2812
+ }
2813
+
2814
+ async getOrg({ teamDid, id }, context) {
2815
+ try {
2816
+ const state = await this.getOrgState(teamDid);
2817
+ return state.get({ id }, context);
2818
+ } catch (err) {
2819
+ logger.error('Failed to get org', { err, teamDid, id });
2820
+ throw err;
2821
+ }
2822
+ }
2823
+
2824
+ async crateDefaultOrgForUser({ teamDid, user }) {
2825
+ try {
2826
+ // 创建失败不要影响主流程
2827
+ await this.createOrg(
2828
+ { teamDid, name: user.fullName, description: `this is a default org for ${user.fullName}` },
2829
+ { user }
2830
+ );
2831
+ } catch (err) {
2832
+ logger.error('Failed to create default org for user', { err, teamDid, user });
2833
+ }
2834
+ }
2835
+
2836
+ async createOrg({ teamDid, ...rest }, context) {
2837
+ try {
2838
+ // 1. 对输入进行转义
2839
+ const sanitizedOrg = {
2840
+ ...rest,
2841
+ description: sanitizeTag(rest.description),
2842
+ name: sanitizeTag(rest.name),
2843
+ };
2844
+
2845
+ const { error } = createOrgInputSchema.validate(sanitizedOrg);
2846
+ if (error) {
2847
+ throw new CustomError(400, error.message);
2848
+ }
2849
+
2850
+ const state = await this.getOrgState(teamDid);
2851
+ const blocklet = await getBlocklet({ did: teamDid, states: this.states, dataDirs: this.dataDirs });
2852
+ const orgCount = await state.getOrgCountByUser(context.user.did);
2853
+
2854
+ const { veriftMaxOrgPerUser } = createOrgValidators(blocklet);
2855
+
2856
+ veriftMaxOrgPerUser(orgCount); // 验证用户创建的 org 数量是否超过最大限制, 内部已经验证 org 是否开启
2857
+
2858
+ const result = await state.create({ ...sanitizedOrg }, context);
2859
+
2860
+ // 创建 org 的 owner passport, 并赋值给 owner
2861
+ const roleName = md5(`${result.id}-owner`); // 避免 name 重复
2862
+ await this.createRole({ teamDid, name: roleName, title: result.name, description: 'Owner', orgId: result.id });
2863
+ await this.issuePassportToUser({
2864
+ teamDid,
2865
+ userDid: result.ownerDid,
2866
+ role: roleName,
2867
+ notification: {},
2868
+ });
2869
+
2870
+ return result;
2871
+ } catch (err) {
2872
+ logger.error('Failed to create org', { err, teamDid });
2873
+ throw err;
2874
+ }
2875
+ }
2876
+
2877
+ async updateOrg({ teamDid, org }, context) {
2878
+ try {
2879
+ const sanitizedOrg = {
2880
+ ...org,
2881
+ description: sanitizeTag(org.description),
2882
+ name: sanitizeTag(org.name),
2883
+ };
2884
+
2885
+ const { error } = updateOrgInputSchema.validate(sanitizedOrg);
2886
+ if (error) {
2887
+ throw new CustomError(400, error.message);
2888
+ }
2889
+
2890
+ const state = await this.getOrgState(teamDid);
2891
+ return state.updateOrg({ org: sanitizedOrg }, context);
2892
+ } catch (err) {
2893
+ logger.error('Failed to update org', { err, teamDid });
2894
+ throw err;
2895
+ }
2896
+ }
2897
+
2898
+ async deleteOrg({ teamDid, id }, context) {
2899
+ try {
2900
+ const state = await this.getOrgState(teamDid);
2901
+ // 要同时删除与 org 相关的 passport 和 邀请链接
2902
+ const roles = await this.getRoles({ teamDid, orgId: id });
2903
+ const result = await state.deleteOrg({ id }, context);
2904
+ try {
2905
+ await state.removeOrgRelatedData({ roles, orgId: id });
2906
+ } catch (err) {
2907
+ logger.error('Failed to remove org related data', { err, teamDid, roles, id });
2908
+ }
2909
+ return result;
2910
+ } catch (err) {
2911
+ logger.error('Failed to delete org', { err, teamDid, id });
2912
+ throw err;
2913
+ }
2914
+ }
2915
+
2916
+ async addOrgMember({ teamDid, orgId, userDid }, context) {
2917
+ try {
2918
+ const state = await this.getOrgState(teamDid);
2919
+ return state.addOrgMember({ orgId, userDid }, context);
2920
+ } catch (err) {
2921
+ logger.error('Add member to org failed', { err, teamDid, orgId, userDid });
2922
+ throw err;
2923
+ }
2924
+ }
2925
+
2926
+ async updateOrgMember({ teamDid, orgId, userDid, status }, context) {
2927
+ try {
2928
+ const state = await this.getOrgState(teamDid);
2929
+ return state.updateOrgMember({ orgId, userDid, status }, context);
2930
+ } catch (err) {
2931
+ logger.error('Update member in org failed', { err, teamDid, orgId, userDid, status });
2932
+ throw err;
2933
+ }
2934
+ }
2935
+
2936
+ /**
2937
+ * 发送邀请通知
2938
+ * @param {*} param0
2939
+ */
2940
+ async sendInvitationNotification({
2941
+ teamDid,
2942
+ invitor,
2943
+ org,
2944
+ role,
2945
+ successUserDids,
2946
+ inviteLink,
2947
+ email,
2948
+ inviteType,
2949
+ blocklet,
2950
+ }) {
2951
+ try {
2952
+ const userInfo = await this.getUser({
2953
+ teamDid,
2954
+ user: { did: invitor.did },
2955
+ options: { enableConnectedAccount: true },
2956
+ });
2957
+
2958
+ // 检测是否开启了 email 服务
2959
+ const provider = getEmailServiceProvider(blocklet);
2960
+
2961
+ const translate = {
2962
+ en: {
2963
+ title: 'Inviting you to join the organization',
2964
+ 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.`,
2965
+ accept: 'Accept',
2966
+ },
2967
+ zh: {
2968
+ title: '邀请您加入组织',
2969
+ description: `<${userInfo.fullName}(did:abt:${userInfo.did})> 邀请您加入 ${org.name} 组织,角色为 ${role}。<br/>接受后,你将能够访问该组织的资源和协作内容。<br/><br/>请点击下方按钮处理邀请。<br/><br/>如果你不想加入,可以忽略此通知。`,
2970
+ accept: '接受',
2971
+ },
2972
+ };
2973
+ const content = translate[userInfo.locale || 'en'] || translate.en;
2974
+
2975
+ const message = {
2976
+ title: content.title,
2977
+ body: content.description,
2978
+ actions: [
2979
+ {
2980
+ name: content.accept,
2981
+ title: content.accept,
2982
+ link: inviteLink,
2983
+ },
2984
+ ],
2985
+ };
2986
+
2987
+ if (inviteType === 'internal') {
2988
+ await this.createNotification({
2989
+ teamDid,
2990
+ receiver: successUserDids,
2991
+ entityId: teamDid,
2992
+ source: 'system',
2993
+ severity: 'info',
2994
+ ...message,
2995
+ });
2996
+ logger.info('Invite notification sent successfully', {
2997
+ teamDid,
2998
+ orgId: org.id,
2999
+ sentToUsers: successUserDids,
3000
+ sentCount: successUserDids.length,
3001
+ });
3002
+ } else if (inviteType === 'external' && provider && email) {
3003
+ // 当 service 开启 email 服务时才会发送邮件通知
3004
+ const emailInputSchema = Joi.string().email().required();
3005
+ const { error } = emailInputSchema.validate(email);
3006
+ if (error) {
3007
+ throw new CustomError(400, error.message);
3008
+ }
3009
+ const nodeInfo = await this.node.read();
3010
+ const blockletInfo = getBlockletInfo(blocklet, nodeInfo.sk);
3011
+ const sender = {
3012
+ appDid: blockletInfo.wallet.address,
3013
+ appSk: blockletInfo.wallet.secretKey,
3014
+ };
3015
+
3016
+ await sendToUser(email, message, sender, undefined, 'send-to-mail');
3017
+ logger.info('Send invitation notification to email completed', {
3018
+ teamDid,
3019
+ orgId: org.id,
3020
+ email,
3021
+ });
3022
+ }
3023
+ } catch (notificationErr) {
3024
+ // 通知发送失败不影响邀请的成功,只记录警告
3025
+ logger.warn('Failed to send invitation notification, but invitations were created successfully', {
3026
+ notificationErr,
3027
+ teamDid,
3028
+ orgId: org.id,
3029
+ successUserDids,
3030
+ });
3031
+ }
3032
+ }
3033
+
3034
+ /**
3035
+ * 邀请成员到组织
3036
+ * 1. 批量添加成员到组织 - 记录添加成功和添加失败的用户 DID
3037
+ * 2. 创建邀请链接,只创建添加成功的用户邀请链接
3038
+ * 3. 发送邀请通知,只发送添加成功的用户邀请通知
3039
+ * @param {*} param0
3040
+ * @param {*} param0.inviteType 邀请类型,internal 内部邀请,external 外部邀请
3041
+ * @param {*} context
3042
+ * @returns 返回邀请成功的用户和失败的用户 did 列表
3043
+ */
3044
+ async inviteMembersToOrg({ teamDid, orgId, userDids, role, inviteType = 'internal', email }, context) {
3045
+ try {
3046
+ const state = await this.getOrgState(teamDid);
3047
+ const { user } = context || {};
3048
+ if (!user) {
3049
+ throw new CustomError(400, 'User is required');
3050
+ }
3051
+ const org = await this.getOrg({ teamDid, id: orgId }, context);
3052
+ if (!org) {
3053
+ throw new CustomError(400, 'Org not found');
3054
+ }
3055
+
3056
+ // dashboard 或者非 org owner 无法邀请成员
3057
+ if (isAdmingPath(context) || !isOrgOwner(user, org)) {
3058
+ throw new CustomError(403, "You cannot invite members to other users' org");
3059
+ }
3060
+
3061
+ if (inviteType === 'internal' && userDids.length === 0) {
3062
+ throw new CustomError(400, 'You must invite at least one user');
3063
+ }
3064
+
3065
+ // Step 1: 批量添加成员到组织 - 记录添加成功和添加失败的用户 DID
3066
+ const successUserDids = [];
3067
+ const failedUserDids = [];
3068
+
3069
+ // 内部邀请
3070
+ if (inviteType === 'internal') {
3071
+ for (const userDid of userDids) {
3072
+ try {
3073
+ // eslint-disable-next-line no-await-in-loop
3074
+ await state.addOrgMember({ orgId, userDid, status: 'inviting' }, context);
3075
+ successUserDids.push(userDid);
3076
+ } catch (addErr) {
3077
+ failedUserDids.push(userDid);
3078
+ logger.warn('Failed to add user to org', { userDid, orgId, error: addErr.message });
3079
+ }
3080
+ }
3081
+
3082
+ logger.info('Batch add members to org completed', {
3083
+ teamDid,
3084
+ orgId,
3085
+ totalUsers: userDids.length,
3086
+ successCount: successUserDids.length,
3087
+ failedCount: failedUserDids.length,
3088
+ });
3089
+
3090
+ // 如果没有成功添加的用户,直接返回结果
3091
+ if (successUserDids.length === 0) {
3092
+ logger.warn('No users were successfully added to org', { teamDid, orgId, failedUserDids });
3093
+ throw new CustomError(500, 'No users were successfully added to org');
3094
+ }
3095
+ }
3096
+
3097
+ // Step 2: 创建邀请链接,只创建添加成功的用户邀请链接
3098
+ const inviteInfo = await this.createMemberInvitation(
3099
+ {
3100
+ teamDid,
3101
+ role,
3102
+ expireTime: this.memberInvitationExpireTime,
3103
+ orgId,
3104
+ inviteUserDids: inviteType === 'internal' ? successUserDids : [],
3105
+ },
3106
+ context
3107
+ );
3108
+
3109
+ const blocklet = await getBlocklet({ did: teamDid, states: this.states, dataDirs: this.dataDirs });
3110
+ const inviteLink = getOrgInviteLink(inviteInfo, blocklet);
3111
+
3112
+ logger.info('Invite link created for successful users', {
3113
+ teamDid,
3114
+ orgId,
3115
+ inviteId: inviteInfo.inviteId,
3116
+ userCount: successUserDids.length,
3117
+ });
3118
+
3119
+ // Step 3: 发送邀请通知,只发送添加成功的用户邀请通知
3120
+
3121
+ this.sendInvitationNotification({
3122
+ teamDid,
3123
+ invitor: user,
3124
+ org,
3125
+ role,
3126
+ successUserDids,
3127
+ email,
3128
+ inviteType,
3129
+ inviteLink,
3130
+ blocklet,
3131
+ });
3132
+
3133
+ // 返回成功和失败的用户列表
3134
+ return {
3135
+ successDids: successUserDids,
3136
+ failedDids: failedUserDids,
3137
+ inviteLink,
3138
+ };
3139
+ } catch (err) {
3140
+ logger.error('Invite users to org failed', { err, teamDid, orgId, userDids, role });
3141
+ throw err;
3142
+ }
3143
+ }
3144
+
3145
+ async removeOrgMember({ teamDid, orgId, userDid }, context) {
3146
+ try {
3147
+ const state = await this.getOrgState(teamDid);
3148
+ const roles = await this.getRoles({ teamDid, orgId });
3149
+ const result = await state.removeOrgMember({ orgId, userDid }, context);
3150
+ try {
3151
+ await state.removeOrgRelatedData({ roles, orgId, userDid });
3152
+ } catch (err) {
3153
+ logger.error('Failed to remove user related passports', { err, teamDid, roles, userDid });
3154
+ }
3155
+ return result;
3156
+ } catch (err) {
3157
+ logger.error('Remove member from org failed', { err, teamDid, orgId, userDid });
3158
+ throw err;
3159
+ }
3160
+ }
3161
+
3162
+ async getOrgMembers({ teamDid, orgId, paging }, context) {
3163
+ try {
3164
+ const state = await this.getOrgState(teamDid);
3165
+ const result = await state.getOrgMembers({ orgId, paging, options: { includePassport: true } }, context);
3166
+ const info = await this.node.read();
3167
+ const isServer = this.teamManager.isNodeTeam(teamDid);
3168
+ const blocklet = await getBlocklet({ did: teamDid, states: this.states, dataDirs: this.dataDirs });
3169
+ const baseUrl = blocklet.environmentObj.BLOCKLET_APP_URL;
3170
+ const roles = await this.getRoles({ teamDid, orgId });
3171
+ result.users.forEach((item) => {
3172
+ if (item?.user?.avatar) {
3173
+ item.user.avatar = getUserAvatarUrl(baseUrl, item.user.avatar, info, isServer);
3174
+ }
3175
+ if (item?.user?.passports?.length > 0) {
3176
+ item.user.passports = item.user.passports.filter((passport) =>
3177
+ roles.some((role) => role.name === passport.name)
3178
+ );
3179
+ }
3180
+ });
3181
+
3182
+ return result;
3183
+ } catch (err) {
3184
+ logger.error('Get org members failed', { err, teamDid, orgId });
3185
+ throw err;
3186
+ }
3187
+ }
3188
+
3189
+ async getOrgInvitableUsers({ teamDid, id, query, paging }, context) {
3190
+ try {
3191
+ const state = await this.getOrgState(teamDid);
3192
+ return state.getOrgInvitableUsers({ id, query, paging }, context);
3193
+ } catch (err) {
3194
+ logger.error('Get org invitable users failed', { err, teamDid, id });
3195
+ throw err;
3196
+ }
3197
+ }
3198
+
3199
+ async getOrgResource({ teamDid, orgId, resourceId }, context) {
3200
+ try {
3201
+ const state = await this.getOrgState(teamDid);
3202
+ return state.getOrgResource({ orgId, resourceId }, context);
3203
+ } catch (err) {
3204
+ logger.error('Get org resource failed', { err, teamDid, orgId, resourceId });
3205
+ throw err;
3206
+ }
3207
+ }
3208
+
3209
+ async addOrgResource({ teamDid, orgId, resourceIds, type, metadata }, context) {
3210
+ try {
3211
+ const state = await this.getOrgState(teamDid);
3212
+ return state.addOrgResource({ orgId, resourceIds, type, metadata }, context);
3213
+ } catch (err) {
3214
+ logger.error('Add org resource failed', { err, teamDid, orgId, resourceIds, type, metadata });
3215
+ throw err;
3216
+ }
3217
+ }
3218
+
3219
+ async removeOrgResource({ teamDid, orgId, resourceIds }, context) {
3220
+ try {
3221
+ const state = await this.getOrgState(teamDid);
3222
+ return state.removeOrgResource({ orgId, resourceIds }, context);
3223
+ } catch (err) {
3224
+ logger.error('Remove org resource failed', { err, teamDid, orgId, resourceIds });
3225
+ throw err;
3226
+ }
3227
+ }
3228
+
3229
+ async migrateOrgResource({ teamDid, from, to, resourceIds }, context) {
3230
+ try {
3231
+ const state = await this.getOrgState(teamDid);
3232
+ return state.migrateOrgResource({ from, to, resourceIds }, context);
3233
+ } catch (err) {
3234
+ logger.error('Migrate org resource failed', { err, teamDid, from, to, resourceIds });
3235
+ throw err;
3236
+ }
3237
+ }
2706
3238
  }
2707
3239
 
2708
3240
  module.exports = TeamAPI;