dce-expresskit 4.0.0-beta.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.
Files changed (94) hide show
  1. package/.eslintrc.js +93 -0
  2. package/LICENSE +21 -0
  3. package/README.md +17 -0
  4. package/genEncodedSecret.ts +84 -0
  5. package/lib/constants/LOG_REVIEW_ROUTE_PATH_PREFIX.d.ts +6 -0
  6. package/lib/constants/LOG_REVIEW_ROUTE_PATH_PREFIX.js +13 -0
  7. package/lib/constants/LOG_REVIEW_ROUTE_PATH_PREFIX.js.map +1 -0
  8. package/lib/constants/LOG_REVIEW_STATUS_ROUTE.d.ts +7 -0
  9. package/lib/constants/LOG_REVIEW_STATUS_ROUTE.js +14 -0
  10. package/lib/constants/LOG_REVIEW_STATUS_ROUTE.js.map +1 -0
  11. package/lib/constants/LOG_ROUTE_PATH.d.ts +6 -0
  12. package/lib/constants/LOG_ROUTE_PATH.js +13 -0
  13. package/lib/constants/LOG_ROUTE_PATH.js.map +1 -0
  14. package/lib/constants/ROUTE_PATH_PREFIX.d.ts +6 -0
  15. package/lib/constants/ROUTE_PATH_PREFIX.js +9 -0
  16. package/lib/constants/ROUTE_PATH_PREFIX.js.map +1 -0
  17. package/lib/errors/ErrorWithCode.d.ts +9 -0
  18. package/lib/errors/ErrorWithCode.js +33 -0
  19. package/lib/errors/ErrorWithCode.js.map +1 -0
  20. package/lib/helpers/addDBEditorEndpoints/generateEndpointPath.d.ts +9 -0
  21. package/lib/helpers/addDBEditorEndpoints/generateEndpointPath.js +17 -0
  22. package/lib/helpers/addDBEditorEndpoints/generateEndpointPath.js.map +1 -0
  23. package/lib/helpers/addDBEditorEndpoints/index.d.ts +41 -0
  24. package/lib/helpers/addDBEditorEndpoints/index.js +134 -0
  25. package/lib/helpers/addDBEditorEndpoints/index.js.map +1 -0
  26. package/lib/helpers/dataSigner.d.ts +40 -0
  27. package/lib/helpers/dataSigner.js +231 -0
  28. package/lib/helpers/dataSigner.js.map +1 -0
  29. package/lib/helpers/genRouteHandler.d.ts +75 -0
  30. package/lib/helpers/genRouteHandler.js +661 -0
  31. package/lib/helpers/genRouteHandler.js.map +1 -0
  32. package/lib/helpers/handleError.d.ts +18 -0
  33. package/lib/helpers/handleError.js +51 -0
  34. package/lib/helpers/handleError.js.map +1 -0
  35. package/lib/helpers/handleSuccess.d.ts +8 -0
  36. package/lib/helpers/handleSuccess.js +20 -0
  37. package/lib/helpers/handleSuccess.js.map +1 -0
  38. package/lib/helpers/initCrossServerCredentialCollection.d.ts +11 -0
  39. package/lib/helpers/initCrossServerCredentialCollection.js +15 -0
  40. package/lib/helpers/initCrossServerCredentialCollection.js.map +1 -0
  41. package/lib/helpers/initLogCollection.d.ts +11 -0
  42. package/lib/helpers/initLogCollection.js +26 -0
  43. package/lib/helpers/initLogCollection.js.map +1 -0
  44. package/lib/helpers/initServer.d.ts +43 -0
  45. package/lib/helpers/initServer.js +297 -0
  46. package/lib/helpers/initServer.js.map +1 -0
  47. package/lib/helpers/parseUserAgent.d.ts +17 -0
  48. package/lib/helpers/parseUserAgent.js +108 -0
  49. package/lib/helpers/parseUserAgent.js.map +1 -0
  50. package/lib/helpers/visitEndpointOnAnotherServer/index.d.ts +18 -0
  51. package/lib/helpers/visitEndpointOnAnotherServer/index.js +156 -0
  52. package/lib/helpers/visitEndpointOnAnotherServer/index.js.map +1 -0
  53. package/lib/helpers/visitEndpointOnAnotherServer/sendServerToServerRequest.d.ts +23 -0
  54. package/lib/helpers/visitEndpointOnAnotherServer/sendServerToServerRequest.js +168 -0
  55. package/lib/helpers/visitEndpointOnAnotherServer/sendServerToServerRequest.js.map +1 -0
  56. package/lib/html/genErrorPage.d.ts +19 -0
  57. package/lib/html/genErrorPage.js +27 -0
  58. package/lib/html/genErrorPage.js.map +1 -0
  59. package/lib/html/genInfoPage.d.ts +13 -0
  60. package/lib/html/genInfoPage.js +16 -0
  61. package/lib/html/genInfoPage.js.map +1 -0
  62. package/lib/index.d.ts +11 -0
  63. package/lib/index.js +68 -0
  64. package/lib/index.js.map +1 -0
  65. package/lib/types/CrossServerCredential.d.ts +11 -0
  66. package/lib/types/CrossServerCredential.js +3 -0
  67. package/lib/types/CrossServerCredential.js.map +1 -0
  68. package/lib/types/ExpressKitErrorCode.d.ts +31 -0
  69. package/lib/types/ExpressKitErrorCode.js +38 -0
  70. package/lib/types/ExpressKitErrorCode.js.map +1 -0
  71. package/package.json +52 -0
  72. package/src/constants/LOG_REVIEW_ROUTE_PATH_PREFIX.ts +9 -0
  73. package/src/constants/LOG_REVIEW_STATUS_ROUTE.ts +10 -0
  74. package/src/constants/LOG_ROUTE_PATH.ts +9 -0
  75. package/src/constants/ROUTE_PATH_PREFIX.ts +7 -0
  76. package/src/errors/ErrorWithCode.tsx +15 -0
  77. package/src/helpers/addDBEditorEndpoints/generateEndpointPath.ts +16 -0
  78. package/src/helpers/addDBEditorEndpoints/index.ts +130 -0
  79. package/src/helpers/dataSigner.ts +296 -0
  80. package/src/helpers/genRouteHandler.ts +914 -0
  81. package/src/helpers/handleError.ts +66 -0
  82. package/src/helpers/handleSuccess.ts +18 -0
  83. package/src/helpers/initCrossServerCredentialCollection.ts +19 -0
  84. package/src/helpers/initLogCollection.ts +31 -0
  85. package/src/helpers/initServer.ts +284 -0
  86. package/src/helpers/parseUserAgent.ts +108 -0
  87. package/src/helpers/visitEndpointOnAnotherServer/index.ts +157 -0
  88. package/src/helpers/visitEndpointOnAnotherServer/sendServerToServerRequest.ts +164 -0
  89. package/src/html/genErrorPage.ts +144 -0
  90. package/src/html/genInfoPage.ts +101 -0
  91. package/src/index.ts +125 -0
  92. package/src/types/CrossServerCredential.ts +16 -0
  93. package/src/types/ExpressKitErrorCode.ts +37 -0
  94. package/tsconfig.json +19 -0
