@abtnode/blocklet-services 1.6.23
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/LICENSE +13 -0
- package/README.md +84 -0
- package/build/asset-manifest.json +36 -0
- package/build/favicon.ico +0 -0
- package/build/images/blocklet.png +0 -0
- package/build/index.html +1 -0
- package/build/manifest.json +15 -0
- package/build/precache-manifest.b61da416864625d7da6840b710c5fd05.js +150 -0
- package/build/service-worker.js +39 -0
- package/build/static/css/2.73d52aac.chunk.css +2 -0
- package/build/static/css/2.73d52aac.chunk.css.map +1 -0
- package/build/static/js/2.adca7e30.chunk.js +3 -0
- package/build/static/js/2.adca7e30.chunk.js.LICENSE.txt +113 -0
- package/build/static/js/2.adca7e30.chunk.js.map +1 -0
- package/build/static/js/3.9cf8cde1.chunk.js +3 -0
- package/build/static/js/3.9cf8cde1.chunk.js.LICENSE.txt +19 -0
- package/build/static/js/3.9cf8cde1.chunk.js.map +1 -0
- package/build/static/js/4.3bcd2384.chunk.js +2 -0
- package/build/static/js/4.3bcd2384.chunk.js.map +1 -0
- package/build/static/js/5.a0b39d30.chunk.js +2 -0
- package/build/static/js/5.a0b39d30.chunk.js.map +1 -0
- package/build/static/js/6.91e31d64.chunk.js +2 -0
- package/build/static/js/6.91e31d64.chunk.js.map +1 -0
- package/build/static/js/7.aa6a9bc1.chunk.js +2 -0
- package/build/static/js/7.aa6a9bc1.chunk.js.map +1 -0
- package/build/static/js/main.0fef8e40.chunk.js +2 -0
- package/build/static/js/main.0fef8e40.chunk.js.map +1 -0
- package/build/static/js/runtime-main.8d9f8ef7.js +2 -0
- package/build/static/js/runtime-main.8d9f8ef7.js.map +1 -0
- package/build/static/media/rubik-all-400-normal.c2324103.woff +0 -0
- package/build/static/media/rubik-all-500-normal.000c3408.woff +0 -0
- package/build/static/media/rubik-all-600-normal.30df2fd0.woff +0 -0
- package/build/static/media/rubik-cyrillic-400-normal.aa383bbd.woff2 +0 -0
- package/build/static/media/rubik-cyrillic-500-normal.27a1ebd4.woff2 +0 -0
- package/build/static/media/rubik-cyrillic-600-normal.74d5cdba.woff2 +0 -0
- package/build/static/media/rubik-cyrillic-ext-400-normal.b8647475.woff2 +0 -0
- package/build/static/media/rubik-cyrillic-ext-500-normal.860932d9.woff2 +0 -0
- package/build/static/media/rubik-cyrillic-ext-600-normal.942f240f.woff2 +0 -0
- package/build/static/media/rubik-hebrew-400-normal.2c9e3c2a.woff2 +0 -0
- package/build/static/media/rubik-hebrew-500-normal.08d6e502.woff2 +0 -0
- package/build/static/media/rubik-hebrew-600-normal.bfa32e44.woff2 +0 -0
- package/build/static/media/rubik-latin-400-normal.b8fd53c5.woff2 +0 -0
- package/build/static/media/rubik-latin-500-normal.595f1a98.woff2 +0 -0
- package/build/static/media/rubik-latin-600-normal.5f06934f.woff2 +0 -0
- package/build/static/media/rubik-latin-ext-400-normal.3c5c378e.woff2 +0 -0
- package/build/static/media/rubik-latin-ext-500-normal.5663c731.woff2 +0 -0
- package/build/static/media/rubik-latin-ext-600-normal.ff159cb8.woff2 +0 -0
- package/build/static/media/ubuntu-mono-all-400-normal.e39678a8.woff +0 -0
- package/build/static/media/ubuntu-mono-cyrillic-400-normal.e09900e7.woff2 +0 -0
- package/build/static/media/ubuntu-mono-cyrillic-ext-400-normal.762d0b06.woff2 +0 -0
- package/build/static/media/ubuntu-mono-greek-400-normal.81c98658.woff2 +0 -0
- package/build/static/media/ubuntu-mono-greek-ext-400-normal.1af17851.woff2 +0 -0
- package/build/static/media/ubuntu-mono-latin-400-normal.469ee478.woff2 +0 -0
- package/build/static/media/ubuntu-mono-latin-ext-400-normal.e28ff028.woff2 +0 -0
- package/lib/cache.js +25 -0
- package/lib/index.js +443 -0
- package/lib/mount.js +52 -0
- package/lib/util.js +17 -0
- package/package.json +119 -0
- package/services/auth/index.js +290 -0
- package/services/auth/libs/auth.js +76 -0
- package/services/auth/libs/jwt.js +73 -0
- package/services/auth/meta.json +61 -0
- package/services/auth/routes/blocklet-info.js +33 -0
- package/services/auth/routes/env.js +33 -0
- package/services/auth/routes/invite.js +80 -0
- package/services/auth/routes/issue-passport.js +58 -0
- package/services/auth/routes/login.js +230 -0
- package/services/auth/routes/lost-passport-issue.js +5 -0
- package/services/auth/routes/lost-passport-list.js +6 -0
- package/services/auth/routes/notification.js +257 -0
- package/services/auth/routes/passport.js +18 -0
- package/services/auth/routes/session.js +27 -0
- package/services/auth/state/index.js +30 -0
- package/services/auth/state/message.js +32 -0
- package/services/auth/util/index.js +35 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const joinUrl = require('url-join');
|
|
2
|
+
const {
|
|
3
|
+
createIssuePassportRequest,
|
|
4
|
+
handleIssuePassportResponse,
|
|
5
|
+
checkWalletVersion,
|
|
6
|
+
beforeIssuePassportRequest,
|
|
7
|
+
} = require('@abtnode/auth/lib/auth');
|
|
8
|
+
|
|
9
|
+
module.exports = function createRoutes(node, authenticator, login, opts) {
|
|
10
|
+
return {
|
|
11
|
+
action: 'issue-passport',
|
|
12
|
+
|
|
13
|
+
onStart: async ({ extraParams, req }) => {
|
|
14
|
+
const { locale = 'en', id } = extraParams;
|
|
15
|
+
const teamDid = req.headers['x-blocklet-did'];
|
|
16
|
+
|
|
17
|
+
await beforeIssuePassportRequest({ node, teamDid, locale, id });
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
claims: {
|
|
21
|
+
signature: async ({ extraParams, context: { request, abtwallet } }) => {
|
|
22
|
+
const { id, locale } = extraParams;
|
|
23
|
+
checkWalletVersion({ abtwallet, locale });
|
|
24
|
+
const nodeInfo = await node.getNodeInfo();
|
|
25
|
+
const teamDid = request.headers['x-blocklet-did'];
|
|
26
|
+
|
|
27
|
+
return createIssuePassportRequest({
|
|
28
|
+
node,
|
|
29
|
+
nodeInfo,
|
|
30
|
+
teamDid,
|
|
31
|
+
id,
|
|
32
|
+
locale,
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
onAuth: async ({ userDid, userPk, claims, extraParams, req, baseUrl }) => {
|
|
38
|
+
const { locale, id } = extraParams;
|
|
39
|
+
const nodeInfo = await node.getNodeInfo();
|
|
40
|
+
const teamDid = req.headers['x-blocklet-did'];
|
|
41
|
+
const statusEndpointBaseUrl = joinUrl(baseUrl, opts.prefix);
|
|
42
|
+
const endpoint = baseUrl;
|
|
43
|
+
|
|
44
|
+
return handleIssuePassportResponse({
|
|
45
|
+
node,
|
|
46
|
+
nodeInfo,
|
|
47
|
+
teamDid,
|
|
48
|
+
userDid,
|
|
49
|
+
userPk,
|
|
50
|
+
id,
|
|
51
|
+
locale,
|
|
52
|
+
claims,
|
|
53
|
+
statusEndpointBaseUrl,
|
|
54
|
+
endpoint,
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
};
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/* eslint-disable arrow-parens */
|
|
2
|
+
const get = require('lodash/get');
|
|
3
|
+
const joinUrl = require('url-join');
|
|
4
|
+
const {
|
|
5
|
+
messages,
|
|
6
|
+
getVCFromClaims,
|
|
7
|
+
checkWalletVersion,
|
|
8
|
+
getUser,
|
|
9
|
+
getPassportStatusEndpoint,
|
|
10
|
+
validatePassportStatus,
|
|
11
|
+
} = require('@abtnode/auth/lib/auth');
|
|
12
|
+
const { ROLES, VC_TYPE_GENERAL_PASSPORT, VC_TYPE_NODE_PASSPORT } = require('@abtnode/constant');
|
|
13
|
+
const {
|
|
14
|
+
createPassportVC,
|
|
15
|
+
createPassport,
|
|
16
|
+
validatePassport,
|
|
17
|
+
isUserPassportRevoked,
|
|
18
|
+
upsertToPassports,
|
|
19
|
+
getRoleFromLocalPassport,
|
|
20
|
+
getRoleFromExternalPassport,
|
|
21
|
+
createUserPassport,
|
|
22
|
+
} = require('@abtnode/auth/lib/passport');
|
|
23
|
+
const logger = require('@abtnode/logger')(require('../meta.json').name);
|
|
24
|
+
|
|
25
|
+
const vcTypes = [VC_TYPE_GENERAL_PASSPORT, VC_TYPE_NODE_PASSPORT];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @returns {Array} config
|
|
29
|
+
* @returns {Boolean} config[0] is invited user only
|
|
30
|
+
* @returns {String} config[1] default role
|
|
31
|
+
* @returns {Boolean} config[2] issue passport
|
|
32
|
+
*/
|
|
33
|
+
const isInvitedUserOnly = async (invitedUserOnly, node, teamDid) => {
|
|
34
|
+
const count = await node.getUsersCount({ teamDid });
|
|
35
|
+
if (invitedUserOnly === 'not-first') {
|
|
36
|
+
return count > 0 ? [true] : [false, ROLES.OWNER, true];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (invitedUserOnly === true || invitedUserOnly === 'yes') {
|
|
40
|
+
return [true];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return [false, ROLES.GUEST];
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
module.exports = function createRoutes(node, authenticator, login, opts) {
|
|
47
|
+
return {
|
|
48
|
+
action: 'login',
|
|
49
|
+
claims: {
|
|
50
|
+
profile: async ({ extraParams, context }) => {
|
|
51
|
+
const { locale } = extraParams;
|
|
52
|
+
|
|
53
|
+
const config = await context.request.getServiceConfig();
|
|
54
|
+
const profileFields = get(config, 'profileFields');
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
fields: profileFields || ['fullName', 'avatar'],
|
|
58
|
+
description: messages.description[locale],
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
verifiableCredential: async ({ context, extraParams: { locale } }) => {
|
|
63
|
+
const { request, abtwallet } = context;
|
|
64
|
+
const { wallet, did: teamDid } = await request.getBlockletInfo();
|
|
65
|
+
|
|
66
|
+
checkWalletVersion({ abtwallet, locale });
|
|
67
|
+
|
|
68
|
+
const blocklet = await request.getBlocklet();
|
|
69
|
+
const trustedPassports = (blocklet.trustedPassports || []).map((x) => x.issuerDid);
|
|
70
|
+
const trustedIssuers = [wallet.address, ...trustedPassports].filter(Boolean);
|
|
71
|
+
|
|
72
|
+
const config = (await request.getServiceConfig()) || {};
|
|
73
|
+
const [invitedUserOnly] = await isInvitedUserOnly(config.invitedUserOnly, node, teamDid);
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
description: messages.requestPassport[locale],
|
|
77
|
+
item: vcTypes,
|
|
78
|
+
trustedIssuers,
|
|
79
|
+
optional: !invitedUserOnly,
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
// eslint-disable-next-line consistent-return
|
|
85
|
+
onAuth: async ({ claims, challenge, userDid, userPk, token, storage, extraParams, req, baseUrl }) => {
|
|
86
|
+
const { locale } = extraParams;
|
|
87
|
+
const blocklet = await req.getBlocklet();
|
|
88
|
+
const { wallet, name, passportColor, did: teamDid } = await req.getBlockletInfo();
|
|
89
|
+
const teamAppDid = wallet.address;
|
|
90
|
+
|
|
91
|
+
// check user approved
|
|
92
|
+
const user = await getUser(node, teamDid, userDid);
|
|
93
|
+
if (user && !user.approved) {
|
|
94
|
+
throw new Error(messages.notAllowed[locale]);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Get passport
|
|
98
|
+
const trustedPassports = (blocklet.trustedPassports || []).map((x) => x.issuerDid);
|
|
99
|
+
const trustedIssuers = [teamAppDid, ...trustedPassports].filter(Boolean);
|
|
100
|
+
let { vc } = await getVCFromClaims({
|
|
101
|
+
claims,
|
|
102
|
+
challenge,
|
|
103
|
+
trustedIssuers,
|
|
104
|
+
vcTypes,
|
|
105
|
+
locale,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const config = (await req.getServiceConfig()) || {};
|
|
109
|
+
const [invitedUserOnly, defaultRole, issuePassport] = await isInvitedUserOnly(
|
|
110
|
+
config.invitedUserOnly,
|
|
111
|
+
node,
|
|
112
|
+
teamDid
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
if (invitedUserOnly && !vc) {
|
|
116
|
+
throw new Error(messages.missingCredentialClaim[locale]);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// issue passport for the first login user in a invite-only team
|
|
120
|
+
if (issuePassport) {
|
|
121
|
+
logger.info('issue passport to user at the login workflow', { role: defaultRole });
|
|
122
|
+
vc = createPassportVC({
|
|
123
|
+
issuerName: name,
|
|
124
|
+
issuerWallet: wallet,
|
|
125
|
+
ownerDid: userDid,
|
|
126
|
+
passport: await createPassport({
|
|
127
|
+
name: defaultRole,
|
|
128
|
+
node,
|
|
129
|
+
teamDid,
|
|
130
|
+
locale,
|
|
131
|
+
endpoint: baseUrl,
|
|
132
|
+
}),
|
|
133
|
+
endpoint: getPassportStatusEndpoint({
|
|
134
|
+
baseUrl: joinUrl(baseUrl, opts.prefix),
|
|
135
|
+
userDid,
|
|
136
|
+
teamDid,
|
|
137
|
+
}),
|
|
138
|
+
ownerProfile: user,
|
|
139
|
+
preferredColor: passportColor,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Get user passport from vc
|
|
144
|
+
let passport = vc ? createUserPassport(vc) : null;
|
|
145
|
+
if (user && passport && isUserPassportRevoked(user, passport)) {
|
|
146
|
+
throw new Error(messages.passportRevoked[locale](name));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Get role
|
|
150
|
+
let role = ROLES.GUEST;
|
|
151
|
+
if (vc) {
|
|
152
|
+
await validatePassport(get(vc, 'credentialSubject.passport'));
|
|
153
|
+
const issuerId = get(vc, 'issuer.id');
|
|
154
|
+
if (issuerId === teamAppDid) {
|
|
155
|
+
role = getRoleFromLocalPassport(get(vc, 'credentialSubject.passport'));
|
|
156
|
+
} else {
|
|
157
|
+
// map external passport to local role
|
|
158
|
+
const { mappings = [] } = (blocklet.trustedPassports || []).find((x) => x.issuerDid === issuerId) || {};
|
|
159
|
+
role = await getRoleFromExternalPassport({
|
|
160
|
+
passport: get(vc, 'credentialSubject.passport'),
|
|
161
|
+
node,
|
|
162
|
+
teamDid,
|
|
163
|
+
locale,
|
|
164
|
+
mappings,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// check status of external passport if passport has an endpoint
|
|
168
|
+
const endpoint = get(vc, 'credentialStatus.id');
|
|
169
|
+
if (endpoint) {
|
|
170
|
+
await validatePassportStatus({ vcId: vc.id, endpoint, locale });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Recreate passport with correct role
|
|
176
|
+
passport = vc ? createUserPassport(vc, { role }) : null;
|
|
177
|
+
|
|
178
|
+
// Update profile
|
|
179
|
+
try {
|
|
180
|
+
const profile = claims.find((x) => x.type === 'profile');
|
|
181
|
+
if (user) {
|
|
182
|
+
// Update user
|
|
183
|
+
await node.updateUser({
|
|
184
|
+
teamDid,
|
|
185
|
+
user: {
|
|
186
|
+
...profile,
|
|
187
|
+
did: userDid,
|
|
188
|
+
pk: userPk,
|
|
189
|
+
locale,
|
|
190
|
+
passports: upsertToPassports(user.passports || [], passport),
|
|
191
|
+
lastLoginAt: new Date().toISOString(),
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
} else {
|
|
195
|
+
// Create user
|
|
196
|
+
await node.addUser({
|
|
197
|
+
teamDid,
|
|
198
|
+
user: {
|
|
199
|
+
...profile,
|
|
200
|
+
did: userDid,
|
|
201
|
+
pk: userPk,
|
|
202
|
+
approved: true,
|
|
203
|
+
locale,
|
|
204
|
+
passports: [passport].filter(Boolean),
|
|
205
|
+
firstLoginAt: new Date().toISOString(),
|
|
206
|
+
lastLoginAt: new Date().toISOString(),
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Generate new session token that client can save to localStorage
|
|
212
|
+
const loginToken = await login(userDid, { passport, role });
|
|
213
|
+
await storage.update(token, { did: userDid, loginToken });
|
|
214
|
+
logger.info('login.success', { userDid, role });
|
|
215
|
+
|
|
216
|
+
// issue passport for the first login user in a invite-only team
|
|
217
|
+
if (issuePassport) {
|
|
218
|
+
return {
|
|
219
|
+
disposition: 'attachment',
|
|
220
|
+
type: 'VerifiableCredential',
|
|
221
|
+
data: vc,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
} catch (err) {
|
|
225
|
+
logger.error('login.error', { error: err, userDid });
|
|
226
|
+
throw new Error(err.message);
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
const { createLostPassportIssueRoute, TEAM_TYPES } = require('@abtnode/auth/lib/lost-passport');
|
|
2
|
+
|
|
3
|
+
module.exports = function createRoutes(node, authenticator, login, opts) {
|
|
4
|
+
return createLostPassportIssueRoute({ node, type: TEAM_TYPES.BLOCKLET, authServicePrefix: opts.prefix });
|
|
5
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
const { createLostPassportListRoute } = require('@abtnode/auth/lib/lost-passport');
|
|
2
|
+
|
|
3
|
+
// eslint-disable-next-line no-unused-vars
|
|
4
|
+
module.exports = function createRoutes(node, authenticator, login, opts) {
|
|
5
|
+
return createLostPassportListRoute({ node, type: 'blocklet' });
|
|
6
|
+
};
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/* eslint-disable arrow-parens */
|
|
2
|
+
const JWT = require('@arcblock/jwt');
|
|
3
|
+
const { WsServer } = require('@arcblock/ws');
|
|
4
|
+
const uuid = require('uuid');
|
|
5
|
+
const Cron = require('@abtnode/cron');
|
|
6
|
+
const {
|
|
7
|
+
validateNotification,
|
|
8
|
+
validateReceiver,
|
|
9
|
+
validateMessage,
|
|
10
|
+
} = require('@blocklet/sdk/lib/validators/notification');
|
|
11
|
+
// eslint-disable-next-line global-require
|
|
12
|
+
const logger = require('@abtnode/logger')(`${require('../meta.json').name}:notification`);
|
|
13
|
+
const { getTeamInfo } = require('@abtnode/auth/lib/auth');
|
|
14
|
+
const { NODE_MODES } = require('@abtnode/constant');
|
|
15
|
+
|
|
16
|
+
const states = require('../state');
|
|
17
|
+
const { getBlockletLogo } = require('../util');
|
|
18
|
+
|
|
19
|
+
const getDid = (jwt) => jwt.iss.replace(/^did:abt:/, '');
|
|
20
|
+
|
|
21
|
+
const authenticate = (req, cb) => {
|
|
22
|
+
const { searchParams } = new URL(req.url, `http://${req.headers.host || 'unknown'}`);
|
|
23
|
+
const token = searchParams.get('token');
|
|
24
|
+
const pk = searchParams.get('pk');
|
|
25
|
+
|
|
26
|
+
if (!token) {
|
|
27
|
+
cb(new Error('token not found'), null);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!JWT.verify(token, pk)) {
|
|
32
|
+
cb(new Error('token verify failed'));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
cb(null, { did: getDid(JWT.decode(token)) });
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
*
|
|
41
|
+
* @param {ABTNode} node
|
|
42
|
+
* @param {Object} payload
|
|
43
|
+
* {Object} sender: blocklet
|
|
44
|
+
* {String} sender.did: blocklet did
|
|
45
|
+
* {String} sender.token: for the verification
|
|
46
|
+
* {Array|String} receiver: user did
|
|
47
|
+
* {Array|Object} notification
|
|
48
|
+
* @returns
|
|
49
|
+
*/
|
|
50
|
+
const onSendToUser = async (node, payload, wsServer) => {
|
|
51
|
+
const { sender, receiver, notification } = payload;
|
|
52
|
+
|
|
53
|
+
await validateReceiver(receiver);
|
|
54
|
+
|
|
55
|
+
const nodeInfo = await node.getNodeInfo();
|
|
56
|
+
|
|
57
|
+
if (nodeInfo.mode !== NODE_MODES.DEBUG) {
|
|
58
|
+
await validateNotification(notification);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let senderInfo;
|
|
62
|
+
try {
|
|
63
|
+
senderInfo = await getTeamInfo({ node, nodeInfo, teamDid: sender.did });
|
|
64
|
+
} catch (err) {
|
|
65
|
+
if (err.message === 'Blocklet state must be an object') {
|
|
66
|
+
err.message = `Sender blocklet does not exist: ${sender.did}`;
|
|
67
|
+
}
|
|
68
|
+
throw err;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const { wallet, name, type: senderType } = senderInfo;
|
|
72
|
+
if (!JWT.verify(sender.token, wallet.publicKey)) {
|
|
73
|
+
throw new Error(`Invalid authentication token for sender blocklet: ${sender.did}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (sender.appDid !== wallet.address) {
|
|
77
|
+
throw new Error(`Invalid app did, expected: ${wallet.address}, actual: ${sender.appDid}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let blocklet;
|
|
81
|
+
if (senderType === 'blocklet') {
|
|
82
|
+
blocklet = await node.getBlocklet({ did: sender.did, attachRuntimeInfo: false });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// parse notifications
|
|
86
|
+
|
|
87
|
+
const notifications = [].concat(notification);
|
|
88
|
+
|
|
89
|
+
notifications.forEach((x) => {
|
|
90
|
+
x.id = uuid.v4();
|
|
91
|
+
x.sender = {
|
|
92
|
+
did: sender.appDid,
|
|
93
|
+
name,
|
|
94
|
+
icon: senderType === 'blocklet' ? getBlockletLogo({ blocklet }) : null, // deprecated: did wallet will find icon locally by sender appDid
|
|
95
|
+
};
|
|
96
|
+
x.createdAt = new Date();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// parse receivers
|
|
100
|
+
|
|
101
|
+
const receivers = [].concat(receiver);
|
|
102
|
+
|
|
103
|
+
// send notification
|
|
104
|
+
|
|
105
|
+
const EVENT_NAME = 'message';
|
|
106
|
+
const createTask = async (did, data) => {
|
|
107
|
+
try {
|
|
108
|
+
await new Promise((resolve, reject) => {
|
|
109
|
+
try {
|
|
110
|
+
wsServer.broadcast(did, EVENT_NAME, data, ({ count } = {}) => {
|
|
111
|
+
if (count <= 0) {
|
|
112
|
+
logger.info('Online client for the user was not found', { userDid: did });
|
|
113
|
+
reject(new Error('Online client for the user was not found'));
|
|
114
|
+
} else {
|
|
115
|
+
resolve();
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
} catch (error) {
|
|
119
|
+
logger.error('Failed on broadcast message', { error });
|
|
120
|
+
reject(error);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
} catch {
|
|
124
|
+
// failed on broadcast or online client not found
|
|
125
|
+
await states.message.insert({ did, event: EVENT_NAME, data });
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const tasks = [];
|
|
130
|
+
receivers.forEach((did) => {
|
|
131
|
+
notifications.forEach((data) => {
|
|
132
|
+
tasks.push(createTask(did, data));
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// FIXME: Enhance task reliability. e.g. resend after timeout or error; add message status: sent, received, staged
|
|
137
|
+
await Promise.allSettled(tasks);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const verify = async ({ topic, payload }) => {
|
|
141
|
+
const did = getDid(JWT.decode(payload.token));
|
|
142
|
+
|
|
143
|
+
// Support the web wallet to continue to run for one day
|
|
144
|
+
//
|
|
145
|
+
// if abtnode is restarted when there is a wallet wallet connection
|
|
146
|
+
// then the web wallet will auto reconnect to the abtnode and auto rejoin the channels
|
|
147
|
+
// the web wallet will reuse the JWT token when rejoin channel
|
|
148
|
+
// so we need to support token is valid for one day
|
|
149
|
+
const tolerance = 3600 * 24;
|
|
150
|
+
|
|
151
|
+
if (!JWT.verify(payload.token, payload.pk, { tolerance })) {
|
|
152
|
+
throw new Error(`verify did failed: ${did}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (did !== topic) {
|
|
156
|
+
throw new Error(`verified did and topic does not match. did: ${did}, topic: ${topic}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (payload.message) {
|
|
160
|
+
await validateMessage(payload.message);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const receiveMessage = async ({ topic, event, payload, wsServer, node }) => {
|
|
165
|
+
if (event !== 'message') {
|
|
166
|
+
throw new Error(`Invalid event. expect: "message". got: "${event}"`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
await validateMessage(payload);
|
|
170
|
+
|
|
171
|
+
const { receiver, ...data } = payload;
|
|
172
|
+
|
|
173
|
+
// FIXME: use node.getBlocklet() when support index by appID
|
|
174
|
+
const blocklet = await node.getBlocklet({ did: receiver.did, attachConfig: false });
|
|
175
|
+
if (!blocklet) {
|
|
176
|
+
throw new Error('App is not installed in the server');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
logger.info('send message to blocklet', { sender: topic, receiver });
|
|
180
|
+
wsServer.broadcast(payload.receiver.did, 'message', {
|
|
181
|
+
...data,
|
|
182
|
+
sender: {
|
|
183
|
+
did: topic,
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const sendCachedMessages = async (wsServer, payload) => {
|
|
189
|
+
try {
|
|
190
|
+
const did = getDid(JWT.decode(payload.token));
|
|
191
|
+
|
|
192
|
+
const messages = await states.message.find({ did });
|
|
193
|
+
if (!messages.length) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
messages.forEach(({ did: d, event, data }) => {
|
|
198
|
+
wsServer.broadcast(d, event, data);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
await states.message.remove({ did }, { multi: true });
|
|
202
|
+
} catch (error) {
|
|
203
|
+
logger.error('Error on sending cached messages', { error });
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const init = (node, httpRouter, wsRouter, opts) => {
|
|
208
|
+
// Create ws server
|
|
209
|
+
const wsServer = new WsServer({
|
|
210
|
+
logger,
|
|
211
|
+
authenticate,
|
|
212
|
+
hooks: {
|
|
213
|
+
preJoinChannel: (param) => verify({ ...param, wsServer, node }),
|
|
214
|
+
postJoinChannel: async ({ topic, payload }) => {
|
|
215
|
+
logger.info('Subscribe notification success', { account: topic });
|
|
216
|
+
await sendCachedMessages(wsServer, payload);
|
|
217
|
+
if (payload.message) {
|
|
218
|
+
await receiveMessage({ topic, event: 'message', payload: payload.message, wsServer, node });
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
receiveMessage: (param) => receiveMessage({ ...param, wsServer, node }),
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Let first worker process do something as master
|
|
226
|
+
if (process.env.NODE_APP_INSTANCE === '0') {
|
|
227
|
+
// cron
|
|
228
|
+
Cron.init({
|
|
229
|
+
jobs: [
|
|
230
|
+
{
|
|
231
|
+
name: 'prune messages',
|
|
232
|
+
time: '0 0 0 * * 1', // every Monday per week
|
|
233
|
+
fn: () => states.message.prune(),
|
|
234
|
+
},
|
|
235
|
+
],
|
|
236
|
+
onError: (error, name) => {
|
|
237
|
+
logger.error('Run job failed', { name, error });
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// mount
|
|
243
|
+
wsRouter.use(`${opts.prefix}/websocket`, wsServer.onConnect.bind(wsServer));
|
|
244
|
+
|
|
245
|
+
httpRouter.post(`${opts.prefix}/api/sendToUser`, async (req, res) => {
|
|
246
|
+
try {
|
|
247
|
+
await onSendToUser(node, req.body.data, wsServer);
|
|
248
|
+
res.status(200).send('');
|
|
249
|
+
} catch (error) {
|
|
250
|
+
logger.error('Send message to user failed', { error });
|
|
251
|
+
res.statusMessage = error.message;
|
|
252
|
+
res.status(400).send(error.message);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
module.exports = { init };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const { getPassportStatus } = require('@abtnode/auth/lib/auth');
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
init(server, node, opts) {
|
|
5
|
+
server.get(`${opts.prefix}/api/passport/status`, async (req, res) => {
|
|
6
|
+
const { vcId, userDid, locale } = req.query;
|
|
7
|
+
const teamDid = req.headers['x-blocklet-did'];
|
|
8
|
+
|
|
9
|
+
if (teamDid !== req.query.teamDid) {
|
|
10
|
+
throw new Error('teamDid is invalid');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const status = await getPassportStatus({ node, teamDid, userDid, vcId, locale });
|
|
14
|
+
|
|
15
|
+
res.json(status);
|
|
16
|
+
});
|
|
17
|
+
},
|
|
18
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const nocache = require('nocache');
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
init(server, node, opts) {
|
|
5
|
+
const prefix = `${opts.prefix}/api/did/session`;
|
|
6
|
+
|
|
7
|
+
const handler = async (req, res) => {
|
|
8
|
+
if (!req.user) {
|
|
9
|
+
res.json({ user: null });
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const user = { ...req.user };
|
|
14
|
+
if (user.role) {
|
|
15
|
+
const teamDid = req.getBlockletDid();
|
|
16
|
+
// FIXME: this code may have performance issue
|
|
17
|
+
const rbac = await node.getRBAC(teamDid);
|
|
18
|
+
user.permissions = await rbac.getScope(req.user.role);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
res.json({ user });
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
server.get(prefix, nocache(), handler);
|
|
25
|
+
server.post(prefix, nocache(), handler);
|
|
26
|
+
},
|
|
27
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const BaseState = require('@abtnode/core/lib/states/base');
|
|
2
|
+
const MessageState = require('./message');
|
|
3
|
+
|
|
4
|
+
const states = {};
|
|
5
|
+
|
|
6
|
+
// eslint-disable-next-line no-unused-vars
|
|
7
|
+
const init = (dataDir, options = {}) => {
|
|
8
|
+
const messageState = new MessageState(dataDir);
|
|
9
|
+
|
|
10
|
+
Object.assign(states, {
|
|
11
|
+
message: messageState,
|
|
12
|
+
});
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
module.exports = new Proxy(
|
|
16
|
+
{},
|
|
17
|
+
{
|
|
18
|
+
get(target, prop) {
|
|
19
|
+
if (prop === 'init') {
|
|
20
|
+
return init;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (states[prop] instanceof BaseState) {
|
|
24
|
+
return states[prop];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
throw new Error(`State ${String(prop)} may not be initialized`);
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const BaseState = require('@abtnode/core/lib/states/base');
|
|
2
|
+
const logger = require('@abtnode/logger')('auth-service:states:message');
|
|
3
|
+
|
|
4
|
+
class MessageState extends BaseState {
|
|
5
|
+
constructor(baseDir, options = {}) {
|
|
6
|
+
super(baseDir, { filename: 'message.db', ...options });
|
|
7
|
+
|
|
8
|
+
this.db.ensureIndex({ fieldName: 'did' }, (error) => {
|
|
9
|
+
if (error) {
|
|
10
|
+
logger.error('ensure index failed', { error });
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
insert(doc, ...args) {
|
|
16
|
+
return super.insert(
|
|
17
|
+
{
|
|
18
|
+
...doc,
|
|
19
|
+
created: Date.now(),
|
|
20
|
+
updated: Date.now(),
|
|
21
|
+
},
|
|
22
|
+
...args
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
prune(time) {
|
|
27
|
+
const t = time || 30 * 24 * 60 * 60 * 1000; // 30 day
|
|
28
|
+
return this.remove({ created: { $lt: Date.now() - t } });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = MessageState;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const { BlockletSource } = require('@blocklet/meta/lib/constants');
|
|
2
|
+
|
|
3
|
+
const getLogoFromLogoUrl = (blocklet) => {
|
|
4
|
+
try {
|
|
5
|
+
if (blocklet.source === BlockletSource.registry) {
|
|
6
|
+
return new URL(blocklet.meta.logoUrl, blocklet.deployedFrom).href;
|
|
7
|
+
}
|
|
8
|
+
return blocklet.meta.logoUrl || null;
|
|
9
|
+
} catch (error) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const getBlockletLogo = ({ baseUrl, blocklet }) => {
|
|
15
|
+
if (!baseUrl) {
|
|
16
|
+
return getLogoFromLogoUrl(blocklet);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let logo = `${baseUrl}/images/blocklet.png`;
|
|
20
|
+
try {
|
|
21
|
+
if (blocklet.meta.logoUrl) {
|
|
22
|
+
logo = getLogoFromLogoUrl(blocklet);
|
|
23
|
+
} else if (blocklet.meta.logo) {
|
|
24
|
+
logo = `${baseUrl}/blocklet/logo/${blocklet.meta.did}`;
|
|
25
|
+
}
|
|
26
|
+
} catch (err) {
|
|
27
|
+
// Do nothing
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return logo;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
module.exports = {
|
|
34
|
+
getBlockletLogo,
|
|
35
|
+
};
|