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