@@ -0,0 +1,66 @@
1
+ // Import shared types
2
+ import { ReactKitErrorCode } from 'dce-reactkit';
3
+ import ExpressKitErrorCode from '../types/ExpressKitErrorCode';
4
+
5
+ /**
6
+ * Handle an error and respond to the client
7
+ * @author Gabe Abrams
8
+ * @param res express response
9
+ * @param error error info
10
+ * @param opts.err the error to send to the client
11
+ * or the error message
12
+ * @param [opts.code] an error code (only used if err.code is not
13
+ * included)
14
+ * @param [opts.status=500] the https status code to use
15
+ * defined)
16
+ */
17
+ const handleError = (
18
+ res: any,
19
+ error: (
20
+ | {
21
+ message: any,
22
+ code?: string,
23
+ status?: number,
24
+ }
25
+ | Error
26
+ | string
27
+ | any
28
+ ),
29
+ ): undefined => {
30
+ // Get the error message
31
+ let message;
32
+ if (error && (error as any).message) {
33
+ message = (error.message || 'An unknown error occurred.');
34
+ } else if (typeof error === 'string') {
35
+ message = (
36
+ error.trim().length > 0
37
+ ? error
38
+ : 'An unknown error occurred.'
39
+ );
40
+ } else {
41
+ message = 'An unknown error occurred.';
42
+ }
43
+
44
+ // Get the error code
45
+ const code = (error.code || ReactKitErrorCode.NoCode);
46
+
47
+ // Get the status code
48
+ const status = (error.status || 500);
49
+
50
+ // Respond to user
51
+ res
52
+ // Set the http status code
53
+ .status(status)
54
+ // Send a JSON response
55
+ .json({
56
+ // Error message
57
+ message,
58
+ // Error code
59
+ code,
60
+ // Success = false flag so client can detect server-side errors
61
+ success: false,
62
+ });
63
+ return undefined;
64
+ };
65
+
66
+ export default handleError;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Send successful API response
3
+ * @author Gabe Abrams
4
+ * @param res express response
5
+ * @param body the body of the response to send to the client
6
+ */
7
+ const handleSuccess = (res: any, body: any): undefined => {
8
+ // Send a http 200 json response
9
+ res.json({
10
+ // Include the body as a parameter
11
+ body,
12
+ // Success = true flag so client can detect successful responses
13
+ success: true,
14
+ });
15
+ return undefined;
16
+ };
17
+
18
+ export default handleSuccess;
@@ -0,0 +1,19 @@
1
+ // Import dce-mango
2
+ import { Collection as MangoCollection } from 'dce-mango';
3
+
4
+ /**
5
+ * Initialize a cross-server credential collection given the dce-mango Collection class
6
+ * @author Gabe Abrams
7
+ * @param Collection the Collection class from dce-mango
8
+ * @returns initialized logCollection
9
+ */
10
+ const initCrossServerCredentialCollection = (Collection: typeof MangoCollection) => {
11
+ return new Collection(
12
+ 'CrossServerCredential',
13
+ {
14
+ uniqueIndexKey: 'key',
15
+ },
16
+ );
17
+ };
18
+
19
+ export default initCrossServerCredentialCollection;
@@ -0,0 +1,31 @@
1
+ // Import dce-mango
2
+ import { Collection as MangoCollection } from 'dce-mango';
3
+
4
+
5
+ /**
6
+ * Initialize a log collection given the dce-mango Collection class
7
+ * @author Gabe Abrams
8
+ * @param Collection the Collection class from dce-mango
9
+ * @returns initialized logCollection
10
+ */
11
+ const initLogCollection = (Collection: typeof MangoCollection) => {
12
+ return new Collection(
13
+ 'Log',
14
+ {
15
+ uniqueIndexKey: 'id',
16
+ indexKeys: [
17
+ 'courseId',
18
+ 'context',
19
+ 'subcontext',
20
+ 'tags',
21
+ 'year',
22
+ 'month',
23
+ 'day',
24
+ 'hour',
25
+ 'type',
26
+ ],
27
+ },
28
+ );
29
+ };
30
+
31
+ export default initLogCollection;
@@ -0,0 +1,284 @@
1
+ // Import express
2
+ import express from 'express';
3
+
4
+ // Import dce-mango
5
+ import { Collection } from 'dce-mango';
6
+
7
+ // Import dce-reactkit
8
+ import {
9
+ ErrorWithCode,
10
+ ParamType,
11
+ LogFunction,
12
+ } from 'dce-reactkit';
13
+
14
+ // Import internal constants of dce-reactkit
15
+ import LOG_REVIEW_ROUTE_PATH_PREFIX from 'dce-reactkit/src/constants/LOG_REVIEW_ROUTE_PATH_PREFIX';
16
+ import LOG_ROUTE_PATH from 'dce-reactkit/src/constants/LOG_ROUTE_PATH';
17
+ import LOG_REVIEW_STATUS_ROUTE from 'dce-reactkit/src/constants/LOG_REVIEW_STATUS_ROUTE';
18
+
19
+ // Import shared helpers
20
+ import genRouteHandler from './genRouteHandler';
21
+
22
+ // Import shared types
23
+ import ExpressKitErrorCode from '../types/ExpressKitErrorCode';
24
+
25
+ // Stored copy of dce-mango log collection
26
+ let _logCollection: any;
27
+
28
+ // Stored copy of dce-mango cross-server credential collection
29
+ let _crossServerCredentialCollection: any;
30
+
31
+ /*------------------------------------------------------------------------*/
32
+ /* Helpers */
33
+ /*------------------------------------------------------------------------*/
34
+
35
+ /**
36
+ * Get log collection
37
+ * @author Gabe Abrams
38
+ * @returns log collection if one was included during launch or null if we don't
39
+ * have a log collection (yet)
40
+ */
41
+ export const internalGetLogCollection = () => {
42
+ return _logCollection ?? null;
43
+ };
44
+
45
+ /**
46
+ * Get cross-server credential collection
47
+ * @author Gabe Abrams
48
+ * @return cross-server credential collection if one was included during launch or null
49
+ * if we don't have a cross-server credential collection (yet)
50
+ */
51
+ export const internalGetCrossServerCredentialCollection = () => {
52
+ return _crossServerCredentialCollection ?? null;
53
+ };
54
+
55
+ /*------------------------------------------------------------------------*/
56
+ /* Main */
57
+ /*------------------------------------------------------------------------*/
58
+
59
+ /**
60
+ * Prepare dce-reactkit to run on the server
61
+ * @author Gabe Abrams
62
+ * @param opts object containing all arguments
63
+ * @param opts.app express app from inside of the postprocessor function that
64
+ * we will add routes to
65
+ * @param opts.getLaunchInfo CACCL LTI's get launch info function
66
+ * @param [opts.logCollection] mongo collection from dce-mango to use for
67
+ * storing logs. If none is included, logs are written to the console
68
+ * @param [opts.logReviewAdmins=all] info on which admins can review
69
+ * logs from the client. If not included, all Canvas admins are allowed to
70
+ * review logs. If null, no Canvas admins are allowed to review logs.
71
+ * If an array of Canvas userIds (numbers), only Canvas admins with those
72
+ * userIds are allowed to review logs. If a dce-mango collection, only
73
+ * Canvas admins with entries in that collection ({ userId, ...}) are allowed
74
+ * to review logs
75
+ * @param [opts.crossServerCredentialCollection] mongo collection from dce-mango to use for
76
+ * storing cross-server credentials. If none is included, cross-server credentials
77
+ * are not supported
78
+ */
79
+ const initServer = (
80
+ opts: {
81
+ app: express.Application,
82
+ logReviewAdmins?: (number[] | Collection<any>),
83
+ logCollection?: Collection<any>,
84
+ crossServerCredentialCollection?: Collection<any>,
85
+ },
86
+ ) => {
87
+ _logCollection = opts.logCollection;
88
+ _crossServerCredentialCollection = opts.crossServerCredentialCollection;
89
+
90
+ /*----------------------------------------*/
91
+ /* Logging */
92
+ /*----------------------------------------*/
93
+
94
+ /**
95
+ * Log an event
96
+ * @author Gabe Abrams
97
+ * @param {string} context Context of the event (each app determines how to
98
+ * organize its contexts)
99
+ * @param {string} subcontext Subcontext of the event (each app determines
100
+ * how to organize its subcontexts)
101
+ * @param {string} tags stringified list of tags that apply to this action
102
+ * (each app determines tag usage)
103
+ * @param {string} metadata stringified object containing optional custom metadata
104
+ * @param {string} level log level
105
+ * @param {string} [errorMessage] error message if type is an error
106
+ * @param {string} [errorCode] error code if type is an error
107
+ * @param {string} [errorStack] error stack if type is an error
108
+ * @param {string} [target] Target of the action (each app determines the list
109
+ * of targets) These are usually buttons, panels, elements, etc.
110
+ * @param {LogAction} [action] the type of action performed on the target
111
+ * @returns {Log}
112
+ */
113
+ opts.app.post(
114
+ LOG_ROUTE_PATH,
115
+ genRouteHandler({
116
+ paramTypes: {
117
+ context: ParamType.String,
118
+ subcontext: ParamType.String,
119
+ tags: ParamType.JSON,
120
+ level: ParamType.String,
121
+ metadata: ParamType.JSON,
122
+ errorMessage: ParamType.StringOptional,
123
+ errorCode: ParamType.StringOptional,
124
+ errorStack: ParamType.StringOptional,
125
+ target: ParamType.StringOptional,
126
+ action: ParamType.StringOptional,
127
+ },
128
+ handler: ({ params, logServerEvent }) => {
129
+ // Create log info
130
+ const logInfo: Parameters<LogFunction>[0] = (
131
+ (params.errorMessage || params.errorCode || params.errorStack)
132
+ // Error
133
+ ? {
134
+ context: params.context,
135
+ subcontext: params.subcontext,
136
+ tags: params.tags,
137
+ level: params.level,
138
+ metadata: params.metadata,
139
+ error: {
140
+ message: params.errorMessage,
141
+ code: params.errorCode,
142
+ stack: params.errorStack,
143
+ },
144
+ }
145
+ // Action
146
+ : {
147
+ context: params.context,
148
+ subcontext: params.subcontext,
149
+ tags: params.tags,
150
+ level: params.level,
151
+ metadata: params.metadata,
152
+ target: params.target,
153
+ action: params.action,
154
+ }
155
+ );
156
+
157
+ // Add hidden boolean to change source to "client"
158
+ const logInfoForcedFromClient = {
159
+ ...logInfo,
160
+ overrideAsClientEvent: true,
161
+ };
162
+
163
+ // Write the log
164
+ const log = logServerEvent(logInfoForcedFromClient);
165
+
166
+ // Return
167
+ return log;
168
+ },
169
+ }),
170
+ );
171
+
172
+ /*----------------------------------------*/
173
+ /* Log Reviewer */
174
+ /*----------------------------------------*/
175
+
176
+ /**
177
+ * Check if a given user is allowed to review logs
178
+ * @author Gabe Abrams
179
+ * @param userId the id of the user
180
+ * @param isAdmin if true, the user is an admin
181
+ * @returns true if the user can review logs
182
+ */
183
+ const canReviewLogs = async (
184
+ userId: number,
185
+ isAdmin: boolean,
186
+ ): Promise<boolean> => {
187
+ // Immediately deny access if user is not an admin
188
+ if (!isAdmin) {
189
+ return false;
190
+ }
191
+
192
+ // If all admins are allowed, we're done
193
+ if (!opts.logReviewAdmins) {
194
+ return true;
195
+ }
196
+
197
+ // Do a dynamic check
198
+ try {
199
+ // Array of userIds
200
+ if (Array.isArray(opts.logReviewAdmins)) {
201
+ return opts.logReviewAdmins.some((allowedId) => {
202
+ return (userId === allowedId);
203
+ });
204
+ }
205
+
206
+ // Must be a collection
207
+ const matches = await opts.logReviewAdmins.find({ userId });
208
+
209
+ // Make sure at least one entry matches
210
+ return matches.length > 0;
211
+ } catch (err) {
212
+ // If an error occurred, simply return false
213
+ return false;
214
+ }
215
+ };
216
+
217
+ /**
218
+ * Check if the current user has access to logs
219
+ * @author Gabe Abrams
220
+ * @returns {boolean} true if user has access
221
+ */
222
+ opts.app.get(
223
+ LOG_REVIEW_STATUS_ROUTE,
224
+ genRouteHandler({
225
+ handler: async ({ params }) => {
226
+ const { userId, isAdmin } = params;
227
+ const canReview = await canReviewLogs(userId, isAdmin);
228
+ return canReview;
229
+ },
230
+ }),
231
+ );
232
+
233
+ /**
234
+ * Get all logs for a certain month
235
+ * @author Gabe Abrams
236
+ * @param {number} year the year to query (e.g. 2022)
237
+ * @param {number} month the month to query (e.g. 1 = January)
238
+ * @returns {Log[]} list of logs from the given month
239
+ */
240
+ opts.app.get(
241
+ `${LOG_REVIEW_ROUTE_PATH_PREFIX}/years/:year/months/:month`,
242
+ genRouteHandler({
243
+ paramTypes: {
244
+ year: ParamType.Int,
245
+ month: ParamType.Int,
246
+ pageNumber: ParamType.Int,
247
+ },
248
+ handler: async ({ params }) => {
249
+ // Get user info
250
+ const {
251
+ year,
252
+ month,
253
+ pageNumber,
254
+ userId,
255
+ isAdmin,
256
+ } = params;
257
+
258
+ // Validate user
259
+ const canReview = await canReviewLogs(userId, isAdmin);
260
+ if (!canReview) {
261
+ throw new ErrorWithCode(
262
+ 'You cannot access this resource because you do not have the appropriate permissions.',
263
+ ExpressKitErrorCode.NotAllowedToReviewLogs,
264
+ );
265
+ }
266
+
267
+ // Query for logs
268
+ const response = await _logCollection.findPaged({
269
+ query: {
270
+ year,
271
+ month,
272
+ },
273
+ perPage: 1000,
274
+ pageNumber,
275
+ });
276
+
277
+ // Return response
278
+ return response;
279
+ },
280
+ }),
281
+ );
282
+ };
283
+
284
+ export default initServer;
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Perform a rudimentary parsing of the user's browser agent string
3
+ * @author Gabe Abrams
4
+ * @param userAgent the user's browser agent
5
+ * @returns user info
6
+ */
7
+ const parseUserAgent = (userAgent: string) => {
8
+ /* ------------- Browser ------------ */
9
+
10
+ let browser: { name: string, version: string } = {
11
+ name: 'Unknown',
12
+ version: 'Unknown',
13
+ };
14
+
15
+ // Parse user agent
16
+ let verOffset: number;
17
+ let nameOffset: number;
18
+ if ((verOffset = userAgent.indexOf('Opera')) !== -1) {
19
+ // In Opera, the true version is after 'Opera' or after 'Version'
20
+ browser = {
21
+ name: 'Opera',
22
+ version: userAgent.substring(verOffset + 6),
23
+ };
24
+ if ((verOffset = userAgent.indexOf('Version')) !== -1) {
25
+ browser.version = userAgent.substring(verOffset + 8);
26
+ }
27
+ } else if ((verOffset = userAgent.indexOf('MSIE')) !== -1) {
28
+ // In MSIE, the true version is after 'MSIE' in userAgent
29
+ browser = {
30
+ name: 'Internet Explorer',
31
+ version: userAgent.substring(verOffset + 5),
32
+ };
33
+ } else if ((verOffset = userAgent.indexOf('Chrome')) !== -1) {
34
+ // In Chrome, the true version is after 'Chrome'
35
+ browser = {
36
+ name: 'Chrome',
37
+ version: userAgent.substring(verOffset + 7),
38
+ };
39
+ } else if ((verOffset = userAgent.indexOf('Safari')) !== -1) {
40
+ // In Safari, the true version is after 'Safari' or after 'Version'
41
+ browser = {
42
+ name: 'Safari',
43
+ version: userAgent.substring(verOffset + 7),
44
+ };
45
+ if ((verOffset = userAgent.indexOf('Version')) !== -1) {
46
+ browser.version = userAgent.substring(verOffset + 8);
47
+ }
48
+ } else if ((verOffset = userAgent.indexOf('Firefox')) != -1) {
49
+ // In Firefox, the true version is after 'Firefox'
50
+ browser = {
51
+ name: 'Firefox',
52
+ version: userAgent.substring(verOffset + 8),
53
+ };
54
+ } else if (
55
+ (nameOffset = userAgent.lastIndexOf(' ') + 1)
56
+ < (verOffset = userAgent.lastIndexOf('/'))
57
+ ) {
58
+ browser = {
59
+ name: userAgent.substring(nameOffset, verOffset),
60
+ version: userAgent.substring(verOffset + 1),
61
+ };
62
+ }
63
+
64
+ // Postprocess version
65
+ // trim the fullVersion string at semicolon/space if present
66
+ let ix: number;
67
+ if ((ix = browser.version.indexOf(';')) !== -1) {
68
+ browser.version = browser.version.substring(0, ix);
69
+ }
70
+ if ((ix = browser.version.indexOf(' ')) !== -1) {
71
+ browser.version = browser.version.substring(0, ix);
72
+ }
73
+
74
+ /* ------------- Device ------------- */
75
+
76
+ // Detect os
77
+ let os = 'Unknown';
78
+ if (userAgent.includes('Linux')) {
79
+ os = 'Linux';
80
+ } else if (userAgent.includes('like Mac')) {
81
+ os = 'iOS';
82
+ } else if (userAgent.includes('Mac')) {
83
+ os = 'Mac';
84
+ } else if (userAgent.includes('Android')) {
85
+ os = 'Android';
86
+ } else if (userAgent.includes('Win')) {
87
+ os = 'Win';
88
+ }
89
+
90
+ // Check if mobile
91
+ const isMobile = !!userAgent.match(/(iPhone|iPod|iPad|Android|BlackBerry)/);
92
+
93
+ // Device
94
+ const device = {
95
+ isMobile,
96
+ os,
97
+ };
98
+
99
+ /* ------------- Finish ------------- */
100
+
101
+ // Return info
102
+ return {
103
+ browser,
104
+ device,
105
+ };
106
+ };
107
+
108
+ export default parseUserAgent;
@@ -0,0 +1,157 @@
1
+ // Import dce-reactkit
2
+ import {
3
+ ErrorWithCode,
4
+ ReactKitErrorCode,
5
+ } from 'dce-reactkit';
6
+
7
+ // Import data signer
8
+ import { signRequest } from '../dataSigner';
9
+
10
+ // Import shared types
11
+ import ExpressKitErrorCode from '../../types/ExpressKitErrorCode';
12
+ import sendServerToServerRequest from './sendServerToServerRequest';
13
+
14
+ /*------------------------------------------------------------------------*/
15
+ /* ----------------------------- Credentials ---------------------------- */
16
+ /*------------------------------------------------------------------------*/
17
+
18
+ /*
19
+ DCEKIT_CROSS_SERVER_CREDENTIALS format:
20
+ |host:key:secret||host:key:secret|...
21
+ */
22
+
23
+ const credentials: {
24
+ host: string,
25
+ key: string,
26
+ secret: string,
27
+ }[] = (
28
+ (process.env.DCEKIT_CROSS_SERVER_CREDENTIALS ?? '')
29
+ // Replace multiple | with a single one
30
+ .replace(/\|+/g, '|')
31
+ // Split by |
32
+ .split('|')
33
+ // Remove empty strings
34
+ .filter((str) => {
35
+ return str.trim().length > 0;
36
+ })
37
+ // Process each credential
38
+ .map((str) => {
39
+ // Split by :
40
+ const parts = str.split(':');
41
+
42
+ // Check for errors
43
+ if (parts.length !== 3) {
44
+ throw new ErrorWithCode(
45
+ 'Invalid DCEKIT_CROSS_SERVER_CREDENTIALS format. Each credential must be in the format |host:key:secret|',
46
+ ExpressKitErrorCode.InvalidCrossServerCredentialsFormat,
47
+ );
48
+ }
49
+
50
+ // Return the credential
51
+ return {
52
+ host: parts[0].trim(),
53
+ key: parts[1].trim(),
54
+ secret: parts[2].trim(),
55
+ };
56
+ })
57
+ );
58
+
59
+ /*------------------------------------------------------------------------*/
60
+ /* ------------------------------- Helpers ------------------------------ */
61
+ /*------------------------------------------------------------------------*/
62
+
63
+ /**
64
+ * Get the credential to use for the request to another server
65
+ * @author Gabe Abrams
66
+ * @param host the host of the other server
67
+ * @return the credential to use
68
+ */
69
+ const getCrossServerCredential = (host: string) => {
70
+ // Find the credential
71
+ const credential = credentials.find((cred) => {
72
+ return cred.host.toLowerCase() === host.toLowerCase();
73
+ });
74
+ if (!credential) {
75
+ throw new ErrorWithCode(
76
+ 'Cannot send cross-server signed request there was no credential that matched the host that the request is being sent to.',
77
+ ExpressKitErrorCode.CrossServerNoCredentialsToSignWith,
78
+ );
79
+ }
80
+
81
+ // Return credential
82
+ return credential;
83
+ };
84
+
85
+ /*------------------------------------------------------------------------*/
86
+ /* -------------------------------- Main -------------------------------- */
87
+ /*------------------------------------------------------------------------*/
88
+
89
+ /**
90
+ * Visit an endpoint on another server
91
+ * @author Gabe Abrams
92
+ * @param opts object containing all arguments
93
+ * @param opts.method the method of the endpoint
94
+ * @param opts.path the path of the other server's endpoint
95
+ * @param opts.host the host of the other server
96
+ * @param [opts.params={}] query/body parameters to include
97
+ * @param [opts.responseType=JSON] the response type from the other server
98
+ */
99
+ const visitEndpointOnAnotherServer = async (
100
+ opts: {
101
+ method: 'GET' | 'POST' | 'DELETE' | 'PUT',
102
+ path: string,
103
+ host: string,
104
+ params?: { [key in string]: any },
105
+ responseType?: 'JSON' | 'Text',
106
+ },
107
+ ): Promise<any> => {
108
+ // Get cross-server credential
109
+ const credential = getCrossServerCredential(opts.host);
110
+
111
+ // Sign the request, get new params
112
+ const augmentedParams = await signRequest({
113
+ method: opts.method,
114
+ path: opts.path,
115
+ params: opts.params ?? {},
116
+ key: credential.key,
117
+ secret: credential.secret,
118
+ });
119
+
120
+ // Send the request
121
+ const response = await sendServerToServerRequest({
122
+ path: opts.path,
123
+ host: opts.host,
124
+ method: opts.method,
125
+ params: augmentedParams,
126
+ responseType: opts.responseType,
127
+ });
128
+
129
+ // Check for failure
130
+ if (!response || !response.body) {
131
+ throw new ErrorWithCode(
132
+ 'We didn\'t get a response from the other server. Please check the network between the two connection.',
133
+ ReactKitErrorCode.NoResponse,
134
+ );
135
+ }
136
+ if (!response.body.success) {
137
+ // Other errors
138
+ throw new ErrorWithCode(
139
+ (
140
+ response.body.message
141
+ || 'An unknown error occurred. Please contact an admin.'
142
+ ),
143
+ (
144
+ response.body.code
145
+ || ReactKitErrorCode.NoCode
146
+ ),
147
+ );
148
+ }
149
+
150
+ // Success! Extract the body
151
+ const { body } = response.body;
152
+
153
+ // Return
154
+ return body;
155
+ };
156
+
157
+ export default visitEndpointOnAnotherServer;