dce-expresskit 4.0.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +93 -0
- package/LICENSE +21 -0
- package/README.md +17 -0
- package/genEncodedSecret.ts +107 -0
- package/genSalt.ts +15 -0
- package/lib/constants/LOG_REVIEW_ROUTE_PATH_PREFIX.d.ts +6 -0
- package/lib/constants/LOG_REVIEW_ROUTE_PATH_PREFIX.js +13 -0
- package/lib/constants/LOG_REVIEW_ROUTE_PATH_PREFIX.js.map +1 -0
- package/lib/constants/LOG_REVIEW_STATUS_ROUTE.d.ts +7 -0
- package/lib/constants/LOG_REVIEW_STATUS_ROUTE.js +14 -0
- package/lib/constants/LOG_REVIEW_STATUS_ROUTE.js.map +1 -0
- package/lib/constants/LOG_ROUTE_PATH.d.ts +6 -0
- package/lib/constants/LOG_ROUTE_PATH.js +13 -0
- package/lib/constants/LOG_ROUTE_PATH.js.map +1 -0
- package/lib/constants/ROUTE_PATH_PREFIX.d.ts +6 -0
- package/lib/constants/ROUTE_PATH_PREFIX.js +9 -0
- package/lib/constants/ROUTE_PATH_PREFIX.js.map +1 -0
- package/lib/errors/ErrorWithCode.d.ts +9 -0
- package/lib/errors/ErrorWithCode.js +33 -0
- package/lib/errors/ErrorWithCode.js.map +1 -0
- package/lib/helpers/addDBEditorEndpoints/generateEndpointPath.d.ts +9 -0
- package/lib/helpers/addDBEditorEndpoints/generateEndpointPath.js +17 -0
- package/lib/helpers/addDBEditorEndpoints/generateEndpointPath.js.map +1 -0
- package/lib/helpers/addDBEditorEndpoints/index.d.ts +41 -0
- package/lib/helpers/addDBEditorEndpoints/index.js +134 -0
- package/lib/helpers/addDBEditorEndpoints/index.js.map +1 -0
- package/lib/helpers/dataSigner.d.ts +40 -0
- package/lib/helpers/dataSigner.js +236 -0
- package/lib/helpers/dataSigner.js.map +1 -0
- package/lib/helpers/genRouteHandler.d.ts +75 -0
- package/lib/helpers/genRouteHandler.js +661 -0
- package/lib/helpers/genRouteHandler.js.map +1 -0
- package/lib/helpers/handleError.d.ts +18 -0
- package/lib/helpers/handleError.js +51 -0
- package/lib/helpers/handleError.js.map +1 -0
- package/lib/helpers/handleSuccess.d.ts +8 -0
- package/lib/helpers/handleSuccess.js +20 -0
- package/lib/helpers/handleSuccess.js.map +1 -0
- package/lib/helpers/initCrossServerCredentialCollection.d.ts +11 -0
- package/lib/helpers/initCrossServerCredentialCollection.js +15 -0
- package/lib/helpers/initCrossServerCredentialCollection.js.map +1 -0
- package/lib/helpers/initLogCollection.d.ts +11 -0
- package/lib/helpers/initLogCollection.js +26 -0
- package/lib/helpers/initLogCollection.js.map +1 -0
- package/lib/helpers/initServer.d.ts +45 -0
- package/lib/helpers/initServer.js +293 -0
- package/lib/helpers/initServer.js.map +1 -0
- package/lib/helpers/parseUserAgent.d.ts +17 -0
- package/lib/helpers/parseUserAgent.js +108 -0
- package/lib/helpers/parseUserAgent.js.map +1 -0
- package/lib/helpers/visitEndpointOnAnotherServer/index.d.ts +18 -0
- package/lib/helpers/visitEndpointOnAnotherServer/index.js +156 -0
- package/lib/helpers/visitEndpointOnAnotherServer/index.js.map +1 -0
- package/lib/helpers/visitEndpointOnAnotherServer/sendServerToServerRequest.d.ts +23 -0
- package/lib/helpers/visitEndpointOnAnotherServer/sendServerToServerRequest.js +168 -0
- package/lib/helpers/visitEndpointOnAnotherServer/sendServerToServerRequest.js.map +1 -0
- package/lib/html/genErrorPage.d.ts +19 -0
- package/lib/html/genErrorPage.js +27 -0
- package/lib/html/genErrorPage.js.map +1 -0
- package/lib/html/genInfoPage.d.ts +13 -0
- package/lib/html/genInfoPage.js +16 -0
- package/lib/html/genInfoPage.js.map +1 -0
- package/lib/index.d.ts +11 -0
- package/lib/index.js +68 -0
- package/lib/index.js.map +1 -0
- package/lib/types/CrossServerCredential.d.ts +11 -0
- package/lib/types/CrossServerCredential.js +3 -0
- package/lib/types/CrossServerCredential.js.map +1 -0
- package/lib/types/ExpressKitErrorCode.d.ts +31 -0
- package/lib/types/ExpressKitErrorCode.js +38 -0
- package/lib/types/ExpressKitErrorCode.js.map +1 -0
- package/package.json +53 -0
- package/src/constants/LOG_REVIEW_ROUTE_PATH_PREFIX.ts +9 -0
- package/src/constants/LOG_REVIEW_STATUS_ROUTE.ts +10 -0
- package/src/constants/LOG_ROUTE_PATH.ts +9 -0
- package/src/constants/ROUTE_PATH_PREFIX.ts +7 -0
- package/src/errors/ErrorWithCode.tsx +15 -0
- package/src/helpers/addDBEditorEndpoints/generateEndpointPath.ts +16 -0
- package/src/helpers/addDBEditorEndpoints/index.ts +130 -0
- package/src/helpers/dataSigner.ts +306 -0
- package/src/helpers/genRouteHandler.ts +914 -0
- package/src/helpers/handleError.ts +66 -0
- package/src/helpers/handleSuccess.ts +18 -0
- package/src/helpers/initCrossServerCredentialCollection.ts +19 -0
- package/src/helpers/initLogCollection.ts +31 -0
- package/src/helpers/initServer.ts +284 -0
- package/src/helpers/parseUserAgent.ts +108 -0
- package/src/helpers/visitEndpointOnAnotherServer/index.ts +157 -0
- package/src/helpers/visitEndpointOnAnotherServer/sendServerToServerRequest.ts +164 -0
- package/src/html/genErrorPage.ts +144 -0
- package/src/html/genInfoPage.ts +101 -0
- package/src/index.ts +125 -0
- package/src/types/CrossServerCredential.ts +16 -0
- package/src/types/ExpressKitErrorCode.ts +37 -0
- 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
|
+
LOG_REVIEW_ROUTE_PATH_PREFIX,
|
|
13
|
+
LOG_ROUTE_PATH,
|
|
14
|
+
LOG_REVIEW_STATUS_ROUTE,
|
|
15
|
+
Log,
|
|
16
|
+
} from 'dce-reactkit';
|
|
17
|
+
|
|
18
|
+
// Import shared helpers
|
|
19
|
+
import genRouteHandler from './genRouteHandler';
|
|
20
|
+
|
|
21
|
+
// Import shared types
|
|
22
|
+
import ExpressKitErrorCode from '../types/ExpressKitErrorCode';
|
|
23
|
+
import CrossServerCredential from '../types/CrossServerCredential';
|
|
24
|
+
|
|
25
|
+
// Stored copy of dce-mango log collection
|
|
26
|
+
let _logCollection: Collection<Log>;
|
|
27
|
+
|
|
28
|
+
// Stored copy of dce-mango cross-server credential collection
|
|
29
|
+
let _crossServerCredentialCollection: Collection<CrossServerCredential>;
|
|
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<Log>,
|
|
84
|
+
crossServerCredentialCollection?: Collection<CrossServerCredential>,
|
|
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;
|