dce-reactkit 3.12.3 → 4.0.0-beta.1

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 (71) hide show
  1. package/.vscode/settings.json +1 -3
  2. package/README.md +20 -0
  3. package/dist/cjs/index.js +156 -1792
  4. package/dist/cjs/index.js.map +1 -1
  5. package/dist/cjs/types/components/ItemPicker/index.d.ts +0 -1
  6. package/dist/cjs/types/components/Modal/ModalProps.d.ts +0 -2
  7. package/dist/cjs/types/components/TabBox.d.ts +0 -1
  8. package/dist/cjs/types/index.d.ts +8 -12
  9. package/dist/cjs/types/types/ParamType.d.ts +10 -10
  10. package/dist/cjs/types/types/ReactKitErrorCode.d.ts +1 -15
  11. package/dist/esm/index.js +157 -1785
  12. package/dist/esm/index.js.map +1 -1
  13. package/dist/esm/types/components/ItemPicker/index.d.ts +0 -1
  14. package/dist/esm/types/components/Modal/ModalProps.d.ts +0 -2
  15. package/dist/esm/types/components/TabBox.d.ts +0 -1
  16. package/dist/esm/types/index.d.ts +8 -12
  17. package/dist/esm/types/types/ParamType.d.ts +10 -10
  18. package/dist/esm/types/types/ReactKitErrorCode.d.ts +1 -15
  19. package/dist/index.d.ts +122 -351
  20. package/genEncodedSecret.ts +84 -0
  21. package/package.json +7 -5
  22. package/rollup.config.js +2 -0
  23. package/src/components/AppWrapper.tsx +3 -1
  24. package/src/components/CheckboxButton.tsx +1 -1
  25. package/src/components/Dropdown.tsx +1 -1
  26. package/src/components/IntelliTable.tsx +1 -1
  27. package/src/components/ItemPicker/NestableItemList.tsx +0 -1
  28. package/src/components/ItemPicker/index.tsx +0 -96
  29. package/src/components/Modal/ModalProps.ts +0 -4
  30. package/src/components/Modal/index.tsx +21 -60
  31. package/src/components/RadioButton.tsx +3 -1
  32. package/src/components/TabBox.tsx +9 -27
  33. package/src/{client → helpers}/initClient.tsx +5 -1
  34. package/src/helpers/logClientEvent.tsx +3 -1
  35. package/src/helpers/visitServerEndpoint.tsx +1 -1
  36. package/src/index.ts +12 -22
  37. package/src/types/ParamType.ts +10 -10
  38. package/src/types/ReactKitErrorCode.tsx +1 -17
  39. package/dist/cjs/types/helpers/addDBEditorEndpoints.d.ts +0 -41
  40. package/dist/cjs/types/helpers/genRouteHandler.d.ts +0 -76
  41. package/dist/cjs/types/helpers/getWordCount.d.ts +0 -10
  42. package/dist/cjs/types/helpers/handleError.d.ts +0 -18
  43. package/dist/cjs/types/helpers/handleSuccess.d.ts +0 -8
  44. package/dist/cjs/types/helpers/parseUserAgent.d.ts +0 -17
  45. package/dist/cjs/types/helpers/visitEndpointOnAnotherServer/index.d.ts +0 -24
  46. package/dist/cjs/types/helpers/visitEndpointOnAnotherServer/sendServerToServerRequest.d.ts +0 -33
  47. package/dist/cjs/types/server/initLogCollection.d.ts +0 -8
  48. package/dist/cjs/types/server/initServer.d.ts +0 -42
  49. package/dist/esm/types/helpers/addDBEditorEndpoints.d.ts +0 -41
  50. package/dist/esm/types/helpers/genRouteHandler.d.ts +0 -76
  51. package/dist/esm/types/helpers/getWordCount.d.ts +0 -10
  52. package/dist/esm/types/helpers/handleError.d.ts +0 -18
  53. package/dist/esm/types/helpers/handleSuccess.d.ts +0 -8
  54. package/dist/esm/types/helpers/parseUserAgent.d.ts +0 -17
  55. package/dist/esm/types/helpers/visitEndpointOnAnotherServer/index.d.ts +0 -24
  56. package/dist/esm/types/helpers/visitEndpointOnAnotherServer/sendServerToServerRequest.d.ts +0 -33
  57. package/dist/esm/types/server/initLogCollection.d.ts +0 -8
  58. package/dist/esm/types/server/initServer.d.ts +0 -42
  59. package/src/components/ItemPicker/index.test.tsx +0 -783
  60. package/src/helpers/addDBEditorEndpoints.ts +0 -128
  61. package/src/helpers/genRouteHandler.ts +0 -948
  62. package/src/helpers/getWordCount.ts +0 -26
  63. package/src/helpers/handleError.ts +0 -65
  64. package/src/helpers/handleSuccess.ts +0 -18
  65. package/src/helpers/parseUserAgent.ts +0 -108
  66. package/src/helpers/visitEndpointOnAnotherServer/index.ts +0 -93
  67. package/src/helpers/visitEndpointOnAnotherServer/sendServerToServerRequest.ts +0 -164
  68. package/src/server/initLogCollection.ts +0 -27
  69. package/src/server/initServer.ts +0 -287
  70. /package/dist/cjs/types/{client → helpers}/initClient.d.ts +0 -0
  71. /package/dist/esm/types/{client → helpers}/initClient.d.ts +0 -0
