@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 +579 -47
- package/lib/blocklet/downloader/blocklet-downloader.js +2 -2
- package/lib/blocklet/downloader/bundle-downloader.js +13 -38
- package/lib/blocklet/manager/disk.js +200 -84
- package/lib/blocklet/manager/ensure-blocklet-running.js +3 -2
- package/lib/blocklet/manager/helper/blue-green-get-componentids.js +59 -0
- package/lib/blocklet/manager/helper/blue-green-start-blocklet.js +292 -0
- package/lib/blocklet/manager/helper/blue-green-update-blocklet-status.js +18 -0
- package/lib/blocklet/manager/helper/blue-green-upgrade-blocklet.js +191 -0
- package/lib/blocklet/manager/helper/upgrade-components.js +2 -9
- package/lib/blocklet/migration-dist/migration.cjs +459 -457
- package/lib/blocklet/passport/index.js +8 -2
- package/lib/index.js +18 -0
- package/lib/monitor/blocklet-runtime-monitor.js +12 -7
- package/lib/states/audit-log.js +54 -2
- package/lib/states/blocklet.js +23 -8
- package/lib/states/index.js +3 -0
- package/lib/states/org.js +661 -0
- package/lib/team/manager.js +10 -3
- package/lib/util/blocklet.js +190 -115
- package/lib/util/docker/is-docker-only-single-instances.js +17 -0
- package/lib/util/org.js +99 -0
- package/lib/validators/org.js +19 -0
- package/package.json +26 -26
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: [
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
}
|
|
1359
|
-
}
|
|
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;
|