dce-reactkit 3.9.2 → 3.9.4-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.
@@ -44,6 +44,30 @@ export declare const confirm: (title: string, text: string, opts?: {
44
44
  cancelButtonText?: string;
45
45
  cancelButtonVariant?: Variant;
46
46
  }) => Promise<boolean>;
47
+ /**
48
+ * Show a prompt modal asking the user for input
49
+ * @author Yuen Ler Chow
50
+ * @param title the title text to display at the top of the prompt
51
+ * @param text the text to display in the prompt
52
+ * @param [opts={}] additional options for the prompt dialog
53
+ * @param [opts.placeholder] the placeholder text for the input field
54
+ * @param [opts.defaultText] the default text for the input field
55
+ * @param [opts.confirmButtonText=Okay] the text of the confirm button
56
+ * @param [opts.confirmButtonVariant=Variant.Dark] the variant of the confirm button
57
+ * @param [opts.cancelButtonText=Cancel] the text of the cancel button
58
+ * @param [opts.cancelButtonVariant=Variant.Secondary] the variant of the cancel button
59
+ * @returns Promise that resolves with the input string or null if canceled
60
+ */
61
+ export declare const prompt: (title: string, text: string, currentInputFieldText: string, opts?: {
62
+ placeholder?: string | undefined;
63
+ defaultText?: string | undefined;
64
+ confirmButtonText?: string | undefined;
65
+ confirmButtonVariant?: Variant | undefined;
66
+ cancelButtonText?: string | undefined;
67
+ cancelButtonVariant?: Variant | undefined;
68
+ minNumChars?: number | undefined;
69
+ findValidationError?: ((text: string) => string | undefined) | undefined;
70
+ } | undefined) => Promise<string | null>;
47
71
  /**
48
72
  * Show a fatal error message
49
73
  * @author Gabe Abrams
@@ -9,6 +9,12 @@ import LogFunction from '../types/LogFunction';
9
9
  * @param opts.handler function that processes the request
10
10
  * @param [opts.skipSessionCheck] if true, skip the session check (allow users
11
11
  * to not be logged in and launched via LTI)
12
+ * @param [opts.allowedHosts] if included, only allow requests from these hosts
13
+ * (start a hostname with a "*" to only check the end of the hostname)
14
+ * you can include just one string instead of an array
15
+ * @param [opts.bannedHosts] if included, do not allow requests from these hosts
16
+ * (start a hostname with a "*" to only check the end of the hostname)
17
+ * you can include just one string instead of an array
12
18
  * @param [opts.unhandledErrorMessagePrefix] if included, when an error that
13
19
  * is not of type ErrorWithCode is thrown, the client will receive an error
14
20
  * where the error message is prefixed with this string. For example,
@@ -63,6 +69,8 @@ declare const genRouteHandler: (opts: {
63
69
  logServerEvent: LogFunction;
64
70
  }) => any;
65
71
  skipSessionCheck?: boolean | undefined;
72
+ allowedHosts?: string | string[] | undefined;
73
+ bannedHosts?: string | string[] | undefined;
66
74
  unhandledErrorMessagePrefix?: string | undefined;
67
75
  }) => (req: any, res: any, next: () => void) => Promise<undefined>;
68
76
  export default genRouteHandler;
@@ -4,6 +4,7 @@
4
4
  */
