@abtnode/blocklet-services 1.16.0-beta-1d6c582e → 1.16.0-beta-62b42401

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/api/index.js CHANGED
@@ -8,7 +8,7 @@ const compression = require('compression');
8
8
  const cookieParser = require('cookie-parser');
9
9
  const bodyParser = require('body-parser');
10
10
  const httpProxy = require('http-proxy');
11
- const moment = require('dayjs');
11
+ const dayjs = require('dayjs');
12
12
  const minimatch = require('minimatch');
13
13
 
14
14
  const { getAccessLogStream } = require('@abtnode/logger');
@@ -33,6 +33,7 @@ const { init: initDashboard } = require('./services/dashboard');
33
33
  const StaticService = require('./services/static');
34
34
  const StudioService = require('./services/studio');
35
35
  const createEnvRoutes = require('./routes/env');
36
+ const createOAuthRoutes = require('./routes/oauth');
36
37
  const createBlockletRoutes = require('./routes/blocklet');
37
38
  const createConnectRelayRoutes = require('./routes/connect/relay');
38
39
  const createConnectSessionRoutes = require('./routes/connect/session');
@@ -48,7 +49,7 @@ const logFileGenerator = (time, index) => {
48
49
  return 'service.log';
49
50
  }
50
51
 
51
- let filename = `service-${moment(time).subtract(1, 'day').format('YYYY-MM-DD')}`; // prev date
52
+ let filename = `service-${dayjs(time).subtract(1, 'day').format('YYYY-MM-DD')}`; // prev date
52
53
 
