dce-expresskit 4.0.0-beta-logreviewer.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 (99) hide show
  1. package/.eslintrc.js +93 -0
  2. package/LICENSE +21 -0
  3. package/README.md +17 -0
  4. package/genEncodedSecret.ts +107 -0
  5. package/genSalt.ts +15 -0
  6. package/lib/constants/LOG_REVIEW_PAGE_SIZE.d.ts +6 -0
  7. package/lib/constants/LOG_REVIEW_PAGE_SIZE.js +9 -0
  8. package/lib/constants/LOG_REVIEW_PAGE_SIZE.js.map +1 -0
  9. package/lib/constants/LOG_REVIEW_ROUTE_PATH_PREFIX.d.ts +6 -0
  10. package/lib/constants/LOG_REVIEW_ROUTE_PATH_PREFIX.js +13 -0
  11. package/lib/constants/LOG_REVIEW_ROUTE_PATH_PREFIX.js.map +1 -0
  12. package/lib/constants/LOG_REVIEW_STATUS_ROUTE.d.ts +7 -0
  13. package/lib/constants/LOG_REVIEW_STATUS_ROUTE.js +14 -0
  14. package/lib/constants/LOG_REVIEW_STATUS_ROUTE.js.map +1 -0
  15. package/lib/constants/LOG_ROUTE_PATH.d.ts +6 -0
  16. package/lib/constants/LOG_ROUTE_PATH.js +13 -0
  17. package/lib/constants/LOG_ROUTE_PATH.js.map +1 -0
  18. package/lib/constants/ROUTE_PATH_PREFIX.d.ts +6 -0
  19. package/lib/constants/ROUTE_PATH_PREFIX.js +9 -0
  20. package/lib/constants/ROUTE_PATH_PREFIX.js.map +1 -0
  21. package/lib/errors/ErrorWithCode.d.ts +9 -0
  22. package/lib/errors/ErrorWithCode.js +33 -0
  23. package/lib/errors/ErrorWithCode.js.map +1 -0
  24. package/lib/helpers/addDBEditorEndpoints/generateEndpointPath.d.ts +9 -0
  25. package/lib/helpers/addDBEditorEndpoints/generateEndpointPath.js +17 -0
  26. package/lib/helpers/addDBEditorEndpoints/generateEndpointPath.js.map +1 -0
  27. package/lib/helpers/addDBEditorEndpoints/index.d.ts +41 -0
  28. package/lib/helpers/addDBEditorEndpoints/index.js +134 -0
  29. package/lib/helpers/addDBEditorEndpoints/index.js.map +1 -0
  30. package/lib/helpers/dataSigner.d.ts +40 -0
  31. package/lib/helpers/dataSigner.js +242 -0
  32. package/lib/helpers/dataSigner.js.map +1 -0
  33. package/lib/helpers/genRouteHandler.d.ts +75 -0
  34. package/lib/helpers/genRouteHandler.js +662 -0
  35. package/lib/helpers/genRouteHandler.js.map +1 -0
  36. package/lib/helpers/getLogReviewerLogs.d.ts +27 -0
  37. package/lib/helpers/getLogReviewerLogs.js +238 -0
  38. package/lib/helpers/getLogReviewerLogs.js.map +1 -0
  39. package/lib/helpers/handleError.d.ts +18 -0
  40. package/lib/helpers/handleError.js +51 -0
  41. package/lib/helpers/handleError.js.map +1 -0
  42. package/lib/helpers/handleSuccess.d.ts +8 -0
  43. package/lib/helpers/handleSuccess.js +20 -0
  44. package/lib/helpers/handleSuccess.js.map +1 -0
  45. package/lib/helpers/initCrossServerCredentialCollection.d.ts +11 -0
  46. package/lib/helpers/initCrossServerCredentialCollection.js +15 -0
  47. package/lib/helpers/initCrossServerCredentialCollection.js.map +1 -0
  48. package/lib/helpers/initLogCollection.d.ts +11 -0
  49. package/lib/helpers/initLogCollection.js +26 -0
  50. package/lib/helpers/initLogCollection.js.map +1 -0
  51. package/lib/helpers/initServer.d.ts +45 -0
  52. package/lib/helpers/initServer.js +292 -0
  53. package/lib/helpers/initServer.js.map +1 -0
  54. package/lib/helpers/parseUserAgent.d.ts +17 -0
  55. package/lib/helpers/parseUserAgent.js +108 -0
  56. package/lib/helpers/parseUserAgent.js.map +1 -0
  57. package/lib/helpers/visitEndpointOnAnotherServer/index.d.ts +18 -0
  58. package/lib/helpers/visitEndpointOnAnotherServer/index.js +89 -0
  59. package/lib/helpers/visitEndpointOnAnotherServer/index.js.map +1 -0
  60. package/lib/helpers/visitEndpointOnAnotherServer/sendServerToServerRequest.d.ts +23 -0
  61. package/lib/helpers/visitEndpointOnAnotherServer/sendServerToServerRequest.js +236 -0
  62. package/lib/helpers/visitEndpointOnAnotherServer/sendServerToServerRequest.js.map +1 -0
  63. package/lib/html/genErrorPage.d.ts +19 -0
  64. package/lib/html/genErrorPage.js +27 -0
  65. package/lib/html/genErrorPage.js.map +1 -0
  66. package/lib/html/genInfoPage.d.ts +13 -0
  67. package/lib/html/genInfoPage.js +16 -0
  68. package/lib/html/genInfoPage.js.map +1 -0
  69. package/lib/index.d.ts +11 -0
  70. package/lib/index.js +68 -0
  71. package/lib/index.js.map +1 -0
  72. package/lib/types/CrossServerCredential.d.ts +11 -0
  73. package/lib/types/CrossServerCredential.js +3 -0
  74. package/lib/types/CrossServerCredential.js.map +1 -0
  75. package/lib/types/ExpressKitErrorCode.d.ts +31 -0
  76. package/lib/types/ExpressKitErrorCode.js +38 -0
  77. package/lib/types/ExpressKitErrorCode.js.map +1 -0
  78. package/package.json +53 -0
  79. package/src/constants/LOG_REVIEW_PAGE_SIZE.ts +7 -0
  80. package/src/errors/ErrorWithCode.tsx +15 -0
  81. package/src/helpers/addDBEditorEndpoints/generateEndpointPath.ts +16 -0
  82. package/src/helpers/addDBEditorEndpoints/index.ts +130 -0
  83. package/src/helpers/dataSigner.ts +319 -0
  84. package/src/helpers/genRouteHandler.ts +920 -0
  85. package/src/helpers/getLogReviewerLogs.ts +259 -0
  86. package/src/helpers/handleError.ts +66 -0
  87. package/src/helpers/handleSuccess.ts +18 -0
  88. package/src/helpers/initCrossServerCredentialCollection.ts +19 -0
  89. package/src/helpers/initLogCollection.ts +30 -0
  90. package/src/helpers/initServer.ts +283 -0
  91. package/src/helpers/parseUserAgent.ts +108 -0
  92. package/src/helpers/visitEndpointOnAnotherServer/index.ts +70 -0
  93. package/src/helpers/visitEndpointOnAnotherServer/sendServerToServerRequest.ts +257 -0
  94. package/src/html/genErrorPage.ts +144 -0
  95. package/src/html/genInfoPage.ts +101 -0
  96. package/src/index.ts +125 -0
  97. package/src/types/CrossServerCredential.ts +16 -0
  98. package/src/types/ExpressKitErrorCode.ts +37 -0
  99. package/tsconfig.json +19 -0