@@ -1,948 +0,0 @@
1
- // Import caccl functions
2
- // TODO: fix dependency cycle
3
- // eslint-disable-next-line import/no-cycle
4
- import {
5
- cacclGetLaunchInfo,
6
- internalGetLogCollection,
7
- } from '../server/initServer';
8
-
9
- // Import shared types
10
- import ReactKitErrorCode from '../types/ReactKitErrorCode';
11
- import ParamType from '../types/ParamType';
12
-
13
- // Import helpers
14
- import handleError from './handleError';
15
- import handleSuccess from './handleSuccess';
16
- import genErrorPage from '../html/genErrorPage';
17
- import genInfoPage from '../html/genInfoPage';
18
- import parseUserAgent from './parseUserAgent';
19
- import getTimeInfoInET from './getTimeInfoInET';
20
-
21
- // Import shared types
22
- import LogFunction from '../types/LogFunction';
23
- import Log from '../types/Log';
24
- import LogType from '../types/LogType';
25
- import LogSource from '../types/LogSource';
26
- import LogTypeSpecificInfo from '../types/Log/LogTypeSpecificInfo';
27
- import LogMainInfo from '../types/Log/LogMainInfo';
28
- import LogSourceSpecificInfo from '../types/Log/LogSourceSpecificInfo';
29
- import LogBuiltInMetadata from '../types/LogBuiltInMetadata';
30
- import LogAction from '../types/LogAction';
31
- import LogLevel from '../types/LogLevel';
32
-
33
- /**
34
- * Generate an express API route handler
35
- * @author Gabe Abrams
36
- * @param opts object containing all arguments
37
- * @param opts.paramTypes map containing the types for each parameter that is
38
- * included in the request (map: param name => type)
39
- * @param opts.handler function that processes the request
40
- * @param [opts.skipSessionCheck] if true, skip the session check (allow users
41
- * to not be logged in and launched via LTI)
42
- * @param [opts.allowedHosts] if included, only allow requests from these hosts
43
- * (start a hostname with a "*" to only check the end of the hostname)
44
- * you can include just one string instead of an array
45
- * @param [opts.bannedHosts] if included, do not allow requests from these hosts
46
- * (start a hostname with a "*" to only check the end of the hostname)
47
- * you can include just one string instead of an array
48
- * @param [opts.unhandledErrorMessagePrefix] if included, when an error that
49
- * is not of type ErrorWithCode is thrown, the client will receive an error
50
- * where the error message is prefixed with this string. For example,
51
- * if unhandledErrorMessagePrefix is
52
- * 'While saving progress, we encountered an error:'
53
- * and the error is 'progressInfo is not an object',
54
- * the client will receive an error with the message
55
- * 'While saving progress, we encountered an error: progressInfo is not an object'
56
- * @returns express route handler that takes the following arguments:
57
- * params (map: param name => value),
58
- * req (express request object),
59
- * next (express next function),
60
- * send (a function that sends a string to the client),
61
- * redirect (takes a url and redirects the user to that url),
62
- * renderErrorPage (shows a static error page to the user),
63
- * renderInfoPage (shows a static info page to the user),
64
- * renderCustomHTML (renders custom html and sends it to the user),
65
- * and returns the value to send to the client as a JSON API response, or
66
- * calls next() or redirect(...) or send(...) or renderErrorPage(...).
67
- * Note: params also has userId, userFirstName,
68
- * userLastName, userEmail, userAvatarURL, isLearner, isTTM, isAdmin,
69
- * and any other variables that
70
- * are directly added to the session, if the user does have a session.
71
- */
72
- const genRouteHandler = (
73
- opts: {
74
- paramTypes?: {
75
- [k: string]: ParamType
76
- },
77
- handler: (
78
- opts: {
79
- params: {
80
- [k: string]: any
81
- },
82
- req: any,
83
- next: () => void,
84
- redirect: (pathOrURL: string) => void,
85
- send: (text: string, status?: number) => void,
86
- renderErrorPage: (
87
- opts?: {
88
- title?: string,
89
- description?: string,
90
- code?: string,
91
- pageTitle?: string,
92
- status?: number,
93
- },
94
- ) => void,
95
- renderInfoPage: (
96
- opts: {
97
- title: string,
98
- body: string,
99
- },
100
- ) => void,
101
- renderCustomHTML: (
102
- opts: {
103
- html: string,
104
- status?: number,
105
- },
106
- ) => void,
107
- logServerEvent: LogFunction,
108
- },
109
- ) => any,
110
- skipSessionCheck?: boolean,
111
- allowedHosts?: string[] | string,
112
- bannedHosts?: string[] | string,
113
- unhandledErrorMessagePrefix?: string,
114
- },
115
- ) => {
116
- // Return a route handler
117
- return async (req: any, res: any, next: () => void) => {
118
- // Output params
119
- const output: { [k in string]: any } = {};
120
-
121
- /*----------------------------------------*/
122
- /* ----------- Hostname Check ----------- */
123
- /*----------------------------------------*/
124
-
125
- // Get hostnames
126
- const originURL = String(
127
- req.get('origin')
128
- || req.headers.origin
129
- || req.headers.referer,
130
- );
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
-
149
- // Check if server is localhost
150
- if (serverHostname === 'localhost') {
151
- // Allow localhost
152
- allowedArray.push('localhost');
153
- }
154
-
155
- // Check if current host is allowed
156
- const allowed = allowedArray.some((allowedHost) => {
157
- if (allowedHost.startsWith('*')) {
158
- // Check end of hostname
159
- return originHostname.endsWith(allowedHost.substring(1));
160
- }
161
-
162
- // Check full hostname
163
- return originHostname.toLowerCase() === allowedHost.toLowerCase();
164
- });
165
-
166
- // If not allowed, return error
167
- if (!allowed) {
168
- return handleError(
169
- res,
170
- {
171
- message: 'You are not allowed to access this endpoint.',
172
- code: ReactKitErrorCode.HostNotAllowed,
173
- status: 403,
174
- },
175
- );
176
- }
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
-
195
- // Check full hostname
196
- return originHostname.toLowerCase() === bannedHost.toLowerCase();
197
- });
198
-
199
- // If banned, return error
200
- if (banned) {
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
- }
210
- }
211
-
212
- /*----------------------------------------*/
213
- /* ------------ Parse Params ------------ */
214
- /*----------------------------------------*/
215
-
216
- // Process items one by one
217
- const paramList = Object.entries(opts.paramTypes ?? {});
218
- for (let i = 0; i < paramList.length; i++) {
219
- const [name, type] = paramList[i];
220
-
221
- // Find the value as a string
222
- const value = (
223
- req.params[name]
224
- || req.query[name]
225
- || req.body[name]
226
- );
227
-
228
- // Parse
229
- if (type === ParamType.Boolean || type === ParamType.BooleanOptional) {
230
- // Boolean
231
-
232
- // Handle case where value doesn't exist
233
- if (value === undefined) {
234
- if (type === ParamType.BooleanOptional) {
235
- output[name] = undefined;
236
- } else {
237
- return handleError(
238
- res,
239
- {
240
- message: `Parameter ${name} is required, but it was not included.`,
241
- code: ReactKitErrorCode.MissingParameter,
242
- status: 422,
243
- },
244
- );
245
- }
246
- } else {
247
- // Value exists
248
-
249
- // Simplify value
250
- const simpleVal = (
251
- String(value)
252
- .trim()
253
- .toLowerCase()
254
- );
255
-
256
- // Parse
257
- output[name] = (
258
- [
259
- 'true',
260
- 'yes',
261
- 'y',
262
- '1',
263
- 't',
264
- ].indexOf(simpleVal) >= 0
265
- );
266
- }
267
- } else if (type === ParamType.Float || type === ParamType.FloatOptional) {
268
- // Float
269
-
270
- // Handle case where value doesn't exist
271
- if (value === undefined) {
272
- if (type === ParamType.FloatOptional) {
273
- output[name] = undefined;
274
- } else {
275
- return handleError(
276
- res,
277
- {
278
- message: `Parameter ${name} is required, but it was not included.`,
279
- code: ReactKitErrorCode.MissingParameter,
280
- status: 422,
281
- },
282
- );
283
- }
284
- } else if (!Number.isNaN(Number.parseFloat(String(value)))) {
285
- // Value is a number
286
- output[name] = Number.parseFloat(String(value));
287
- } else {
288
- // Issue!
289
- return handleError(
290
- res,
291
- {
292
- message: `Request data was malformed: ${name} was not a valid float.`,
293
- code: ReactKitErrorCode.InvalidParameter,
294
- status: 422,
295
- },
296
- );
297
- }
298
- } else if (type === ParamType.Int || type === ParamType.IntOptional) {
299
- // Int
300
-
301
- // Handle case where value doesn't exist
302
- if (value === undefined) {
303
- if (type === ParamType.IntOptional) {
304
- output[name] = undefined;
305
- } else {
306
- return handleError(
307
- res,
308
- {
309
- message: `Parameter ${name} is required, but it was not included.`,
310
- code: ReactKitErrorCode.MissingParameter,
311
- status: 422,
312
- },
313
- );
314
- }
315
- } else if (!Number.isNaN(Number.parseInt(String(value), 10))) {
316
- // Value is a number
317
- output[name] = Number.parseInt(String(value), 10);
318
- } else {
319
- // Issue!
320
- return handleError(
321
- res,
322
- {
323
- message: `Request data was malformed: ${name} was not a valid int.`,
324
- code: ReactKitErrorCode.InvalidParameter,
325
- status: 422,
326
- },
327
- );
328
- }
329
- } else if (type === ParamType.JSON || type === ParamType.JSONOptional) {
330
- // Stringified JSON
331
-
332
- // Handle case where value doesn't exist
333
- if (value === undefined) {
334
- if (type === ParamType.JSONOptional) {
335
- output[name] = undefined;
336
- } else {
337
- return handleError(
338
- res,
339
- {
340
- message: `Parameter ${name} is required, but it was not included.`,
341
- code: ReactKitErrorCode.MissingParameter,
342
- status: 422,
343
- },
344
- );
345
- }
346
- } else {
347
- // Value exists
348
-
349
- // Parse
350
- try {
351
- output[name] = JSON.parse(String(value));
352
- } catch (err) {
353
- return handleError(
354
- res,
355
- {
356
- message: `Request data was malformed: ${name} was not a valid JSON payload.`,
357
- code: ReactKitErrorCode.InvalidParameter,
358
- status: 422,
359
- },
360
- );
361
- }
362
- }
363
- } else if (type === ParamType.String || type === ParamType.StringOptional) {
364
- // String
365
-
366
- // Handle case where value doesn't exist
367
- if (value === undefined) {
368
- if (type === ParamType.StringOptional) {
369
- output[name] = undefined;
370
- } else {
371
- return handleError(
372
- res,
373
- {
374
- message: `Parameter ${name} is required, but it was not included.`,
375
- code: ReactKitErrorCode.MissingParameter,
376
- status: 422,
377
- },
378
- );
379
- }
380
- } else {
381
- // Value exists
382
-
383
- // Leave as is
384
- output[name] = value;
385
- }
386
- } else {
387
- // No valid data type
388
- return handleError(
389
- res,
390
- {
391
- message: `An internal error occurred: we could not determine the type of ${name}.`,
392
- code: ReactKitErrorCode.InvalidParameter,
393
- status: 422,
394
- },
395
- );
396
- }
397
- }
398
-
399
- /*----------------------------------------*/
400
- /* ------------- Launch Info ------------ */
401
- /*----------------------------------------*/
402
-
403
- // Get launch info
404
- const { launched, launchInfo } = cacclGetLaunchInfo(req);
405
- if (
406
- // Not launched
407
- (!launched || !launchInfo)
408
- // Not skipping the session check
409
- && !opts.skipSessionCheck
410
- ) {
411
- return handleError(
412
- res,
413
- {
414
- message: 'Your session has expired. Please refresh the page and try again.',
415
- code: ReactKitErrorCode.SessionExpired,
416
- status: 401,
417
- },
418
- );
419
- }
420
-
421
- // Error if user info cannot be found
422
- if (
423
- // User information is incomplete
424
- (
425
- !launchInfo
426
- || !launchInfo.userId
427
- || !launchInfo.userFirstName
428
- || !launchInfo.userLastName
429
- || (
430
- launchInfo.notInCourse
431
- && !launchInfo.isAdmin
432
- )
433
- || (
434
- !launchInfo.isTTM
435
- && !launchInfo.isLearner
436
- && !launchInfo.isAdmin
437
- )
438
- )
439
- // Not skipping the session check
440
- && !opts.skipSessionCheck
441
- ) {
442
- return handleError(
443
- res,
444
- {
445
- message: 'Your session was invalid. Please refresh the page and try again.',
446
- code: ReactKitErrorCode.SessionExpired,
447
- status: 401,
448
- },
449
- );
450
- }
451
-
452
- // Add launch info to output
453
- output.userId = (
454
- launchInfo
455
- ? launchInfo.userId
456
- : (output.userId ?? undefined)
457
- );
458
- output.userFirstName = (
459
- launchInfo
460
- ? launchInfo.userFirstName
461
- : (output.userFirstName ?? undefined)
462
- );
463
- output.userLastName = (
464
- launchInfo
465
- ? launchInfo.userLastName
466
- : (output.userLastName ?? undefined)
467
- );
468
- output.userEmail = (
469
- launchInfo
470
- ? launchInfo.userEmail
471
- : (output.userEmail ?? undefined)
472
- );
473
- output.userAvatarURL = (
474
- launchInfo
475
- ? (
476
- launchInfo.userImage
477
- ?? 'http://www.gravatar.com/avatar/?d=identicon'
478
- )
479
- : (output.userAvatarURL ?? undefined)
480
- );
481
- output.isLearner = (
482
- launchInfo
483
- ? !!launchInfo.isLearner
484
- : (output.isLearner ?? undefined)
485
- );
486
- output.isTTM = (
487
- launchInfo
488
- ? !!launchInfo.isTTM
489
- : (output.isTTM ?? undefined)
490
- );
491
- output.isAdmin = (
492
- launchInfo
493
- ? !!launchInfo.isAdmin
494
- : (output.isAdmin ?? undefined)
495
- );
496
- output.courseId = (
497
- launchInfo
498
- ? (output.courseId ?? launchInfo.courseId)
499
- : (output.courseId ?? undefined)
500
- );
501
- output.courseName = (
502
- launchInfo
503
- ? launchInfo.contextLabel
504
- : (output.courseName ?? undefined)
505
- );
506
-
507
- // Add other session variables
508
- Object.keys(req.session).forEach((propName) => {
509
- // Skip if prop already in output
510
- if (output[propName] !== undefined) {
511
- return;
512
- }
513
-
514
- // Add to output
515
- const value = req.session[propName];
516
- if (
517
- typeof value === 'string'
518
- || typeof value === 'boolean'
519
- || typeof value === 'number'
520
- ) {
521
- output[propName] = value;
522
- }
523
- });
524
-
525
- /*----------------------------------------*/
526
- /* ----- Require Course Consistency ----- */
527
- /*----------------------------------------*/
528
-
529
- // Make sure the user actually launched from the appropriate course
530
- if (
531
- output.courseId
532
- && launchInfo
533
- && launchInfo.courseId
534
- && output.courseId !== launchInfo.courseId
535
- && !output.isTTM
536
- && !output.isAdmin
537
- ) {
538
- // Course of interest is not the launch course
539
- return handleError(
540
- res,
541
- {
542
- message: 'You switched sessions by opening this app in another tab. Please refresh the page and try again.',
543
- code: ReactKitErrorCode.WrongCourse,
544
- status: 401,
545
- },
546
- );
547
- }
548
-
549
- /*----------------------------------------*/
550
- /* Require Proper Permissions */
551
- /*----------------------------------------*/
552
-
553
- // Add TTM endpoint security
554
- if (
555
- // This is a TTM endpoint
556
- req.path.startsWith('/api/ttm')
557
- // User is not a TTM
558
- && (
559
- // User is not a TTM
560
- !output.isTTM
561
- // User is not an admin
562
- && !output.isAdmin
563
- )
564
- ) {
565
- // User does not have access
566
- return handleError(
567
- res,
568
- {
569
- message: 'This action is only allowed if you are a teaching team member for the course. Please go back to Canvas, log in as a teaching team member, and try again.',
570
- code: ReactKitErrorCode.NotTTM,
571
- status: 401,
572
- },
573
- );
574
- }
575
-
576
- // Add Admin endpoint security
577
- if (
578
- // This is an admin endpoint
579
- req.path.startsWith('/api/admin')
580
- // User is not an admin
581
- && !output.isAdmin
582
- ) {
583
- // User does not have access
584
- return handleError(
585
- res,
586
- {
587
- message: 'This action is only allowed if you are a Canvas admin. Please go back to Canvas, log in as an admin, and try again.',
588
- code: ReactKitErrorCode.NotAdmin,
589
- status: 401,
590
- },
591
- );
592
- }
593
-
594
- /*----------------------------------------*/
595
- /* ------------- Log Handler ------------ */
596
- /*----------------------------------------*/
597
-
598
- // Create a log handler function
599
-
600
- /**
601
- * Log an event on the server
602
- * @author Gabe Abrams
603
- */
604
- const logServerEvent: LogFunction = async (logOpts) => {
605
- // NOTE: internally, we slip through an opts.overrideAsClientEvent boolean
606
- // that indicates that this is actually a client event, but we don't
607
- // include that in the LogFunction type because this is internal and
608
- // hidden from users
609
- try {
610
- // Parse user agent
611
- const {
612
- browser,
613
- device,
614
- } = parseUserAgent(req.headers['user-agent']);
615
-
616
- // Get time info in ET
617
- const {
618
- timestamp,
619
- year,
620
- month,
621
- day,
622
- hour,
623
- minute,
624
- } = getTimeInfoInET();
625
-
626
- // Main log info
627
- const mainLogInfo: LogMainInfo = {
628
- id: `${launchInfo ? launchInfo.userId : 'unknown'}-${Date.now()}-${Math.floor(Math.random() * 100000)}-${Math.floor(Math.random() * 100000)}`,
629
- userFirstName: (launchInfo ? launchInfo.userFirstName: 'unknown'),
630
- userLastName: (launchInfo ? launchInfo.userLastName : 'unknown'),
631
- userEmail: (launchInfo ? launchInfo.userEmail : 'unknown'),
632
- userId: (launchInfo ? launchInfo.userId : 'unknown'),
633
- isLearner: (launchInfo && !!launchInfo.isLearner),
634
- isAdmin: (launchInfo && !!launchInfo.isAdmin),
635
- isTTM: (launchInfo && !!launchInfo.isTTM),
636
- courseId: (launchInfo ? launchInfo.courseId : 'unknown' ),
637
- courseName: (launchInfo ? launchInfo.contextLabel : 'unknown'),
638
- browser,
639
- device,
640
- year,
641
- month,
642
- day,
643
- hour,
644
- minute,
645
- timestamp,
646
- context: (
647
- typeof logOpts.context === 'string'
648
- ? logOpts.context
649
- : (
650
- ((logOpts.context as any) ?? {})._
651
- ?? LogBuiltInMetadata.Context.Uncategorized
652
- )
653
- ),
654
- subcontext: (
655
- logOpts.subcontext
656
- ?? LogBuiltInMetadata.Context.Uncategorized
657
- ),
658
- tags: (logOpts.tags ?? []),
659
- level: (logOpts.level ?? LogLevel.Info),
660
- metadata: (logOpts.metadata ?? {}),
661
- };
662
-
663
- // Type-specific info
664
- const typeSpecificInfo: LogTypeSpecificInfo = (
665
- ('error' in opts && opts.error)
666
- ? {
667
- type: LogType.Error,
668
- errorMessage: (logOpts as any).error.message ?? 'Unknown message',
669
- errorCode: (logOpts as any).error.code ?? ReactKitErrorCode.NoCode,
670
- errorStack: (logOpts as any).error.stack ?? 'No stack',
671
- }
672
- : {
673
- type: LogType.Action,
674
- target: (
675
- (logOpts as any).target
676
- ?? LogBuiltInMetadata.Target.NoTarget
677
- ),
678
- action: (
679
- (logOpts as any).action
680
- ?? LogAction.Unknown
681
- ),
682
- }
683
- );
684
-
685
- // Source-specific info
686
- const sourceSpecificInfo: LogSourceSpecificInfo = (
687
- (logOpts as any).overrideAsClientEvent
688
- ? {
689
- source: LogSource.Client,
690
- }
691
- : {
692
- source: LogSource.Server,
693
- routePath: req.path,
694
- routeTemplate: req.route.path,
695
- }
696
- );
697
-
698
- // Build log event
699
- const log: Log = {
700
- ...mainLogInfo,
701
- ...typeSpecificInfo,
702
- ...sourceSpecificInfo,
703
- };
704
-
705
- // Either print to console or save to db
706
- const logCollection = internalGetLogCollection();
707
- if (logCollection) {
708
- // Store to the log collection
709
- await logCollection.insert(log);
710
- } else if (log.type === LogType.Error) {
711
- // Print to console
712
- // eslint-disable-next-line no-console
713
- console.error('dce-reactkit error log:', log);
714
- } else {
715
- // eslint-disable-next-line no-console
716
- console.log('dce-reactkit action log:', log);
717
- }
718
-
719
- // Return log entry
720
- return log;
721
- } catch (err) {
722
- // Print because we cannot store the error
723
- // eslint-disable-next-line no-console
724
- console.error(
725
- 'Could not log the following:',
726
- logOpts,
727
- 'due to this error:',
728
- (err as any ?? {}).message,
729
- (err as any ?? {}).stack,
730
- );
731
-
732
- // Create a dummy log to return
733
- const dummyMainInfo: LogMainInfo = {
734
- id: '-1',
735
- userFirstName: 'Unknown',
736
- userLastName: 'Unknown',
737
- userEmail: 'unknown@harvard.edu',
738
- userId: 1,
739
- isLearner: false,
740
- isAdmin: false,
741
- isTTM: false,
742
- courseId: 1,
743
- courseName: 'Unknown',
744
- browser: {
745
- name: 'Unknown',
746
- version: 'Unknown',
747
- },
748
- device: {
749
- isMobile: false,
750
- os: 'Unknown',
751
- },
752
- year: 1,
753
- month: 1,
754
- day: 1,
755
- hour: 1,
756
- minute: 1,
757
- timestamp: Date.now(),
758
- tags: [],
759
- level: LogLevel.Warn,
760
- metadata: {},
761
- context: LogBuiltInMetadata.Context.Uncategorized,
762
- subcontext: LogBuiltInMetadata.Context.Uncategorized,
763
- };
764
-
765
- const dummyTypeSpecificInfo: LogTypeSpecificInfo = {
766
- type: LogType.Error,
767
- errorMessage: 'Unknown',
768
- errorCode: 'Unknown',
769
- errorStack: 'No Stack',
770
- };
771
-
772
- const dummySourceSpecificInfo: LogSourceSpecificInfo = {
773
- source: LogSource.Server,
774
- routePath: req.path,
775
- routeTemplate: req.route.path,
776
- };
777
-
778
- const log: Log = {
779
- ...dummyMainInfo,
780
- ...dummyTypeSpecificInfo,
781
- ...dummySourceSpecificInfo,
782
- };
783
-
784
- return log;
785
- }
786
- };
787
-
788
- /*------------------------------------------------------------------------*/
789
- /* Call handler */
790
- /*------------------------------------------------------------------------*/
791
-
792
- // Keep track of whether a response was already sent
793
- let responseSent = false;
794
-
795
- /**
796
- * Redirect the user to another path or url
797
- * @author Gabe Abrams
798
- * @param pathOrURL the path or url to redirect to
799
- */
800
- const redirect = (pathOrURL: string) => {
801
- responseSent = true;
802
- res.redirect(pathOrURL);
803
- };
804
-
805
- /**
806
- * Send text to the client (with an optional status code)
807
- * @author Gabe Abrams
808
- * @param text the text to send to the client
809
- * @parm [status=200] the http status code to send
810
- */
811
- const send = (text: string, status: number = 200) => {
812
- responseSent = true;
813
- res.status(status).send(text);
814
- };
815
-
816
- /**
817
- * Render an error page
818
- * @author Gabe Abrams
819
- * @param renderOpts object containing all arguments
820
- * @param [renderOpts.title=An Error Occurred] title of the error box
821
- * @param [renderOpts.description=An unknown server error occurred. Please contact support.]
822
- * a human-readable description of the error
823
- * @param [renderOpts.code=ReactKitErrorCode.NoCode] error code to show
824
- * @param [renderOpts.pageTitle=renderOpts.title] title of the page/tab if it differs from
825
- * the title of the error
826
- * @param [renderOpts.status=500] http status code
827
- */
828
- const renderErrorPage = (
829
- renderOpts: {
830
- title?: string,
831
- description?: string,
832
- code?: string,
833
- pageTitle?: string,
834
- status?: number,
835
- } = {},
836
- ) => {
837
- const html = genErrorPage(renderOpts);
838
- send(html, renderOpts.status ?? 500);
839
-
840
- // Log server-side error if not a session expired error or 404
841
- if (renderOpts.status && renderOpts.status === 404) {
842
- return;
843
- }
844
- if (renderOpts.title?.toLowerCase().includes('session expired')) {
845
- return;
846
- }
847
- logServerEvent({
848
- context: LogBuiltInMetadata.Context.ServerRenderedErrorPage,
849
- error: {
850
- message: `${renderOpts.title}: ${renderOpts.description}`,
851
- code: renderOpts.code,
852
- },
853
- metadata: {
854
- title: renderOpts.title,
855
- description: renderOpts.description,
856
- code: renderOpts.code,
857
- pageTitle: renderOpts.pageTitle,
858
- status: renderOpts.status ?? 500,
859
- },
860
- });
861
- };
862
-
863
- /**
864
- * Render an info page
865
- * @author Gabe Abrams
866
- * @param renderOpts object containing all arguments
867
- * @param renderOpts.title title of the info box
868
- * @param renderOpts.body a human-readable text body for the info alert
869
- */
870
- const renderInfoPage = (
871
- renderOpts: {
872
- title: string,
873
- body: string,
874
- },
875
- ) => {
876
- const html = genInfoPage(renderOpts);
877
- send(html, 200);
878
- };
879
-
880
- /**
881
- * Render custom HTML
882
- * @author Gabe Abrams
883
- * @param htmlOpts object containing all arguments
884
- * @param htmlOpts.html the HTML to send to the client
885
- * @param [ejsOpts.status=200] the http status code to send
886
- */
887
- const renderCustomHTML = (
888
- htmlOpts: {
889
- html: string,
890
- status?: number,
891
- },
892
- ) => {
893
- send(htmlOpts.html, htmlOpts.status ?? 200);
894
- };
895
-
896
- // Call the handler
897
- try {
898
- const results = await opts.handler({
899
- params: output,
900
- req,
901
- send,
902
- next: () => {
903
- responseSent = true;
904
- next();
905
- },
906
- redirect,
907
- renderErrorPage,
908
- renderInfoPage,
909
- renderCustomHTML,
910
- logServerEvent,
911
- });
912
-
913
- // Send results to client (only if next wasn't called)
914
- if (!responseSent) {
915
- return handleSuccess(res, results ?? undefined);
916
- }
917
- } catch (err) {
918
- // Prefix error message if needed
919
- if (
920
- opts.unhandledErrorMessagePrefix
921
- && err instanceof Error
922
- && err.message
923
- && err.name !== 'ErrorWithCode'
924
- ) {
925
- err.message = `${opts.unhandledErrorMessagePrefix.trim()} ${err.message.trim()}`;
926
- }
927
-
928
- // Send error to client (only if next wasn't called)
929
- if (!responseSent) {
930
- handleError(res, err);
931
-
932
- // Log server-side error
933
- logServerEvent({
934
- context: LogBuiltInMetadata.Context.ServerEndpointError,
935
- error: err,
936
- });
937
-
938
- return;
939
- }
940
-
941
- // Log error that was not responded with
942
- // eslint-disable-next-line no-console
943
- console.log('Error occurred but could not be sent to client because a response was already sent:', err);
944
- }
945
- };
946
- };
947
-
948
- export default genRouteHandler;