53
54
  if (index > 1) {
54
55
  filename = `${filename}-${index}`;
@@ -220,6 +221,7 @@ module.exports = function createServer(node, serverOptions = {}) {
220
221
  server.use(authMiddlewares.userInfo);
221
222
 
222
223
  // API: auth
224
+ createOAuthRoutes.init(server, node, options);
223
225
  createEnvRoutes.init(server, node, options);
224
226
  createBlockletRoutes.init(server, node);
225
227
  createConnectSessionRoutes.init(server, node, options);
@@ -0,0 +1,16 @@
1
+ const { AuthenticationClient: Auth0AuthenticationClient } = require('auth0');
2
+
3
+ class AuthenticationClient {
4
+ constructor(options) {
5
+ this.instance = new Auth0AuthenticationClient({
6
+ domain: options.domain,
7
+ clientId: options.clientId,
8
+ });
9
+ }
10
+
11
+ getProfile(...args) {
12
+ return this.instance.getProfile(...args);
13
+ }
14
+ }
15
+
16
+ module.exports = AuthenticationClient;
@@ -0,0 +1,7 @@
1
+ const AuthenticationClient = require('./authentication-client');
2
+ const ManagementClient = require('./management-client');
3
+
4
+ module.exports = {
5
+ AuthenticationClient,
6
+ ManagementClient,
7
+ };
@@ -0,0 +1,36 @@
1
+ const { ManagementClient: Auth0ManagementClient } = require('auth0');
2
+
3
+ class ManagementClient {
4
+ constructor(options) {
5
+ this.config = {
6
+ domain: options.domain,
7
+ token: options.token,
8
+ };
9
+ this.instance = new Auth0ManagementClient({
10
+ domain: options.domain,
11
+ token: options.token,
12
+ });
13
+ }
14
+
15
+ async getClient(...args) {
16
+ return this.instance.getClient(...args);
17
+ }
18
+
19
+ getClients(...args) {
20
+ return this.instance.getClients(...args);
21
+ }
22
+
23
+ createClient(...args) {
24
+ return this.instance.createClient(...args);
25
+ }
26
+
27
+ updateClient(...args) {
28
+ return this.instance.updateClient(...args);
29
+ }
30
+
31
+ deleteClient(...args) {
32
+ return this.instance.deleteClient(...args);
33
+ }
34
+ }
35
+
36
+ module.exports = ManagementClient;
@@ -0,0 +1,90 @@
1
+ const md5 = require('md5');
2
+ const axios = require('axios');
3
+ const logger = require('@abtnode/auth/lib/logger');
4
+ const { getPassportStatusEndpoint, getApplicationInfo } = require('@abtnode/auth/lib/auth');
5
+ const { createPassportVC } = require('@abtnode/auth/lib/passport');
6
+ const { VC_TYPE_NODE_PASSPORT } = require('@abtnode/constant');
7
+ const { parseUserAvatar } = require('@abtnode/util/lib/user-avatar');
8
+ const pick = require('lodash/pick');
9
+
10
+ const { sendToUser } = require('../notification');
11
+
12
+ function getEmailHash(email = '') {
13
+ const cleanEmail = email.trim().toLowerCase();
14
+ return md5(cleanEmail);
15
+ }
16
+
17
+ async function getAvatarByEmail(email = '') {
18
+ try {
19
+ const emailHash = getEmailHash(email);
20
+ const gravatarUrl = `https://www.gravatar.com/avatar/${emailHash}`;
21
+ const { data } = await axios.get(gravatarUrl, {
22
+ responseType: 'arraybuffer',
23
+ });
24
+ const base64Content = Buffer.from(data, 'binary').toString('base64');
25
+
26
+ return `data:image/png;base64,${base64Content}`;
27
+ } catch {
28
+ logger.error(`Fetch gravatar failed: ${email}`);
29
+ return null;
30
+ }
31
+ }
32
+
33
+ async function transferPassport(fromUser, toUser, { req, teamDid, node, nodeInfo }) {
34
+ const {
35
+ name: issuerName,
36
+ wallet: issuerWallet,
37
+ passportColor,
38
+ dataDir,
39
+ } = await getApplicationInfo({ node, nodeInfo, teamDid });
40
+
41
+ const waitPassportList = fromUser.passports || [];
42
+ const attachments = await Promise.all(
43
+ waitPassportList.map(async (item) => {
44
+ const avatar = await parseUserAvatar(toUser.avatar, { dataDir });
45
+ const vcParams = {
46
+ issuerName,
47
+ issuerWallet,
48
+ ownerDid: toUser.did,
49
+ passport: pick(item, ['name', 'title', 'endpoint', 'specVersion']),
50
+ endpoint: getPassportStatusEndpoint({
51
+ baseUrl: item.endpoint,
52
+ userDid: toUser.did,
53
+ teamDid,
54
+ }),
55
+ types: teamDid === nodeInfo.did ? [VC_TYPE_NODE_PASSPORT] : [],
56
+ ownerProfile: {
57
+ email: toUser.email,
58
+ fullName: toUser.fullName,
59
+ avatar,
60
+ },
61
+ preferredColor: passportColor,
62
+ };
63
+
64
+ const vc = createPassportVC(vcParams);
65
+ return {
66
+ type: 'vc',
67
+ data: {
68
+ credential: vc,
69
+ tag: item.name,
70
+ },
71
+ };
72
+ })
73
+ );
74
+
75
+ await sendToUser(
76
+ toUser.did,
77
+ {
78
+ title: 'Transfer passports',
79
+ body: `Transfer ${fromUser.did}'s passports to ${toUser.did}`,
80
+ attachments,
81
+ },
82
+ { req }
83
+ );
84
+ }
85
+
86
+ module.exports = {
87
+ getEmailHash,
88
+ getAvatarByEmail,
89
+ transferPassport,
90
+ };
@@ -9,6 +9,7 @@ const {
9
9
  getVCFromClaims,
10
10
  validatePassportStatus,
11
11
  getPassportStatusEndpoint,
12
+ getApplicationInfo,
12
13
  } = require('@abtnode/auth/lib/auth');
13
14
  const {
14
15
  NODE_SERVICES,
@@ -18,6 +19,7 @@ const {
18
19
  VC_TYPE_NODE_PASSPORT,
19
20
  WHO_CAN_ACCESS,
20
21
  WHO_CAN_ACCESS_PREFIX_ROLES,
22
+ USER_TYPE,
21
23
  } = require('@abtnode/constant');
22
24
  const {
23
25
  validatePassport,
@@ -30,33 +32,17 @@ const {
30
32
  upsertToPassports,
31
33
  } = require('@abtnode/auth/lib/passport');
32
34
  const { getKeyPairClaim } = require('@abtnode/auth/lib/server');
35
+ const sortBy = require('lodash/sortBy');
36
+ const head = require('lodash/head');
33
37
 
34
38
  const { getRolesFromAuthConfig, getBlockletAppIdList } = require('@blocklet/meta/lib/util');
35
39
 
36
40
  const logger = require('@abtnode/logger')(require('../../../package.json').name);
37
41
 
38
- const vcTypes = [VC_TYPE_GENERAL_PASSPORT, VC_TYPE_NODE_PASSPORT];
39
-
40
- /**
41
- * @returns {Array} config
42
- * @returns {Boolean} config[0] is invited user only
43
- * @returns {String} config[1] default role
44
- * @returns {Boolean} config[2] issue passport
45
- */
46
- const isInvitedUserOnly = async (config, node, teamDid) => {
47
- const count = await node.getUsersCount({ teamDid });
48
-
49
- // issue owner passport for first login user
50
- if (count === 0) {
51
- return [false, ROLES.OWNER, true];
52
- }
42
+ const { isInvitedUserOnly } = require('../../util');
43
+ const { transferPassport } = require('../auth/utils');
53
44
 
54
- if (config.whoCanAccess && config.whoCanAccess !== WHO_CAN_ACCESS.ALL) {
55
- return [true];
56
- }
57
-
58
- return [false, ROLES.GUEST];
59
- };
45
+ const vcTypes = [VC_TYPE_GENERAL_PASSPORT, VC_TYPE_NODE_PASSPORT];
60
46
 
61
47
  const getPassportVc = async ({ blocklet, claims, challenge, locale }) => {
62
48
  const trustedPassports = (blocklet.trustedPassports || []).map((x) => x.issuerDid);
@@ -256,7 +242,10 @@ module.exports = {
256
242
  did: userDid,
257
243
  pk: userPk,
258
244
  locale,
259
- passports: upsertToPassports(user.passports || [], passport).filter(Boolean),
245
+ passports: upsertToPassports(
246
+ user.passports || [],
247
+ passport && { ...passport, lastLoginAt: new Date().toISOString() }
248
+ ).filter(Boolean),
260
249
  lastLoginAt: new Date().toISOString(),
261
250
  lastLoginIp: get(request, 'headers[x-real-ip]') || '',
262
251
  },
@@ -490,6 +479,17 @@ module.exports = {
490
479
  // Recreate passport with correct role
491
480
  passport = vc ? createUserPassport(vc, { role }) : null;
492
481
 
482
+ await node.updateUser({
483
+ teamDid,
484
+ user: {
485
+ did: user.did,
486
+ passports: upsertToPassports(
487
+ user.passports || [],
488
+ passport && { ...passport, lastLoginAt: new Date().toISOString() }
489
+ ),
490
+ },
491
+ });
492
+
493
493
  // Audit log
494
494
  const passportForLog = passport || { name: 'Guest', role: 'guest' };
495
495
  await node.createAuditLog(
@@ -508,6 +508,181 @@ module.exports = {
508
508
  },
509
509
  },
510
510
 
511
+ // 基本流程与 login 一致,但在创建更新用户信息的逻辑不一样
512
+ bindWallet: {
513
+ onConnect: async ({ node, request, userDid, locale, passportId = '', componentId, previousUserDid }) => {
514
+ const translations = {
515
+ en: {
516
+ notFound: "Can't get bind account infomation",
517
+ notDerivedAccount: "Current account is not a derived account, can't bind a DID Wallet account",
518
+ notWalletAccount: "Bind account is a derived account, can't bind",
519
+ alreadyBind: 'Bind account already bind with another account',
520
+ },
521
+ zh: {
522
+ notFound: '获取绑定账户信息失败',
523
+ notDerivedAccount: '当前账户不是一个派生账号,无法绑定 DID Wallet 账户',
524
+ notWalletAccount: '绑定的账户是一个派生账号,无法绑定',
525
+ alreadyBind: '该账户已绑定 OAuth 账户',
526
+ },
527
+ };
528
+ const blocklet = await request.getBlocklet();
529
+ const config = await request.getServiceConfig(NODE_SERVICES.AUTH, { componentId });
530
+ const { did: teamDid } = await request.getBlockletInfo();
531
+ const oauthUser = await getUser(node, teamDid, previousUserDid);
532
+ const bindUser = await node.getUser({ teamDid: blocklet.meta.did, user: { did: userDid } });
533
+
534
+ if (!oauthUser) {
535
+ throw new Error(translations[locale].notFound);
536
+ }
537
+ if (oauthUser.source !== USER_TYPE.DERIVED) {
538
+ throw new Error(translations[locale].notDerivedAccount);
539
+ }
540
+ if (bindUser) {
541
+ if (bindUser.source === USER_TYPE.DERIVED) {
542
+ throw new Error(translations[locale].notWalletAccount);
543
+ }
544
+ if (bindUser.derivedAccount) {
545
+ throw new Error(translations[locale].alreadyBind);
546
+ }
547
+ }
548
+
549
+ const profileFields = get(config, 'profileFields');
550
+ const claims = {
551
+ profile: {
552
+ type: 'profile',
553
+ description: messages.description[locale],
554
+ items: profileFields || ['fullName', 'avatar'],
555
+ },
556
+ };
557
+
558
+ // 至少需要一个 claim
559
+ // if (oauthUser.avatar) {
560
+ // delete claims.profile;
561
+ // }
562
+
563
+ if (passportId) {
564
+ claims.verifiableCredential.target = passportId;
565
+ }
566
+
567
+ return claims;
568
+ },
569
+ onApprove: async ({ node, request, locale, userDid, userPk, claims, createSessionToken, previousUserDid }) => {
570
+ const { did: teamDid } = await request.getBlockletInfo();
571
+
572
+ const oauthUser = await getUser(node, teamDid, previousUserDid);
573
+ const nodeInfo = await request.getNodeInfo();
574
+ // Check user approved
575
+ let bindUser = await getUser(node, teamDid, userDid);
576
+ if (bindUser && !bindUser.approved) {
577
+ throw new Error(messages.notAllowed[locale]);
578
+ }
579
+
580
+ const { dataDir } = await getApplicationInfo({ node, nodeInfo, teamDid });
581
+
582
+ const profileold = claims.find((x) => x.type === 'profile') || { avatar: null };
583
+ const avatar = await extractUserAvatar(oauthUser.avatar || profileold.avatar, { dataDir });
584
+ const profile = {
585
+ fullName: oauthUser.fullName,
586
+ avatar,
587
+ email: oauthUser.email,
588
+ };
589
+
590
+ // TODO: 获取当前登录使用的 passport(无法获取到 passport.id)
591
+ // 使用最近一次使用的 passport 来代替
592
+ const validPassports = oauthUser.passports.filter((item) => item.status === 'valid');
593
+ const lastUsedPassport = head(sortBy(validPassports, 'lastLoginAt'));
594
+ const passport = lastUsedPassport || { name: 'Guest', role: 'guest' };
595
+ if (bindUser) {
596
+ const mergePassport = (oauthUser.passports || []).reduce((sum, cur) => {
597
+ return upsertToPassports(sum, cur);
598
+ }, bindUser.passports || []);
599
+ // Update bindUser account
600
+ await node.updateUser({
601
+ teamDid,
602
+ user: {
603
+ derivedAccount: {
604
+ provider: oauthUser.sourceProvider,
605
+ did: oauthUser.did,
606
+ pk: oauthUser.pk,
607
+ },
608
+ connectedAccounts: [
609
+ {
610
+ provider: oauthUser.sourceProvider,
611
+ id: oauthUser.sourceId,
612
+ lastLoginAt: oauthUser.lastLoginAt,
613
+ },
614
+ ],
615
+ did: userDid,
616
+ pk: userPk,
617
+ locale,
618
+ passports: mergePassport,
619
+ lastLoginAt: new Date().toISOString(),
620
+ lastLoginIp: get(request, 'headers[x-real-ip]') || '',
621
+ },
622
+ });
623
+ // FIXME: @zhanghan 将来是否需要移除 derived account
624
+ // await node.removeUser({
625
+ // teamDid,
626
+ // user: {
627
+ // did: previousUserDid,
628
+ // },
629
+ // });
630
+ // TODO: @zhanghan 需要增加 auditLog
631
+ } else {
632
+ const doc = await node.addUser({
633
+ teamDid,
634
+ user: {
635
+ ...profile,
636
+ source: USER_TYPE.WALLET,
637
+ derivedAccount: {
638
+ provider: oauthUser.sourceProvider,
639
+ did: oauthUser.did,
640
+ pk: oauthUser.pk,
641
+ },
642
+ connectedAccounts: [
643
+ {
644
+ provider: oauthUser.sourceProvider,
645
+ id: oauthUser.sourceId,
646
+ lastLoginAt: oauthUser.lastLoginAt,
647
+ },
648
+ ],
649
+ avatar,
650
+ did: userDid,
651
+ pk: userPk,
652
+ approved: true,
653
+ locale,
654
+ passports: oauthUser.passports,
655
+ firstLoginAt: new Date().toISOString(),
656
+ lastLoginAt: new Date().toISOString(),
657
+ lastLoginIp: get(request, 'headers[x-real-ip]') || '',
658
+ },
659
+ });
660
+ bindUser = doc;
661
+ // remove derived account (updateUser use did as key, so did can't be update)
662
+ await node.removeUser({
663
+ teamDid,
664
+ user: {
665
+ did: previousUserDid,
666
+ },
667
+ });
668
+ // TODO: @zhanghan 需要增加 auditLog
669
+ }
670
+
671
+ await transferPassport(oauthUser, bindUser, { req: request, node, nodeInfo, teamDid });
672
+
673
+ // Generate new session token that client can save to localStorage
674
+ const sessionToken = await createSessionToken(userDid, { passport, role: passport.role });
675
+ logger.info('login.success', { userDid, role: passport.role });
676
+
677
+ return {
678
+ sessionToken,
679
+ nextWorkflowData: {
680
+ userDid,
681
+ },
682
+ };
683
+ },
684
+ },
685
+
511
686
  migrateToStructV2: {
512
687
  getClaims: ({ node }) => ({
513
688
  verifiableCredential: async ({ extraParams: { locale }, context: { request } }) => {
@@ -0,0 +1,19 @@
1
+ const Notification = require('@blocklet/sdk/lib/util/send-notification');
2
+
3
+ async function sendToUser(userDid, notification, { req }) {
4
+ const { wallet } = await req.getBlockletInfo();
5
+ const notificationRes = await Notification.sendToUser(
6
+ userDid,
7
+ notification,
8
+ {
9
+ appDid: wallet.address,
10
+ appSk: wallet.secretKey,
11
+ },
12
+ process.env.ABT_NODE_SERVICE_PORT
13
+ );
14
+ return notificationRes;
15
+ }
16
+
17
+ module.exports = {
18
+ sendToUser,
19
+ };