dce-reactkit 3.11.4 → 3.12.1-beta-cross-server.2
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/.vscode/settings.json +1 -3
- package/README.md +20 -0
- package/dist/cjs/index.js +294 -162
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/types/helpers/dataSigner.d.ts +38 -0
- package/dist/cjs/types/helpers/genRouteHandler.d.ts +10 -10
- package/dist/cjs/types/helpers/visitEndpointOnAnotherServer/index.d.ts +15 -13
- package/dist/cjs/types/helpers/visitEndpointOnAnotherServer/sendServerToServerRequest.d.ts +0 -6
- package/dist/cjs/types/index.d.ts +3 -2
- package/dist/cjs/types/server/initCrossServerCredentialCollection.d.ts +8 -0
- package/dist/cjs/types/server/initServer.d.ts +11 -0
- package/dist/cjs/types/types/CrossServerCredential.d.ts +11 -0
- package/dist/cjs/types/types/ReactKitErrorCode.d.ts +13 -1
- package/dist/esm/index.js +293 -162
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/types/helpers/dataSigner.d.ts +38 -0
- package/dist/esm/types/helpers/genRouteHandler.d.ts +10 -10
- package/dist/esm/types/helpers/visitEndpointOnAnotherServer/index.d.ts +15 -13
- package/dist/esm/types/helpers/visitEndpointOnAnotherServer/sendServerToServerRequest.d.ts +0 -6
- package/dist/esm/types/index.d.ts +3 -2
- package/dist/esm/types/server/initCrossServerCredentialCollection.d.ts +8 -0
- package/dist/esm/types/server/initServer.d.ts +11 -0
- package/dist/esm/types/types/CrossServerCredential.d.ts +11 -0
- package/dist/esm/types/types/ReactKitErrorCode.d.ts +13 -1
- package/dist/index.d.ts +62 -35
- package/genEncodedSecret.ts +28 -0
- package/package.json +5 -2
- package/src/components/IntelliTable.tsx +1 -1
- package/src/components/ItemPicker/NestableItemList.tsx +0 -1
- package/src/helpers/dataSigner.ts +232 -0
- package/src/helpers/genRouteHandler.ts +55 -95
- package/src/helpers/visitEndpointOnAnotherServer/index.ts +42 -41
- package/src/helpers/visitEndpointOnAnotherServer/sendServerToServerRequest.ts +3 -5
- package/src/index.ts +4 -2
- package/src/server/initCrossServerCredentialCollection.ts +16 -0
- package/src/server/initServer.ts +18 -0
- package/src/types/CrossServerCredential.ts +16 -0
- package/src/types/ReactKitErrorCode.tsx +13 -1
- package/dist/cjs/types/helpers/getWordCount.d.ts +0 -10
- package/dist/esm/types/helpers/getWordCount.d.ts +0 -10
- package/src/helpers/getWordCount.ts +0 -26
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// Import crypto
|
|
2
|
+
import { createHash } from 'crypto';
|
|
3
|
+
|
|
4
|
+
// Import jwt
|
|
5
|
+
import jwt from 'jwt-simple';
|
|
6
|
+
|
|
7
|
+
// Import shared helpers
|
|
8
|
+
import { internalGetCrossServerCredentialCollection } from '../server/initServer';
|
|
9
|
+
|
|
10
|
+
// Import shared types
|
|
11
|
+
import ErrorWithCode from '../errors/ErrorWithCode';
|
|
12
|
+
import ReactKitErrorCode from '../types/ReactKitErrorCode';
|
|
13
|
+
import CrossServerCredential from '../types/CrossServerCredential';
|
|
14
|
+
|
|
15
|
+
// Import shared constants
|
|
16
|
+
import MINUTE_IN_MS from '../constants/MINUTE_IN_MS';
|
|
17
|
+
|
|
18
|
+
/*------------------------------------------------------------------------*/
|
|
19
|
+
/* ------------------------------ Encoding ------------------------------ */
|
|
20
|
+
/*------------------------------------------------------------------------*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Validate a JWT token
|
|
24
|
+
* @author Gabe Abrams
|
|
25
|
+
* @param encodedSecret the encoded secret
|
|
26
|
+
* @return the secret
|
|
27
|
+
*/
|
|
28
|
+
const decodeCredentialSecret = (encodedSecret: string): string => {
|
|
29
|
+
// Get the credential encoding salt
|
|
30
|
+
const credentialEncodingSalt = process.env.REACTKIT_CRED_ENCODING_SALT;
|
|
31
|
+
if (!credentialEncodingSalt) {
|
|
32
|
+
throw new ErrorWithCode(
|
|
33
|
+
'Cannot decode a cross-server credential secret because the credential encoding salt was not found in env.',
|
|
34
|
+
ReactKitErrorCode.CrossServerNoCredentialEncodingSalt,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Decode the secret
|
|
39
|
+
return jwt.decode(encodedSecret, credentialEncodingSalt);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/*------------------------------------------------------------------------*/
|
|
43
|
+
/* ------------------------------- Signing ------------------------------ */
|
|
44
|
+
/*------------------------------------------------------------------------*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Sign using sha 256
|
|
48
|
+
* @author Gabe Abrams
|
|
49
|
+
* @param opts object containing all arguments
|
|
50
|
+
* @param opts.pack the pack to sign
|
|
51
|
+
* @param opts.secret the reactkit secret to sign with
|
|
52
|
+
* @return the signed hash
|
|
53
|
+
*/
|
|
54
|
+
const genSignature = (
|
|
55
|
+
opts: {
|
|
56
|
+
pack: string,
|
|
57
|
+
secret: string,
|
|
58
|
+
},
|
|
59
|
+
): string => {
|
|
60
|
+
// Generate signature
|
|
61
|
+
return (
|
|
62
|
+
createHash('sha256')
|
|
63
|
+
.update(JSON.stringify({ pack: opts.pack, secret: opts.secret }))
|
|
64
|
+
.digest('base64')
|
|
65
|
+
);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Sign data with a private reactkit key, package it into a signed data pack
|
|
70
|
+
* @author Gabe Abrams
|
|
71
|
+
* @param opts object containing all arguments
|
|
72
|
+
* @param opts.method the method to sign
|
|
73
|
+
* @param opts.path the http request path
|
|
74
|
+
* @param opts.params the data in the body to sign
|
|
75
|
+
* @param opts.key the reactkit key to sign with
|
|
76
|
+
* @param opts.secret the reactkit secret to sign with
|
|
77
|
+
* @return the signed data
|
|
78
|
+
*/
|
|
79
|
+
export const createSignedPack = (
|
|
80
|
+
opts: {
|
|
81
|
+
method: string,
|
|
82
|
+
path: string,
|
|
83
|
+
params: { [key: string]: any },
|
|
84
|
+
key: string,
|
|
85
|
+
secret: string,
|
|
86
|
+
},
|
|
87
|
+
): string => {
|
|
88
|
+
// Create a timestamp
|
|
89
|
+
const timestamp = Date.now();
|
|
90
|
+
|
|
91
|
+
// Create the pack
|
|
92
|
+
const pack = JSON.stringify({
|
|
93
|
+
method: opts.method,
|
|
94
|
+
path: opts.path,
|
|
95
|
+
params: opts.params,
|
|
96
|
+
key: opts.key,
|
|
97
|
+
timestamp,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Generate signature
|
|
101
|
+
const signature = genSignature({
|
|
102
|
+
pack,
|
|
103
|
+
secret: opts.secret,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Create a signed pack
|
|
107
|
+
const signedPack = encodeURIComponent(JSON.stringify({
|
|
108
|
+
pack,
|
|
109
|
+
signature,
|
|
110
|
+
}));
|
|
111
|
+
|
|
112
|
+
// Return the signed pack
|
|
113
|
+
return signedPack;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Parse signed pack. Throws an error if invalid
|
|
118
|
+
* @author Gabe Abrams
|
|
119
|
+
* @param opts object containing all arguments
|
|
120
|
+
* @param opts.method the method of the data validate
|
|
121
|
+
* @param opts.path the http request path to validate
|
|
122
|
+
* @param opts.scope the name of the scope to validate
|
|
123
|
+
* @param opts.signedPack the signed data pack to validate
|
|
124
|
+
* @returns parsed and validated params
|
|
125
|
+
*/
|
|
126
|
+
export const parseSignedPack = async (
|
|
127
|
+
opts: {
|
|
128
|
+
method: string,
|
|
129
|
+
path: string,
|
|
130
|
+
scope: string,
|
|
131
|
+
signedPack: string,
|
|
132
|
+
},
|
|
133
|
+
): Promise<{ [key: string]: any }> => {
|
|
134
|
+
// Extract signature
|
|
135
|
+
let pack: string;
|
|
136
|
+
let signature: string;
|
|
137
|
+
let method: string;
|
|
138
|
+
let path: string;
|
|
139
|
+
let key: string;
|
|
140
|
+
let timestamp: number;
|
|
141
|
+
let params: { [k: string]: any };
|
|
142
|
+
try {
|
|
143
|
+
({
|
|
144
|
+
pack,
|
|
145
|
+
signature,
|
|
146
|
+
} = JSON.parse(decodeURIComponent(opts.signedPack)));
|
|
147
|
+
|
|
148
|
+
// Unpack
|
|
149
|
+
({
|
|
150
|
+
method,
|
|
151
|
+
path,
|
|
152
|
+
params,
|
|
153
|
+
key,
|
|
154
|
+
timestamp,
|
|
155
|
+
} = JSON.parse(pack));
|
|
156
|
+
} catch (err) {
|
|
157
|
+
throw new ErrorWithCode(
|
|
158
|
+
'Could not validate a cross-server request because the request could not be parsed.',
|
|
159
|
+
ReactKitErrorCode.PackUnparseable,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Make sure the method and path match
|
|
164
|
+
if (method !== opts.method) {
|
|
165
|
+
throw new ErrorWithCode(
|
|
166
|
+
'Could not validate a cross-server request because the method did not match.',
|
|
167
|
+
ReactKitErrorCode.PackInvalidMethod,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
if (path !== opts.path) {
|
|
171
|
+
throw new ErrorWithCode(
|
|
172
|
+
'Could not validate a cross-server request because the path did not match.',
|
|
173
|
+
ReactKitErrorCode.PackInvalidPath,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Make sure the timestamp was recent enough
|
|
178
|
+
const elapsedMs = Math.abs(Date.now() - timestamp);
|
|
179
|
+
if (elapsedMs < MINUTE_IN_MS) {
|
|
180
|
+
throw new ErrorWithCode(
|
|
181
|
+
'Could not validate a cross-server request because the request was too old.',
|
|
182
|
+
ReactKitErrorCode.PackInvalidTimestamp,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Get the cross-server credential collection
|
|
187
|
+
const crossServerCredentialCollection = internalGetCrossServerCredentialCollection();
|
|
188
|
+
if (!crossServerCredentialCollection) {
|
|
189
|
+
throw new ErrorWithCode(
|
|
190
|
+
'Could not validate a cross-server request because the cross-server credential collection was not ready in time.',
|
|
191
|
+
ReactKitErrorCode.PackInvalidCollection,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Get the cross-server credential
|
|
196
|
+
const crossServerCredential: CrossServerCredential = await crossServerCredentialCollection.find({ key });
|
|
197
|
+
if (!crossServerCredential) {
|
|
198
|
+
throw new ErrorWithCode(
|
|
199
|
+
'Could not validate a cross-server request because the credential was not found.',
|
|
200
|
+
ReactKitErrorCode.PackInvalidCredential,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Make sure the scope is included
|
|
205
|
+
const allowedScopes = crossServerCredential.scopes;
|
|
206
|
+
if (!allowedScopes.includes(opts.scope)) {
|
|
207
|
+
throw new ErrorWithCode(
|
|
208
|
+
'Could not validate a cross-server request because the scope was not included.',
|
|
209
|
+
ReactKitErrorCode.PackInvalidScope,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Decode the secret
|
|
214
|
+
const secret = decodeCredentialSecret(crossServerCredential.encodedeSecret);
|
|
215
|
+
|
|
216
|
+
// Generate signature
|
|
217
|
+
const expectedSignature = genSignature({
|
|
218
|
+
pack,
|
|
219
|
+
secret,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Make sure the signature matches
|
|
223
|
+
if (signature !== expectedSignature) {
|
|
224
|
+
throw new ErrorWithCode(
|
|
225
|
+
'Could not validate a cross-server request because the signature did not match.',
|
|
226
|
+
ReactKitErrorCode.PackInvalidSignature,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Return body
|
|
231
|
+
return params ?? {};
|
|
232
|
+
};
|
|
@@ -17,6 +17,7 @@ import genErrorPage from '../html/genErrorPage';
|
|
|
17
17
|
import genInfoPage from '../html/genInfoPage';
|
|
18
18
|
import parseUserAgent from './parseUserAgent';
|
|
19
19
|
import getTimeInfoInET from './getTimeInfoInET';
|
|
20
|
+
import { parseSignedPack } from './dataSigner';
|
|
20
21
|
|
|
21
22
|
// Import shared types
|
|
22
23
|
import LogFunction from '../types/LogFunction';
|
|
@@ -29,6 +30,7 @@ import LogSourceSpecificInfo from '../types/Log/LogSourceSpecificInfo';
|
|
|
29
30
|
import LogBuiltInMetadata from '../types/LogBuiltInMetadata';
|
|
30
31
|
import LogAction from '../types/LogAction';
|
|
31
32
|
import LogLevel from '../types/LogLevel';
|
|
33
|
+
import ErrorWithCode from '../errors/ErrorWithCode';
|
|
32
34
|
|
|
33
35
|
/**
|
|
34
36
|
* Generate an express API route handler
|
|
@@ -37,14 +39,15 @@ import LogLevel from '../types/LogLevel';
|
|
|
37
39
|
* @param opts.paramTypes map containing the types for each parameter that is
|
|
38
40
|
* included in the request (map: param name => type)
|
|
39
41
|
* @param opts.handler function that processes the request
|
|
40
|
-
* @param [opts.
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
42
|
+
* @param [opts.crossServerScope] the scope associated with this endpoint.
|
|
43
|
+
* If defined, this is a cross-server endpoint, which will never
|
|
44
|
+
* have any launch data, will never check Canvas roles or launch status, and will
|
|
45
|
+
* instead use scopes and reactkit credentials to sign and validate requests.
|
|
46
|
+
* Never start the path with /api/ttm or /api/admin if the endpoint is a cross-server
|
|
47
|
+
* endpoint because those roles will not be validated
|
|
48
|
+
* @param [opts.skipSessionCheck=true if crossServerScope defined] if true, skip
|
|
49
|
+
* the session check (allow users to not be logged in and launched via LTI).
|
|
50
|
+
* If crossServerScope is defined, this is always true
|
|
48
51
|
* @param [opts.unhandledErrorMessagePrefix] if included, when an error that
|
|
49
52
|
* is not of type ErrorWithCode is thrown, the client will receive an error
|
|
50
53
|
* where the error message is prefixed with this string. For example,
|
|
@@ -107,106 +110,67 @@ const genRouteHandler = (
|
|
|
107
110
|
logServerEvent: LogFunction,
|
|
108
111
|
},
|
|
109
112
|
) => any,
|
|
113
|
+
crossServerScope?: string,
|
|
110
114
|
skipSessionCheck?: boolean,
|
|
111
|
-
allowedHosts?: string[] | string,
|
|
112
|
-
bannedHosts?: string[] | string,
|
|
113
115
|
unhandledErrorMessagePrefix?: string,
|
|
114
116
|
},
|
|
115
117
|
) => {
|
|
116
118
|
// Return a route handler
|
|
117
119
|
return async (req: any, res: any, next: () => void) => {
|
|
120
|
+
/*----------------------------------------*/
|
|
121
|
+
/* ------------- Preparation ------------ */
|
|
122
|
+
/*----------------------------------------*/
|
|
123
|
+
|
|
118
124
|
// Output params
|
|
119
125
|
const output: { [k in string]: any } = {};
|
|
120
126
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
127
|
+
// Determine cross server scopes
|
|
128
|
+
let crossServerScope: string | null = null;
|
|
129
|
+
if (opts.crossServerScope) {
|
|
130
|
+
crossServerScope = opts.crossServerScope ?? null;
|
|
131
|
+
}
|
|
124
132
|
|
|
125
|
-
//
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
||
|
|
129
|
-
|| req.headers.referer,
|
|
133
|
+
// Determine whether we're skipping the session check
|
|
134
|
+
const skipSessionCheck = !!(
|
|
135
|
+
opts.skipSessionCheck
|
|
136
|
+
|| crossServerScope
|
|
130
137
|
);
|
|
131
|
-
const originHostname = (
|
|
132
|
-
originURL
|
|
133
|
-
// Remove protocol
|
|
134
|
-
.replace(/(^\w+:|^)\/\//, '')
|
|
135
|
-
// Remove port
|
|
136
|
-
.replace(/:\d+$/, '')
|
|
137
|
-
);
|
|
138
|
-
const serverHostname = String(req.hostname);
|
|
139
|
-
|
|
140
|
-
// Check allowed
|
|
141
|
-
if (opts.allowedHosts) {
|
|
142
|
-
// Only accept requests from allowed hosts
|
|
143
|
-
const allowedArray = (
|
|
144
|
-
Array.isArray(opts.allowedHosts)
|
|
145
|
-
? opts.allowedHosts
|
|
146
|
-
: [opts.allowedHosts]
|
|
147
|
-
);
|
|
148
138
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
139
|
+
// Get body from everywhere it can come from
|
|
140
|
+
let requestBody: {
|
|
141
|
+
[k: string]: any,
|
|
142
|
+
} = {
|
|
143
|
+
...req.body,
|
|
144
|
+
...req.query,
|
|
145
|
+
...req.params,
|
|
146
|
+
};
|
|
154
147
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
// Check end of hostname
|
|
159
|
-
return originHostname.endsWith(allowedHost.substring(1));
|
|
160
|
-
}
|
|
148
|
+
/*----------------------------------------*/
|
|
149
|
+
/* ------- Cross-Server Validation ------ */
|
|
150
|
+
/*----------------------------------------*/
|
|
161
151
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
152
|
+
if (crossServerScope) {
|
|
153
|
+
// Get the signed pack
|
|
154
|
+
const { signedPack } = requestBody;
|
|
165
155
|
|
|
166
|
-
// If
|
|
167
|
-
if (!
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
message: 'You are not allowed to access this endpoint.',
|
|
172
|
-
code: ReactKitErrorCode.HostNotAllowed,
|
|
173
|
-
status: 403,
|
|
174
|
-
},
|
|
156
|
+
// If no pack, throw error
|
|
157
|
+
if (!signedPack || typeof signedPack !== 'string') {
|
|
158
|
+
throw new ErrorWithCode(
|
|
159
|
+
'Could not process a cross server request because there was no valid signed pack.',
|
|
160
|
+
ReactKitErrorCode.CrossServerNoPack,
|
|
175
161
|
);
|
|
176
162
|
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Check banned
|
|
180
|
-
if (opts.bannedHosts) {
|
|
181
|
-
// Do not allow requests from banned hosts
|
|
182
|
-
const bannedArray = (
|
|
183
|
-
Array.isArray(opts.bannedHosts)
|
|
184
|
-
? opts.bannedHosts
|
|
185
|
-
: [opts.bannedHosts]
|
|
186
|
-
);
|
|
187
|
-
|
|
188
|
-
// Check if current host is banned
|
|
189
|
-
const banned = bannedArray.some((bannedHost) => {
|
|
190
|
-
if (bannedHost.startsWith('*')) {
|
|
191
|
-
// Check end of hostname
|
|
192
|
-
return originHostname.endsWith(bannedHost.substring(1));
|
|
193
|
-
}
|
|
194
163
|
|
|
195
|
-
|
|
196
|
-
|
|
164
|
+
// Validate the request
|
|
165
|
+
const crossServerParams = await parseSignedPack({
|
|
166
|
+
method: req.method ?? 'GET',
|
|
167
|
+
path: req.path,
|
|
168
|
+
scope: crossServerScope,
|
|
169
|
+
signedPack,
|
|
197
170
|
});
|
|
198
171
|
|
|
199
|
-
//
|
|
200
|
-
|
|
201
|
-
return handleError(
|
|
202
|
-
res,
|
|
203
|
-
{
|
|
204
|
-
message: 'You are not allowed to access this endpoint.',
|
|
205
|
-
code: ReactKitErrorCode.HostBanned,
|
|
206
|
-
status: 403,
|
|
207
|
-
},
|
|
208
|
-
);
|
|
209
|
-
}
|
|
172
|
+
// Replace body with params from the pack
|
|
173
|
+
requestBody = crossServerParams ?? {};
|
|
210
174
|
}
|
|
211
175
|
|
|
212
176
|
/*----------------------------------------*/
|
|
@@ -219,11 +183,7 @@ const genRouteHandler = (
|
|
|
219
183
|
const [name, type] = paramList[i];
|
|
220
184
|
|
|
221
185
|
// Find the value as a string
|
|
222
|
-
const value =
|
|
223
|
-
req.params[name]
|
|
224
|
-
|| req.query[name]
|
|
225
|
-
|| req.body[name]
|
|
226
|
-
);
|
|
186
|
+
const value = requestBody[name];
|
|
227
187
|
|
|
228
188
|
// Parse
|
|
229
189
|
if (type === ParamType.Boolean || type === ParamType.BooleanOptional) {
|
|
@@ -406,7 +366,7 @@ const genRouteHandler = (
|
|
|
406
366
|
// Not launched
|
|
407
367
|
(!launched || !launchInfo)
|
|
408
368
|
// Not skipping the session check
|
|
409
|
-
&& !
|
|
369
|
+
&& !skipSessionCheck
|
|
410
370
|
) {
|
|
411
371
|
return handleError(
|
|
412
372
|
res,
|
|
@@ -437,7 +397,7 @@ const genRouteHandler = (
|
|
|
437
397
|
)
|
|
438
398
|
)
|
|
439
399
|
// Not skipping the session check
|
|
440
|
-
&& !
|
|
400
|
+
&& !skipSessionCheck
|
|
441
401
|
) {
|
|
442
402
|
return handleError(
|
|
443
403
|
res,
|
|
@@ -1,71 +1,72 @@
|
|
|
1
|
-
// Import
|
|
2
|
-
import
|
|
1
|
+
// Import data signer
|
|
2
|
+
import { createSignedPack } from '../dataSigner';
|
|
3
3
|
|
|
4
4
|
// Import shared types
|
|
5
|
+
import ErrorWithCode from '../../errors/ErrorWithCode';
|
|
5
6
|
import ReactKitErrorCode from '../../types/ReactKitErrorCode';
|
|
6
|
-
|
|
7
|
-
// Import other helpers
|
|
8
7
|
import sendServerToServerRequest from './sendServerToServerRequest';
|
|
9
8
|
|
|
10
9
|
/**
|
|
11
|
-
*
|
|
12
|
-
* dce-reactkit [for server only]
|
|
10
|
+
* Visit an endpoint on another server
|
|
13
11
|
* @author Gabe Abrams
|
|
14
12
|
* @param opts object containing all arguments
|
|
15
|
-
* @param opts.
|
|
16
|
-
* @param opts.path
|
|
17
|
-
* @param
|
|
18
|
-
* @param [opts.
|
|
19
|
-
*
|
|
20
|
-
* @
|
|
13
|
+
* @param opts.method the method of the endpoint
|
|
14
|
+
* @param opts.path the path of the other server's endpoint
|
|
15
|
+
* @param opts.host the host of the other server
|
|
16
|
+
* @param [opts.key=process.env.REACTKIT_CROSS_SERVER_CREDENTIAL_KEY] reactkit cross-server
|
|
17
|
+
* credential key
|
|
18
|
+
* @param [opts.secret=process.env.REACTKIT_CROSS_SERVER_CREDENTIAL_SECRET reactkit cross-server
|
|
19
|
+
* credential secret
|
|
20
|
+
* @param [opts.params={}] query/body parameters to include
|
|
21
|
+
* @param [opts.responseType=JSON] the response type from the other server
|
|
21
22
|
*/
|
|
22
23
|
const visitEndpointOnAnotherServer = async (
|
|
23
24
|
opts: {
|
|
24
|
-
|
|
25
|
+
method: 'GET' | 'POST' | 'DELETE' | 'PUT',
|
|
25
26
|
path: string,
|
|
26
|
-
|
|
27
|
+
host: string,
|
|
28
|
+
key?: string,
|
|
29
|
+
secret?: string,
|
|
27
30
|
params?: { [key in string]: any },
|
|
28
|
-
|
|
31
|
+
responseType?: 'JSON' | 'Text',
|
|
29
32
|
},
|
|
30
33
|
): Promise<any> => {
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
params = Object.fromEntries(
|
|
35
|
-
Object
|
|
36
|
-
.entries(opts.params)
|
|
37
|
-
.filter(([, value]) => {
|
|
38
|
-
return value !== undefined;
|
|
39
|
-
}),
|
|
40
|
-
);
|
|
41
|
-
}
|
|
34
|
+
// Get cross-server credentials
|
|
35
|
+
const key = opts.key ?? process.env.REACTKIT_CROSS_SERVER_KEY;
|
|
36
|
+
const secret = opts.secret ?? process.env.REACTKIT_CROSS_SERVER_SECRET;
|
|
42
37
|
|
|
43
|
-
//
|
|
44
|
-
if (
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
.map(([key, value]) => {
|
|
49
|
-
if (Array.isArray(value) || typeof value === 'object') {
|
|
50
|
-
return [key, JSON.stringify(value)];
|
|
51
|
-
}
|
|
52
|
-
return [key, value];
|
|
53
|
-
}),
|
|
38
|
+
// Throw error if no credentials
|
|
39
|
+
if (!key || !secret) {
|
|
40
|
+
throw new ErrorWithCode(
|
|
41
|
+
'Cannot send cross-server signed request because either or both the key and secret were not included or found in env.',
|
|
42
|
+
ReactKitErrorCode.CrossServerNoCredentialsToSignWith,
|
|
54
43
|
);
|
|
55
44
|
}
|
|
56
45
|
|
|
46
|
+
// Create signed pack
|
|
47
|
+
const signedPack = createSignedPack({
|
|
48
|
+
method: opts.method,
|
|
49
|
+
path: opts.path,
|
|
50
|
+
params: opts.params ?? {},
|
|
51
|
+
key,
|
|
52
|
+
secret,
|
|
53
|
+
});
|
|
54
|
+
|
|
57
55
|
// Send the request
|
|
58
56
|
const response = await sendServerToServerRequest({
|
|
59
|
-
host: opts.host,
|
|
60
57
|
path: opts.path,
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
host: opts.host,
|
|
59
|
+
method: opts.method,
|
|
60
|
+
params: {
|
|
61
|
+
signedPack,
|
|
62
|
+
},
|
|
63
|
+
responseType: opts.responseType,
|
|
63
64
|
});
|
|
64
65
|
|
|
65
66
|
// Check for failure
|
|
66
67
|
if (!response || !response.body) {
|
|
67
68
|
throw new ErrorWithCode(
|
|
68
|
-
'We didn\'t get a response from the server. Please check your internet connection.',
|
|
69
|
+
'We didn\'t get a response from the other server. Please check your internet connection.',
|
|
69
70
|
ReactKitErrorCode.NoResponse,
|
|
70
71
|
);
|
|
71
72
|
}
|
|
@@ -15,9 +15,6 @@ import ErrorWithCode from '../../errors/ErrorWithCode';
|
|
|
15
15
|
* @param [opts.host] host to send request to
|
|
16
16
|
* @param [opts.method=GET] http method to use
|
|
17
17
|
* @param [opts.params] body/data to include in the request
|
|
18
|
-
* @param [opts.headers] headers to include in the request
|
|
19
|
-
* @param [opts.sendCrossDomainCredentials=true if in development mode] if true,
|
|
20
|
-
* send cross-domain credentials even if not in dev mode
|
|
21
18
|
* @param [opts.responseType=JSON] expected response type
|
|
22
19
|
* @returns { body, status, headers } on success
|
|
23
20
|
*/
|
|
@@ -27,7 +24,6 @@ const sendServerToServerRequest = async (
|
|
|
27
24
|
host?: string,
|
|
28
25
|
method?: ('GET' | 'POST' | 'PUT' | 'DELETE'),
|
|
29
26
|
params?: { [k in string]: any },
|
|
30
|
-
headers?: { [k in string]: any },
|
|
31
27
|
responseType?: 'Text' | 'JSON',
|
|
32
28
|
},
|
|
33
29
|
): Promise<{
|
|
@@ -70,7 +66,9 @@ const sendServerToServerRequest = async (
|
|
|
70
66
|
}
|
|
71
67
|
|
|
72
68
|
// Update headers
|
|
73
|
-
const headers
|
|
69
|
+
const headers: {
|
|
70
|
+
[k: string]: any,
|
|
71
|
+
} = {};
|
|
74
72
|
let data: string | null | { [k: string]: any } | undefined = null;
|
|
75
73
|
if (!headers['Content-Type']) {
|
|
76
74
|
// Form encoded
|
package/src/index.ts
CHANGED
|
@@ -70,6 +70,7 @@ import onlyKeepLetters from './helpers/onlyKeepLetters';
|
|
|
70
70
|
import parallelLimit from './helpers/parallelLimit';
|
|
71
71
|
import logClientEvent, { setClientEventMetadataPopulator } from './helpers/logClientEvent';
|
|
72
72
|
import initLogCollection from './server/initLogCollection';
|
|
73
|
+
import initCrossServerCredentialCollection from './server/initCrossServerCredentialCollection';
|
|
73
74
|
import getMonthName from './helpers/getMonthName';
|
|
74
75
|
import genCSV from './helpers/genCSV';
|
|
75
76
|
import canReviewLogs from './helpers/canReviewLogs';
|
|
@@ -95,7 +96,6 @@ import someAsync from './helpers/asyncArrayFunctions/someAsync';
|
|
|
95
96
|
import capitalize from './helpers/capitalize';
|
|
96
97
|
import shuffleArray from './helpers/shuffleArray';
|
|
97
98
|
import visitEndpointOnAnotherServer from './helpers/visitEndpointOnAnotherServer';
|
|
98
|
-
import getWordCount from './helpers/getWordCount';
|
|
99
99
|
|
|
100
100
|
// Import types
|
|
101
101
|
import ModalButtonType from './types/ModalButtonType';
|
|
@@ -114,6 +114,7 @@ import LogMetadataType from './types/LogMetadataType';
|
|
|
114
114
|
import LogFunction from './types/LogFunction';
|
|
115
115
|
import IntelliTableColumn from './types/IntelliTableColumn';
|
|
116
116
|
import DropdownItemType from './types/DropdownItemType';
|
|
117
|
+
import CrossServerCredential from './types/CrossServerCredential';
|
|
117
118
|
|
|
118
119
|
// Component-specific-types
|
|
119
120
|
import PickableItem from './components/ItemPicker/types/PickableItem';
|
|
@@ -202,7 +203,6 @@ export {
|
|
|
202
203
|
someAsync,
|
|
203
204
|
capitalize,
|
|
204
205
|
shuffleArray,
|
|
205
|
-
getWordCount,
|
|
206
206
|
// Client helpers
|
|
207
207
|
initClient,
|
|
208
208
|
visitServerEndpoint,
|
|
@@ -218,6 +218,7 @@ export {
|
|
|
218
218
|
handleError,
|
|
219
219
|
handleSuccess,
|
|
220
220
|
initLogCollection,
|
|
221
|
+
initCrossServerCredentialCollection,
|
|
221
222
|
addDBEditorEndpoints,
|
|
222
223
|
visitEndpointOnAnotherServer,
|
|
223
224
|
// Types
|
|
@@ -236,6 +237,7 @@ export {
|
|
|
236
237
|
LogFunction,
|
|
237
238
|
IntelliTableColumn,
|
|
238
239
|
DropdownItemType,
|
|
240
|
+
CrossServerCredential,
|
|
239
241
|
// Component-specific-types
|
|
240
242
|
PickableItem,
|
|
241
243
|
DBEntry,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Initialize a cross-server credential collection given the dce-mango Collection class
|
|
3
|
+
* @author Gabe Abrams
|
|
4
|
+
* @param Collection the Collection class from dce-mango
|
|
5
|
+
* @returns initialized logCollection
|
|
6
|
+
*/
|
|
7
|
+
const initCrossServerCredentialCollection = (Collection: any) => {
|
|
8
|
+
return new Collection(
|
|
9
|
+
'CrossServerCredential',
|
|
10
|
+
{
|
|
11
|
+
uniqueIndexKey: 'key',
|
|
12
|
+
},
|
|
13
|
+
);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default initCrossServerCredentialCollection;
|