@@ -0,0 +1,259 @@
1
+ // Import dce-reactkit
2
+ import {
3
+ DAY_IN_MS,
4
+ Log,
5
+ LogReviewerFilterState,
6
+ LogType
7
+ } from 'dce-reactkit';
8
+
9
+ // Import shared types
10
+ import LOG_REVIEW_PAGE_SIZE from '../constants/LOG_REVIEW_PAGE_SIZE';
11
+ import { Collection } from 'dce-mango';
12
+
13
+ /**
14
+ * Get logs for the log reviewer interface
15
+ * @author Yuen Ler Chow
16
+ * @param opts object containing all arguments
17
+ * @param opts.pageNumber the page number to retrieve (1-indexed)
18
+ * @param opts.filters filter criteria for logs
19
+ * @param opts.countDocuments if true, count number of documents matching
20
+ * filters and return num pages (not always required because if changing pages,
21
+ * we don't need to recount documents)
22
+ * @param opts.logCollection MongoDB collection containing logs
23
+ * @returns object with logs for the requested page and optionally total number of pages
24
+ */
25
+ const getLogReviewerLogs = async (
26
+ opts: {
27
+ pageNumber: number,
28
+ filters: LogReviewerFilterState,
29
+ countDocuments: boolean,
30
+ logCollection: Collection<Log>,
31
+ },
32
+ ) => {
33
+
34
+ // Destructure opts
35
+ const {
36
+ pageNumber,
37
+ filters,
38
+ countDocuments,
39
+ logCollection,
40
+ } = opts;
41
+
42
+
43
+ // Destructure filters
44
+ const {
45
+ dateFilterState,
46
+ contextFilterState,
47
+ tagFilterState,
48
+ actionErrorFilterState,
49
+ advancedFilterState,
50
+ } = filters as LogReviewerFilterState;
51
+
52
+ // Build MongoDB query based on filters
53
+ const query: { [k: string]: any } = {};
54
+
55
+ /* -------------- Date Filter ------------- */
56
+
57
+ // Convert start and end dates from the dateFilterState into timestamps
58
+ const { startDate, endDate } = dateFilterState;
59
+ const startTimestamp = new Date(
60
+ `${startDate.month}/${startDate.day}/${startDate.year}`,
61
+ ).getTime();
62
+ const endTimestamp = (
63
+ (new Date(`${endDate.month}/${endDate.day}/${endDate.year}`)).getTime()
64
+ + DAY_IN_MS
65
+ );
66
+
67
+ // Add a date range condition to the query
68
+ query.timestamp = {
69
+ $gte: startTimestamp,
70
+ $lt: endTimestamp,
71
+ };
72
+
73
+ /* ------------ Context Filter ------------ */
74
+
75
+ // Process context filters to include selected contexts and subcontexts
76
+ const contextConditions: { [k: string]: any }[] = [];
77
+ Object.keys(contextFilterState).forEach((context) => {
78
+ const value = contextFilterState[context];
79
+ if (typeof value === 'boolean') {
80
+ if (value) {
81
+ // The entire context is selected
82
+ contextConditions.push({ context });
83
+ }
84
+ } else {
85
+ // The context has subcontexts
86
+ const subcontexts = Object.keys(value).filter((subcontext) => {
87
+ return value[subcontext];
88
+ });
89
+
90
+ if (subcontexts.length > 0) {
91
+ contextConditions.push({
92
+ context,
93
+ subcontext: { $in: subcontexts },
94
+ });
95
+ }
96
+ }
97
+ });
98
+ if (contextConditions.length > 0) {
99
+ query.$or = contextConditions;
100
+ }
101
+
102
+ /* -------------- Tag Filter -------------- */
103
+
104
+ const selectedTags = Object.keys(tagFilterState).filter((tag) => { return tagFilterState[tag]; });
105
+ if (selectedTags.length > 0) {
106
+ query.tags = { $in: selectedTags };
107
+ }
108
+
109
+ /* --------- Action/Error Filter ---------- */
110
+
111
+ if (actionErrorFilterState.type) {
112
+ query.type = actionErrorFilterState.type;
113
+ }
114
+
115
+ if (actionErrorFilterState.type === LogType.Error) {
116
+ if (actionErrorFilterState.errorMessage) {
117
+ // Add error message to the query.
118
+ // $i is used for case-insensitive search, and $regex is used for partial matching
119
+ query.errorMessage = {
120
+ $regex: actionErrorFilterState.errorMessage,
121
+ $options: 'i',
122
+ };
123
+ }
124
+
125
+ if (actionErrorFilterState.errorCode) {
126
+ query.errorCode = {
127
+ $regex: actionErrorFilterState.errorCode,
128
+ $options: 'i',
129
+ };
130
+ }
131
+ }
132
+
133
+ if (actionErrorFilterState.type === LogType.Action) {
134
+ const selectedTargets = (
135
+ Object.keys(actionErrorFilterState.target)
136
+ .filter((target) => {
137
+ return actionErrorFilterState.target[target];
138
+ })
139
+ );
140
+ const selectedActions = (
141
+ Object.keys(actionErrorFilterState.action)
142
+ .filter((action) => {
143
+ return actionErrorFilterState.action[action];
144
+ })
145
+ );
146
+ if (selectedTargets.length > 0) {
147
+ query.target = { $in: selectedTargets };
148
+ }
149
+ if (selectedActions.length > 0) {
150
+ query.action = { $in: selectedActions };
151
+ }
152
+ }
153
+
154
+ /* ------------ Advanced Filter ----------- */
155
+
156
+ if (advancedFilterState.userFirstName) {
157
+ query.userFirstName = {
158
+ $regex: advancedFilterState.userFirstName,
159
+ $options: 'i',
160
+ };
161
+ }
162
+
163
+ if (advancedFilterState.userLastName) {
164
+ query.userLastName = {
165
+ $regex: advancedFilterState.userLastName,
166
+ $options: 'i',
167
+ };
168
+ }
169
+
170
+ if (advancedFilterState.userEmail) {
171
+ query.userEmail = {
172
+ $regex: advancedFilterState.userEmail,
173
+ $options: 'i',
174
+ };
175
+ }
176
+
177
+ if (advancedFilterState.userId) {
178
+ query.userId = Number.parseInt(advancedFilterState.userId, 10);
179
+ }
180
+
181
+ const roles: {
182
+ isLearner?: boolean,
183
+ isTTM?: boolean,
184
+ isAdmin?: boolean,
185
+ }[] = [];
186
+ if (advancedFilterState.includeLearners) {
187
+ roles.push({ isLearner: true });
188
+ }
189
+ if (advancedFilterState.includeTTMs) {
190
+ roles.push({ isTTM: true });
191
+ }
192
+ if (advancedFilterState.includeAdmins) {
193
+ roles.push({ isAdmin: true });
194
+ }
195
+ // If any roles are selected, add them to the query
196
+ if (roles.length > 0) {
197
+ // The $or operator is used to match any of the roles
198
+ // The $and operator is to ensure that other conditions in the query are met
199
+ query.$and = [{ $or: roles }];
200
+ }
201
+
202
+ if (advancedFilterState.courseId) {
203
+ query.courseId = Number.parseInt(advancedFilterState.courseId, 10);
204
+ }
205
+
206
+ if (advancedFilterState.courseName) {
207
+ query.courseName = {
208
+ $regex: advancedFilterState.courseName,
209
+ $options: 'i',
210
+ };
211
+ }
212
+
213
+ if (advancedFilterState.isMobile !== undefined) {
214
+ query['device.isMobile'] = Boolean(advancedFilterState.isMobile);
215
+ }
216
+
217
+ if (advancedFilterState.source) {
218
+ query.source = advancedFilterState.source;
219
+ }
220
+
221
+ if (advancedFilterState.routePath) {
222
+ query.routePath = {
223
+ $regex: advancedFilterState.routePath,
224
+ $options: 'i',
225
+ };
226
+ }
227
+
228
+ if (advancedFilterState.routeTemplate) {
229
+ query.routeTemplate = {
230
+ $regex: advancedFilterState.routeTemplate,
231
+ $options: 'i',
232
+ };
233
+ }
234
+
235
+ // Query for logs
236
+ const response = await logCollection.findPaged({
237
+ query,
238
+ perPage: LOG_REVIEW_PAGE_SIZE,
239
+ pageNumber,
240
+ sortDescending: true,
241
+ });
242
+
243
+ // Count documents if requested
244
+ if (countDocuments) {
245
+ const numPages = Math.ceil(
246
+ await logCollection.count(query)
247
+ / LOG_REVIEW_PAGE_SIZE
248
+ );
249
+ return {
250
+ ...response,
251
+ numPages,
252
+ };
253
+ }
254
+
255
+ // Return response
256
+ return response;
257
+ };
258
+
259
+ export default getLogReviewerLogs;
@@ -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,30 @@
1
+ // Import dce-mango
2
+ import { Collection as MangoCollection } from 'dce-mango';
3
+
4
+ /**
5
+ * Initialize a log 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 initLogCollection = (Collection: typeof MangoCollection) => {
11
+ return new Collection(
12
+ 'Log',
13
+ {
14
+ uniqueIndexKey: 'id',
15
+ indexKeys: [
16
+ 'courseId',
17
+ 'context',
18
+ 'subcontext',
19
+ 'tags',
20
+ 'year',
21
+ 'month',
22
+ 'day',
23
+ 'hour',
24
+ 'type',
25
+ ],
26
+ },
27
+ );
28
+ };
29
+
30
+ export default initLogCollection;
@@ -0,0 +1,283 @@
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
+ ParamType,
10
+ LogFunction,
11
+ LOG_ROUTE_PATH,
12
+ LOG_REVIEW_STATUS_ROUTE,
13
+ Log,
14
+ LOG_REVIEW_GET_LOGS_ROUTE,
15
+ ErrorWithCode,
16
+ } from 'dce-reactkit';
17
+
18
+ // Import shared helpers
19
+ import genRouteHandler from './genRouteHandler';
20
+ import getLogReviewerLogs from './getLogReviewerLogs';
21
+
22
+ // Import shared types
23
+ import ExpressKitErrorCode from '../types/ExpressKitErrorCode';
24
+ import CrossServerCredential from '../types/CrossServerCredential';
25
+
26
+ // Stored copy of dce-mango log collection
27
+ let _logCollection: Collection<Log>;
28
+
29
+ // Stored copy of dce-mango cross-server credential collection
30
+ let _crossServerCredentialCollection: Collection<CrossServerCredential>;
31
+
32
+ /*------------------------------------------------------------------------*/
33
+ /* Helpers */
34
+ /*------------------------------------------------------------------------*/
35
+
36
+ /**
37
+ * Get log collection
38
+ * @author Gabe Abrams
39
+ * @returns log collection if one was included during launch or null if we don't
40
+ * have a log collection (yet)
41
+ */
42
+ export const internalGetLogCollection = () => {
43
+ return _logCollection ?? null;
44
+ };
45
+
46
+ /**
47
+ * Get cross-server credential collection
48
+ * @author Gabe Abrams
49
+ * @return cross-server credential collection if one was included during launch or null
50
+ * if we don't have a cross-server credential collection (yet)
51
+ */
52
+ export const internalGetCrossServerCredentialCollection = () => {
53
+ return _crossServerCredentialCollection ?? null;
54
+ };
55
+
56
+ /*------------------------------------------------------------------------*/
57
+ /* Main */
58
+ /*------------------------------------------------------------------------*/
59
+
60
+ /**
61
+ * Prepare dce-reactkit to run on the server
62
+ * @author Gabe Abrams
63
+ * @param opts object containing all arguments
64
+ * @param opts.app express app from inside of the postprocessor function that
65
+ * we will add routes to
66
+ * @param opts.getLaunchInfo CACCL LTI's get launch info function
67
+ * @param [opts.logCollection] mongo collection from dce-mango to use for
68
+ * storing logs. If none is included, logs are written to the console
69
+ * @param [opts.logReviewAdmins=all] info on which admins can review
70
+ * logs from the client. If not included, all Canvas admins are allowed to
71
+ * review logs. If null, no Canvas admins are allowed to review logs.
72
+ * If an array of Canvas userIds (numbers), only Canvas admins with those
73
+ * userIds are allowed to review logs. If a dce-mango collection, only
74
+ * Canvas admins with entries in that collection ({ userId, ...}) are allowed
75
+ * to review logs
76
+ * @param [opts.crossServerCredentialCollection] mongo collection from dce-mango to use for
77
+ * storing cross-server credentials. If none is included, cross-server credentials
78
+ * are not supported
79
+ */
80
+ const initServer = (
81
+ opts: {
82
+ app: express.Application,
83
+ logReviewAdmins?: (number[] | Collection<any>),
84
+ logCollection?: Collection<Log>,
85
+ crossServerCredentialCollection?: Collection<CrossServerCredential>,
86
+ },
87
+ ) => {
88
+ _logCollection = opts.logCollection;
89
+ _crossServerCredentialCollection = opts.crossServerCredentialCollection;
90
+
91
+ /*----------------------------------------*/
92
+ /* Logging */
93
+ /*----------------------------------------*/
94
+
95
+ /**
96
+ * Log an event
97
+ * @author Gabe Abrams
98
+ * @param {string} context Context of the event (each app determines how to
99
+ * organize its contexts)
100
+ * @param {string} subcontext Subcontext of the event (each app determines
101
+ * how to organize its subcontexts)
102
+ * @param {string} tags stringified list of tags that apply to this action
103
+ * (each app determines tag usage)
104
+ * @param {string} metadata stringified object containing optional custom metadata
105
+ * @param {string} level log level
106
+ * @param {string} [errorMessage] error message if type is an error
107
+ * @param {string} [errorCode] error code if type is an error
108
+ * @param {string} [errorStack] error stack if type is an error
109
+ * @param {string} [target] Target of the action (each app determines the list
110
+ * of targets) These are usually buttons, panels, elements, etc.
111
+ * @param {LogAction} [action] the type of action performed on the target
112
+ * @returns {Log}
113
+ */
114
+ opts.app.post(
115
+ LOG_ROUTE_PATH,
116
+ genRouteHandler({
117
+ paramTypes: {
118
+ context: ParamType.String,
119
+ subcontext: ParamType.String,
120
+ tags: ParamType.JSON,
121
+ level: ParamType.String,
122
+ metadata: ParamType.JSON,
123
+ errorMessage: ParamType.StringOptional,
124
+ errorCode: ParamType.StringOptional,
125
+ errorStack: ParamType.StringOptional,
126
+ target: ParamType.StringOptional,
127
+ action: ParamType.StringOptional,
128
+ },
129
+ handler: ({ params, logServerEvent }) => {
130
+ // Create log info
131
+ const logInfo: Parameters<LogFunction>[0] = (
132
+ (params.errorMessage || params.errorCode || params.errorStack)
133
+ // Error
134
+ ? {
135
+ context: params.context,
136
+ subcontext: params.subcontext,
137
+ tags: params.tags,
138
+ level: params.level,
139
+ metadata: params.metadata,
140
+ error: {
141
+ message: params.errorMessage,
142
+ code: params.errorCode,
143
+ stack: params.errorStack,
144
+ },
145
+ }
146
+ // Action
147
+ : {
148
+ context: params.context,
149
+ subcontext: params.subcontext,
150
+ tags: params.tags,
151
+ level: params.level,
152
+ metadata: params.metadata,
153
+ target: params.target,
154
+ action: params.action,
155
+ }
156
+ );
157
+
158
+ // Add hidden boolean to change source to "client"
159
+ const logInfoForcedFromClient = {
160
+ ...logInfo,
161
+ overrideAsClientEvent: true,
162
+ };
163
+
164
+ // Write the log
165
+ const log = logServerEvent(logInfoForcedFromClient);
166
+
167
+ // Return
168
+ return log;
169
+ },
170
+ }),
171
+ );
172
+
173
+ /*----------------------------------------*/
174
+ /* Log Reviewer */
175
+ /*----------------------------------------*/
176
+
177
+ /**
178
+ * Check if a given user is allowed to review logs
179
+ * @author Gabe Abrams
180
+ * @param userId the id of the user
181
+ * @param isAdmin if true, the user is an admin
182
+ * @returns true if the user can review logs
183
+ */
184
+ const canReviewLogs = async (
185
+ userId: number,
186
+ isAdmin: boolean,
187
+ ): Promise<boolean> => {
188
+ // Immediately deny access if user is not an admin
189
+ if (!isAdmin) {
190
+ return false;
191
+ }
192
+
193
+ // If all admins are allowed, we're done
194
+ if (!opts.logReviewAdmins) {
195
+ return true;
196
+ }
197
+
198
+ // Do a dynamic check
199
+ try {
200
+ // Array of userIds
201
+ if (Array.isArray(opts.logReviewAdmins)) {
202
+ return opts.logReviewAdmins.some((allowedId) => {
203
+ return (userId === allowedId);
204
+ });
205
+ }
206
+
207
+ // Must be a collection
208
+ const matches = await opts.logReviewAdmins.find({ userId });
209
+
210
+ // Make sure at least one entry matches
211
+ return matches.length > 0;
212
+ } catch (err) {
213
+ // If an error occurred, simply return false
214
+ return false;
215
+ }
216
+ };
217
+
218
+ /**
219
+ * Check if the current user has access to logs
220
+ * @author Gabe Abrams
221
+ * @returns {boolean} true if user has access
222
+ */
223
+ opts.app.get(
224
+ LOG_REVIEW_STATUS_ROUTE,
225
+ genRouteHandler({
226
+ handler: async ({ params }) => {
227
+ const { userId, isAdmin } = params;
228
+ const canReview = await canReviewLogs(userId, isAdmin);
229
+ return canReview;
230
+ },
231
+ }),
232
+ );
233
+
234
+ /**
235
+ * Get filtered logs based on provided filters
236
+ * @author Gabe Abrams, Yuen Ler Chow
237
+ * @param pageNumber the page number to get
238
+ * @param filters the filters to apply to the logs
239
+ * @returns {Log[]} list of logs that match the filters
240
+ */
241
+ opts.app.get(
242
+ LOG_REVIEW_GET_LOGS_ROUTE,
243
+ genRouteHandler({
244
+ paramTypes: {
245
+ pageNumber: ParamType.Int,
246
+ filters: ParamType.JSON,
247
+ countDocuments: ParamType.Boolean,
248
+ },
249
+ handler: async ({ params }) => {
250
+ // Destructure params
251
+ const {
252
+ pageNumber,
253
+ userId,
254
+ isAdmin,
255
+ filters,
256
+ countDocuments,
257
+ } = params;
258
+
259
+ // Validate user
260
+ const canReview = await canReviewLogs(userId, isAdmin);
261
+ if (!canReview) {
262
+ throw new ErrorWithCode(
263
+ 'You cannot access this resource because you do not have the appropriate permissions.',
264
+ ExpressKitErrorCode.NotAllowedToReviewLogs,
265
+ );
266
+ }
267
+
268
+ // Get logs
269
+ const response = await getLogReviewerLogs({
270
+ pageNumber,
271
+ filters,
272
+ countDocuments,
273
+ logCollection: _logCollection,
274
+ });
275
+
276
+ // Return response
277
+ return response;
278
+ },
279
+ }),
280
+ );
281
+ };
282
+
283
+ export default initServer;