5
5
  declare enum ModalType {
6
6
  Okay = "okay",
7
+ Cancel = "cancel",
7
8
  OkayCancel = "okay-cancel",
8
9
  YesNo = "yes-no",
9
10
  YesNoCancel = "yes-no-cancel",
@@ -8,6 +8,8 @@ declare enum ReactKitErrorCode {
8
8
  SessionExpired = "DRK3",
9
9
  MissingParameter = "DRK4",
10
10
  InvalidParameter = "DRK5",
11
+ HostNotAllowed = "DRK17",
12
+ HostBanned = "DRK18",
11
13
  WrongCourse = "DRK6",
12
14
  NoCACCLSendRequestFunction = "DRK7",
13
15
  NoCACCLGetLaunchInfoFunction = "DRK8",
package/dist/index.d.ts CHANGED
@@ -131,6 +131,7 @@ declare enum ModalSize {
131
131
  */
132
132
  declare enum ModalType {
133
133
  Okay = "okay",
134
+ Cancel = "cancel",
134
135
  OkayCancel = "okay-cancel",
135
136
  YesNo = "yes-no",
136
137
  YesNoCancel = "yes-no-cancel",
@@ -1044,6 +1045,12 @@ type LogFunction = (opts: ({
1044
1045
  * @param opts.handler function that processes the request
1045
1046
  * @param [opts.skipSessionCheck] if true, skip the session check (allow users
1046
1047
  * to not be logged in and launched via LTI)
1048
+ * @param [opts.allowedHosts] if included, only allow requests from these hosts
1049
+ * (start a hostname with a "*" to only check the end of the hostname)
1050
+ * you can include just one string instead of an array
1051
+ * @param [opts.bannedHosts] if included, do not allow requests from these hosts
1052
+ * (start a hostname with a "*" to only check the end of the hostname)
1053
+ * you can include just one string instead of an array
1047
1054
  * @param [opts.unhandledErrorMessagePrefix] if included, when an error that
1048
1055
  * is not of type ErrorWithCode is thrown, the client will receive an error
1049
1056
  * where the error message is prefixed with this string. For example,
@@ -1098,6 +1105,8 @@ declare const genRouteHandler: (opts: {
1098
1105
  logServerEvent: LogFunction;
1099
1106
  }) => any;
1100
1107
  skipSessionCheck?: boolean | undefined;
1108
+ allowedHosts?: string | string[] | undefined;
1109
+ bannedHosts?: string | string[] | undefined;
1101
1110
  unhandledErrorMessagePrefix?: string | undefined;
1102
1111
  }) => (req: any, res: any, next: () => void) => Promise<undefined>;
1103
1112
 
@@ -1637,6 +1646,8 @@ declare enum ReactKitErrorCode {
1637
1646
  SessionExpired = "DRK3",
1638
1647
  MissingParameter = "DRK4",
1639
1648
  InvalidParameter = "DRK5",
1649
+ HostNotAllowed = "DRK17",
1650
+ HostBanned = "DRK18",
1640
1651
  WrongCourse = "DRK6",
1641
1652
  NoCACCLSendRequestFunction = "DRK7",
1642
1653
  NoCACCLGetLaunchInfoFunction = "DRK8",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dce-reactkit",
3
- "version": "3.9.2",
3
+ "version": "3.9.4-beta.1",
4
4
  "description": "Shared components for Harvard DCE apps",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -240,6 +240,124 @@ export const confirm = async (
240
240
  });
241
241
  };
242
242
 
243
+ /*----------------------------------------*/
244
+ /* --------------- Prompt -------------- */
245
+ /*----------------------------------------*/
246
+
247
+ // Stored copies of setters
248
+ let setPromptInfo: (
249
+ info:
250
+ | undefined
251
+ | {
252
+ title: string;
253
+ text: string;
254
+ currentInputFieldText: string,
255
+ opts: {
256
+ placeholder?: string,
257
+ defaultText?: string,
258
+ confirmButtonText?: string,
259
+ confirmButtonVariant?: Variant,
260
+ cancelButtonText?: string,
261
+ cancelButtonVariant?: Variant,
262
+ minNumChars?: number,
263
+ findValidationError?: (text: string) => string | undefined,
264
+ },
265
+ },
266
+ ) => void;
267
+
268
+ // Function to call when prompt is closed
269
+ let onPromptClosed: (result: string | null) => void;
270
+
271
+ /**
272
+ * Show a prompt modal asking the user for input
273
+ * @author Yuen Ler Chow
274
+ * @param title the title text to display at the top of the prompt
275
+ * @param text the text to display in the prompt
276
+ * @param [opts={}] additional options for the prompt dialog
277
+ * @param [opts.placeholder] the placeholder text for the input field
278
+ * @param [opts.defaultText] the default text for the input field
279
+ * @param [opts.confirmButtonText=Okay] the text of the confirm button
280
+ * @param [opts.confirmButtonVariant=Variant.Dark] the variant of the confirm button
281
+ * @param [opts.cancelButtonText=Cancel] the text of the cancel button
282
+ * @param [opts.cancelButtonVariant=Variant.Secondary] the variant of the cancel button
283
+ * @returns Promise that resolves with the input string or null if canceled
284
+ */
285
+ export const prompt = async (
286
+ title: string,
287
+ text: string,
288
+ currentInputFieldText: string,
289
+ opts?: {
290
+ placeholder?: string,
291
+ defaultText?: string,
292
+ confirmButtonText?: string,
293
+ confirmButtonVariant?: Variant,
294
+ cancelButtonText?: string,
295
+ cancelButtonVariant?: Variant,
296
+ minNumChars?: number,
297
+ findValidationError?: (text: string) => string | undefined,
298
+ },
299
+ ): Promise<string | null> => {
300
+ // Wait for helper to exist
301
+ await waitForHelper(() => {
302
+ return !!setPromptInfo;
303
+ });
304
+
305
+ // Fallback if prompt is not available
306
+ if (!setPromptInfo) {
307
+ const resultPassesValidation = false;
308
+ while (!resultPassesValidation) {
309
+ // eslint-disable-next-line no-alert
310
+ const result = window.prompt(
311
+ `${title}\n\n${text}`,
312
+ opts?.defaultText ?? '',
313
+ );
314
+
315
+ if (result === null) {
316
+ return null;
317
+ }
318
+
319
+ // Validate min num chars
320
+ const minNumCharsValidationError = (
321
+ (opts?.minNumChars && result.length < opts.minNumChars)
322
+ ? `Please enter at least ${opts.minNumChars} characters.`
323
+ : undefined
324
+ );
325
+
326
+ // Run custom validation
327
+ const customValidationError = (
328
+ opts?.findValidationError
329
+ && opts.findValidationError(result)
330
+ );
331
+
332
+ // Show validation issue
333
+ if (minNumCharsValidationError || customValidationError) {
334
+ alert(
335
+ 'Invalid Input',
336
+ `${minNumCharsValidationError ?? ''} ${customValidationError ?? ''}`,
337
+ );
338
+ } else {
339
+ return result;
340
+ }
341
+ }
342
+ }
343
+
344
+ // Return promise that resolves with result of prompt
345
+ return new Promise((resolve) => {
346
+ // Setup handler
347
+ onPromptClosed = (result: string | null) => {
348
+ resolve(result);
349
+ };
350
+
351
+ // Show the prompt
352
+ setPromptInfo({
353
+ title,
354
+ text,
355
+ currentInputFieldText,
356
+ opts: (opts ?? {}),
357
+ });
358
+ });
359
+ };
360
+
243
361
  /*----------------------------------------*/
244
362
  /* ------------- Fatal Error ------------ */
245
363
  /*----------------------------------------*/
@@ -465,6 +583,27 @@ const AppWrapper: React.FC<Props> = (props: Props): React.ReactElement => {
465
583
  >(undefined);
466
584
  setConfirmInfo = setConfirmInfoInner;
467
585
 
586
+ // Prompt
587
+ const [promptInfo, setPromptInfoInner] = useState<
588
+ | undefined
589
+ | {
590
+ title: string,
591
+ text: string,
592
+ currentInputFieldText: string,
593
+ opts: {
594
+ placeholder?: string,
595
+ defaultText?: string,
596
+ confirmButtonText?: string,
597
+ confirmButtonVariant?: Variant,
598
+ cancelButtonText?: string,
599
+ cancelButtonVariant?: Variant,
600
+ minNumChars?: number,
601
+ findValidationError?: (text: string) => string | undefined,
602
+ }
603
+ }
604
+ >(undefined);
605
+ setPromptInfo = setPromptInfoInner;
606
+
468
607
  // Session expired
469
608
  const [
470
609
  sessionHasExpired,
@@ -531,6 +670,83 @@ const AppWrapper: React.FC<Props> = (props: Props): React.ReactElement => {
531
670
  );
532
671
  }
533
672
 
673
+ /* ------------- Prompt ------------ */
674
+
675
+ if (promptInfo) {
676
+ // Run min char validation
677
+ const minNumCharsValidationError = (
678
+ (
679
+ promptInfo.opts.minNumChars
680
+ && promptInfo.currentInputFieldText.length < promptInfo.opts.minNumChars
681
+ )
682
+ ? `Please enter at least ${promptInfo.opts.minNumChars} characters.`
683
+ : undefined
684
+ );
685
+
686
+ // Run custom validation
687
+ const customValidationError = (
688
+ promptInfo.opts.findValidationError
689
+ && promptInfo.opts.findValidationError(promptInfo.currentInputFieldText)
690
+ );
691
+
692
+ modal = (
693
+ <ModalForWrapper
694
+ key={`prompt-${promptInfo.title}-${promptInfo.text}`}
695
+ title={promptInfo.title}
696
+ // only show the cancel button if there is a validation error
697
+ type={(
698
+ (customValidationError || minNumCharsValidationError)
699
+ ? ModalType.Cancel
700
+ : ModalType.OkayCancel
701
+ )}
702
+ okayLabel={promptInfo.opts.confirmButtonText}
703
+ okayVariant={promptInfo.opts.confirmButtonVariant}
704
+ cancelLabel={promptInfo.opts.cancelButtonText}
705
+ cancelVariant={promptInfo.opts.cancelButtonVariant}
706
+ onClose={(buttonType) => {
707
+ const result = (
708
+ buttonType === ModalButtonType.Okay
709
+ ? promptInfo.currentInputFieldText
710
+ : null
711
+ );
712
+
713
+ setPromptInfo(undefined);
714
+
715
+ if (onPromptClosed) {
716
+ onPromptClosed(result);
717
+ }
718
+ }}
719
+ onTopOfOtherModals
720
+ dontAllowBackdropExit
721
+ >
722
+ <div className="d-flex flex-column align-items-center">
723
+ <p>{promptInfo.text}</p>
724
+ <input
725
+ type="text"
726
+ placeholder={promptInfo.opts.placeholder}
727
+ value={promptInfo.currentInputFieldText}
728
+ onChange={(e) => {
729
+ return setPromptInfo({
730
+ ...promptInfo,
731
+ currentInputFieldText: e.target.value,
732
+ });
733
+ }}
734
+ />
735
+ {minNumCharsValidationError && (
736
+ <div className="text-danger bg-danger bg-opacity-25 p-2 m-1 rounded">
737
+ {minNumCharsValidationError}
738
+ </div>
739
+ )}
740
+ {customValidationError && (
741
+ <div className="text-danger bg-danger bg-opacity-25 p-2 m-1 rounded">
742
+ {customValidationError}
743
+ </div>
744
+ )}
745
+ </div>
746
+ </ModalForWrapper>
747
+ );
748
+ }
749
+
534
750
  /* ------ Custom Modal Portals ------ */
535
751
 
536
752
  // Custom modal portals
@@ -49,6 +49,9 @@ const modalTypeToModalButtonTypes: {
49
49
  [ModalType.Okay]: [
50
50
  ModalButtonType.Okay,
51
51
  ],
52
+ [ModalType.Cancel]: [
53
+ ModalButtonType.Cancel,
54
+ ],
52
55
  [ModalType.OkayCancel]: [
53
56
  ModalButtonType.Okay,
54
57
  ModalButtonType.Cancel,
@@ -39,6 +39,12 @@ import LogLevel from '../types/LogLevel';
39
39
  * @param opts.handler function that processes the request
40
40
  * @param [opts.skipSessionCheck] if true, skip the session check (allow users
41
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
42
48
  * @param [opts.unhandledErrorMessagePrefix] if included, when an error that
43
49
  * is not of type ErrorWithCode is thrown, the client will receive an error
44
50
  * where the error message is prefixed with this string. For example,
@@ -102,6 +108,8 @@ const genRouteHandler = (
102
108
  },
103
109
  ) => any,
104
110
  skipSessionCheck?: boolean,
111
+ allowedHosts?: string[] | string,
112
+ bannedHosts?: string[] | string,
105
113
  unhandledErrorMessagePrefix?: string,
106
114
  },
107
115
  ) => {
@@ -110,6 +118,97 @@ const genRouteHandler = (
110
118
  // Output params
111
119
  const output: { [k in string]: any } = {};
112
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
+
113
212
  /*----------------------------------------*/
114
213
  /* ------------ Parse Params ------------ */
115
214
  /*----------------------------------------*/
@@ -4,6 +4,7 @@
4
4
  */
5
5
  enum ModalType {
6
6
  Okay = 'okay', // [Okay]
7
+ Cancel = 'cancel', // [Cancel]
7
8
  OkayCancel = 'okay-cancel', // [Okay] [Cancel]
8
9
  YesNo = 'yes-no', // [Yes] [No]
9
10
  YesNoCancel = 'yes-no-cancel', // [Yes] [No] [Cancel]
@@ -1,4 +1,4 @@
1
- // Highest error code = DRK16
1
+ // Highest error code = DRK18
2
2
 
3
3
  /**
4
4
  * List of error codes built into the react kit
@@ -10,6 +10,8 @@ enum ReactKitErrorCode {
10
10
  SessionExpired = 'DRK3',
11
11
  MissingParameter = 'DRK4',
12
12
  InvalidParameter = 'DRK5',
13
+ HostNotAllowed = 'DRK17',
14
+ HostBanned = 'DRK18',
13
15
  WrongCourse = 'DRK6',
14
16
  NoCACCLSendRequestFunction = 'DRK7',
15
17
  NoCACCLGetLaunchInfoFunction = 'DRK8',