• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

opengovsg / FormSG / 12740798736

13 Jan 2025 04:57AM UTC coverage: 72.433% (-0.03%) from 72.465%
12740798736

push

github

Kevin Foong
fix: missing typing

2729 of 4598 branches covered (59.35%)

Branch coverage included in aggregate %.

9970 of 12934 relevant lines covered (77.08%)

44.93 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.03
/src/app/modules/form/admin-form/admin-form.controller.ts
1
import JoiDate from '@joi/date'
8✔
2
import axios from 'axios'
8✔
3
import { ObjectId } from 'bson'
8✔
4
import { celebrate, Joi as BaseJoi, Segments } from 'celebrate'
8✔
5
import { NextFunction } from 'express'
6
import { AuthedSessionData } from 'express-session'
7
import { StatusCodes } from 'http-status-codes'
8✔
8
import JSONStream from 'JSONStream'
8✔
9
import multer from 'multer'
8✔
10
import { ResultAsync } from 'neverthrow'
8✔
11

12
import {
8✔
13
  MAX_UPLOAD_FILE_SIZE,
14
  VALID_UPLOAD_FILE_TYPES,
15
} from '../../../../../shared/constants/file'
16
import {
8✔
17
  AdminDashboardFormMetaDto,
18
  BasicField,
19
  CreateFormBodyDto,
20
  DeserializeTransform,
21
  DuplicateFormBodyDto,
22
  EndPageUpdateDto,
23
  ErrorDto,
24
  FieldCreateDto,
25
  FieldUpdateDto,
26
  FormAuthType,
27
  FormColorTheme,
28
  FormDto,
29
  FormFeedbackMetaDto,
30
  FormFieldDto,
31
  FormLogoState,
32
  FormResponseMode,
33
  FormSettings,
34
  FormWebhookResponseModeSettings,
35
  FormWebhookSettings,
36
  FormWorkflowDto,
37
  FormWorkflowStepDto,
38
  Language,
39
  LogicConditionState,
40
  LogicDto,
41
  LogicIfValue,
42
  LogicType,
43
  PermissionsUpdateDto,
44
  PreviewFormViewDto,
45
  PrivateFormErrorDto,
46
  PublicFormDto,
47
  SettingsUpdateDto,
48
  StartPageUpdateDto,
49
  SubmissionCountQueryDto,
50
  WebhookSettingsUpdateDto,
51
} from '../../../../../shared/types'
52
import {
8✔
53
  EncryptedStringsMessageContent,
54
  encryptStringsMessage,
55
} from '../../../../../shared/utils/crypto'
56
import { IFormDocument, IPopulatedForm } from '../../../../types'
57
import {
58
  EncryptSubmissionDto,
59
  FormUpdateParams,
60
  ParsedEmailModeSubmissionBody,
61
} from '../../../../types/api'
62
import { goGovConfig } from '../../../config/features/gogov.config'
8✔
63
import { createLoggerWithLabel } from '../../../config/logger'
8✔
64
import MailService from '../../../services/mail/mail.service'
8✔
65
import { createReqMeta } from '../../../utils/request'
8✔
66
import * as AuthService from '../../auth/auth.service'
8✔
67
import {
68
  DatabaseConflictError,
69
  DatabaseError,
70
  DatabasePayloadSizeError,
71
  DatabaseValidationError,
72
} from '../../core/core.errors'
73
import { ControllerHandler } from '../../core/core.types'
74
import * as FeedbackService from '../../feedback/feedback.service'
8✔
75
import * as EmailSubmissionMiddleware from '../../submission/email-submission/email-submission.middleware'
8✔
76
import * as EmailSubmissionService from '../../submission/email-submission/email-submission.service'
8✔
77
import {
8✔
78
  mapRouteError as mapEmailSubmissionError,
79
  SubmissionEmailObj,
80
} from '../../submission/email-submission/email-submission.util'
81
import * as EncryptSubmissionService from '../../submission/encrypt-submission/encrypt-submission.service'
8✔
82
import ParsedResponsesObject from '../../submission/ParsedResponsesObject.class'
8✔
83
import * as ReceiverMiddleware from '../../submission/receiver/receiver.middleware'
8✔
84
import * as SubmissionService from '../../submission/submission.service'
8✔
85
import {
8✔
86
  extractEmailConfirmationData,
87
  mapAttachmentsFromResponses,
88
  mapRouteError as mapSubmissionError,
89
} from '../../submission/submission.utils'
90
import * as UserService from '../../user/user.service'
8✔
91
import { removeFormsFromAllWorkspaces } from '../../workspace/workspace.service'
8✔
92
import { PrivateFormError } from '../form.errors'
8✔
93
import * as FormService from '../form.service'
8✔
94

95
import {
8✔
96
  PREVIEW_CORPPASS_UID,
97
  PREVIEW_CORPPASS_UINFIN,
98
  PREVIEW_SINGPASS_UINFIN,
99
} from './admin-form.constants'
100
import { EditFieldError, GoGovServerError } from './admin-form.errors'
8✔
101
import {
8✔
102
  createWorkflowStepValidator,
103
  getWebhookSettingsValidator,
104
  updateSettingsValidator,
105
  updateWebhookSettingsValidator,
106
  updateWorkflowStepValidator,
107
} from './admin-form.middlewares'
108
import * as AdminFormService from './admin-form.service'
8✔
109
import { PermissionLevel } from './admin-form.types'
8✔
110
import {
8✔
111
  mapGoGovErrors,
112
  mapRouteError,
113
  verifyValidUnicodeString,
114
} from './admin-form.utils'
115

116
// NOTE: Refer to this for documentation: https://github.com/sideway/joi-date/blob/master/API.md
117
const Joi = BaseJoi.extend(JoiDate) as typeof BaseJoi
8✔
118

119
const logger = createLoggerWithLabel(module)
8✔
120

121
// Validators
122
const createFormValidator = celebrate({
8✔
123
  [Segments.BODY]: {
124
    form: BaseJoi.object<CreateFormBodyDto>()
125
      .keys({
126
        // Require valid responsesMode field.
127
        responseMode: Joi.string()
128
          .valid(...Object.values(FormResponseMode))
129
          .required(),
130
        // Require title field.
131
        title: Joi.string().min(4).max(200).required(),
132
        // Require emails string (for backwards compatibility) or string
133
        // array if form to be created in Email mode.
134
        // Must be string array (which can be empty) if form is to be created in Encrypt mode.
135
        emails: Joi.when('responseMode', {
136
          switch: [
137
            {
138
              is: FormResponseMode.Email,
139
              then: Joi.alternatives()
140
                .try(
141
                  Joi.array().items(Joi.string().email()).min(1),
142
                  Joi.string().email(),
143
                )
144
                .required(),
145
            },
146
            {
147
              is: FormResponseMode.Encrypt,
148
              then: Joi.array().items(Joi.string().email()).required(),
149
            },
150
            {
151
              is: FormResponseMode.Multirespondent,
152
              then: Joi.forbidden(),
153
            },
154
          ],
155
          otherwise: Joi.forbidden(),
156
        }),
157
        // Require publicKey field if form to be created in Storage mode or
158
        // Multirespondent mode
159
        publicKey: Joi.when('responseMode', {
160
          is: [FormResponseMode.Encrypt, FormResponseMode.Multirespondent],
161
          then: Joi.string().required().disallow(''),
162
          otherwise: Joi.forbidden(),
163
        }),
164
        workspaceId: Joi.string(),
165
      })
166
      .required()
167
      // Allow other form schema keys to be passed for form creation.
168
      .unknown(true)
169
      .custom((value, helpers) => verifyValidUnicodeString(value, helpers)),
7✔
170
  },
171
})
172

173
const duplicateFormValidator = celebrate({
8✔
174
  // uses CreateFormBodyDto as that is the shape of the data used in client's WorkspaceService
175
  [Segments.BODY]: BaseJoi.object<DuplicateFormBodyDto>({
176
    // Require valid responsesMode field.
177
    responseMode: Joi.string()
178
      .valid(...Object.values(FormResponseMode))
179
      .required(),
180
    // Require title field.
181
    title: Joi.string().min(4).max(200).required(),
182
    // Require emails string (for backwards compatibility) or string array
183
    // if form to be duplicated in Email mode.
184
    emails: Joi.when('responseMode', {
185
      switch: [
186
        {
187
          is: FormResponseMode.Email,
188
          then: Joi.alternatives()
189
            .try(
190
              Joi.array().items(Joi.string().email()).min(1),
191
              Joi.string().email(),
192
            )
193
            .required(),
194
        },
195
        {
196
          // When duplicating forms, we reset the `emails` field to be empty
197
          // Refer to `admin-form.utils.ts`.
198
          is: FormResponseMode.Encrypt,
199
          then: Joi.forbidden(),
200
        },
201
        {
202
          is: FormResponseMode.Multirespondent,
203
          then: Joi.forbidden(),
204
        },
205
      ],
206
      otherwise: Joi.forbidden(),
207
    }),
208
    // Require publicKey field if form to be duplicated in Storage mode or
209
    // Multirespondent mode
210
    publicKey: Joi.when('responseMode', {
211
      is: [FormResponseMode.Encrypt, FormResponseMode.Multirespondent],
212
      then: Joi.string().required().disallow(''),
213
      otherwise: Joi.forbidden(),
214
    }),
215
    workspaceId: Joi.string(),
216
  }),
217
})
218

219
const transferFormOwnershipValidator = celebrate({
8✔
220
  [Segments.BODY]: {
221
    email: Joi.string()
222
      .required()
223
      .email()
224
      .message('Please enter a valid email')
225
      .lowercase(),
226
  },
227
})
228

229
const fileUploadValidator = celebrate({
8✔
230
  [Segments.BODY]: {
231
    fileId: Joi.string().required(),
232
    fileMd5Hash: Joi.string().base64().required(),
233
    fileType: Joi.string()
234
      .valid(...VALID_UPLOAD_FILE_TYPES)
235
      .required(),
236
    isNewClient: Joi.boolean().optional(), // TODO(#4228): isNewClient in param was allowed for backward compatibility after #4213 removed isNewClient flag from frontend. To remove 2 weeks after release.
237
  },
238
})
239

240
/**
241
 * Handler for GET /adminform endpoint.
242
 * @security session
243
 *
244
 * @returns 200 with list of forms user can access when list is retrieved successfully
245
 * @returns 422 when user of given id cannnot be found in the database
246
 * @returns 500 when database errors occur
247
 */
248
export const handleListDashboardForms: ControllerHandler<
8✔
249
  unknown,
250
  AdminDashboardFormMetaDto[] | ErrorDto
251
> = async (req, res) => {
8✔
252
  const authedUserId = (req.session as AuthedSessionData).user._id
7✔
253

254
  return AdminFormService.getDashboardForms(authedUserId)
7✔
255
    .map((dashboardView) => res.json(dashboardView))
3✔
256
    .mapErr((error) => {
257
      logger.error({
4✔
258
        message: 'Error listing dashboard forms',
259
        meta: {
260
          action: 'handleListDashboardForms',
261
          userId: authedUserId,
262
        },
263
        error,
264
      })
265
      const { errorMessage, statusCode } = mapRouteError(error)
4✔
266
      return res.status(statusCode).json({ message: errorMessage })
4✔
267
    })
268
}
269

270
/**
271
 * Handler for GET /:formId/adminform.
272
 * @security session
273
 *
274
 * @returns 200 with retrieved form with formId if user has read permissions
275
 * @returns 403 when user does not have permissions to access form
276
 * @returns 404 when form cannot be found
277
 * @returns 410 when form is archived
278
 * @returns 422 when user in session cannot be retrieved from the database
279
 * @returns 500 when database error occurs
280
 */
281
export const handleGetAdminForm: ControllerHandler<{ formId: string }> = (
8✔
282
  req,
283
  res,
284
) => {
285
  const { formId } = req.params
15✔
286
  const sessionUserId = (req.session as AuthedSessionData).user._id
15✔
287

288
  return (
15✔
289
    // Step 1: Retrieve currently logged in user.
290
    UserService.getPopulatedUserById(sessionUserId)
291
      .andThen((user) =>
292
        // Step 2: Check whether user has read permissions to form
293
        AuthService.getFormAfterPermissionChecks({
12✔
294
          user,
295
          formId,
296
          level: PermissionLevel.Read,
297
        }),
298
      )
299
      .map((form) => res.status(StatusCodes.OK).json({ form }))
4✔
300
      .mapErr((error) => {
301
        logger.error({
11✔
302
          message: 'Error retrieving single form',
303
          meta: {
304
            action: 'handleGetSingleForm',
305
            ...createReqMeta(req),
306
          },
307
          error,
308
        })
309

310
        const { statusCode, errorMessage } = mapRouteError(error)
11✔
311
        return res.status(statusCode).json({ message: errorMessage })
11✔
312
      })
313
  )
314
}
315

316
/**
317
 * Handler for GET /api/v3/admin/forms/:formId/collaborators
318
 * @security session
319
 *
320
 * @returns 200 with collaborators
321
 * @returns 403 when current user does not have read permissions for the form
322
 * @returns 404 when form cannot be found
323
 * @returns 410 when retrieving collaborators for an archived form
324
 * @returns 422 when user in session cannot be retrieved from the database
325
 * @returns 500 when database error occurs
326
 */
327
export const handleGetFormCollaborators: ControllerHandler<
8✔
328
  { formId: string },
329
  PermissionsUpdateDto | ErrorDto
330
> = (req, res) => {
8✔
331
  const { formId } = req.params
12✔
332
  const sessionUserId = (req.session as AuthedSessionData).user._id
12✔
333

334
  return (
12✔
335
    // Step 1: Retrieve currently logged in user.
336
    UserService.getPopulatedUserById(sessionUserId)
337
      .andThen((user) =>
338
        // Step 2: Check whether user has read permissions to form
339
        AuthService.getFormAfterPermissionChecks({
8✔
340
          user,
341
          formId,
342
          level: PermissionLevel.Read,
343
        }),
344
      )
345
      .map(({ permissionList }) =>
346
        res.status(StatusCodes.OK).send(permissionList),
2✔
347
      )
348
      .mapErr((error) => {
349
        logger.error({
10✔
350
          message: 'Error retrieving form collaborators',
351
          meta: {
352
            action: 'handleGetFormCollaborators',
353
            ...createReqMeta(req),
354
          },
355
          error,
356
        })
357

358
        const { statusCode, errorMessage } = mapRouteError(error)
10✔
359
        return res.status(statusCode).json({ message: errorMessage })
10✔
360
      })
361
  )
362
}
363

364
/**
365
 * Handler for GET /:formId/adminform/preview.
366
 * @security session
367
 *
368
 * @returns 200 with form with private details scrubbed for previewing if user has read permissions
369
 * @returns 403 when user does not have permissions to access form
370
 * @returns 404 when form cannot be found
371
 * @returns 410 when form is archived
372
 * @returns 422 when user in session cannot be retrieved from the database
373
 * @returns 500 when database error occurs
374
 */
375
export const handlePreviewAdminForm: ControllerHandler<{ formId: string }> = (
8✔
376
  req,
377
  res,
378
) => {
379
  const { formId } = req.params
14✔
380
  const sessionUserId = (req.session as AuthedSessionData).user._id
14✔
381
  return (
14✔
382
    // Step 1: Retrieve currently logged in user.
383
    UserService.getPopulatedUserById(sessionUserId)
384
      .andThen((user) =>
385
        // Step 2: Check whether user has read permissions to form
386
        AuthService.getFormAfterPermissionChecks({
11✔
387
          user,
388
          formId,
389
          level: PermissionLevel.Read,
390
        }),
391
      )
392
      // Step 3: Remove private details from form for previewing.
393
      .map((populatedForm) => populatedForm.getPublicView())
3✔
394
      .map((scrubbedForm) =>
395
        res.status(StatusCodes.OK).json({ form: scrubbedForm }),
3✔
396
      )
397
      .mapErr((error) => {
398
        logger.error({
11✔
399
          message: 'Error previewing admin form',
400
          meta: {
401
            action: 'handlePreviewAdminForm',
402
            ...createReqMeta(req),
403
            userId: sessionUserId,
404
            formId,
405
          },
406
          error,
407
        })
408
        const { errorMessage, statusCode } = mapRouteError(error)
11✔
409
        return res.status(statusCode).json({ message: errorMessage })
11✔
410
      })
411
  )
412
}
413

414
/**
415
 * Handler for POST /:formId([a-fA-F0-9]{24})/adminform/images.
416
 * @security session
417
 *
418
 * @returns 200 with presigned POST URL object
419
 * @returns 400 when error occurs whilst creating presigned POST URL object
420
 * @returns 403 when user does not have write permissions for form
421
 * @returns 404 when form cannot be found
422
 * @returns 410 when form is archived
423
 * @returns 422 when user in session cannot be retrieved from the database
424
 */
425
export const createPresignedPostUrlForImages: ControllerHandler<
8✔
426
  { formId: string },
427
  unknown,
428
  {
429
    fileId: string
430
    fileMd5Hash: string
431
    fileType: string
432
  }
433
> = async (req, res) => {
13✔
434
  const { formId } = req.params
13✔
435
  const { fileId, fileMd5Hash, fileType } = req.body
13✔
436
  const sessionUserId = (req.session as AuthedSessionData).user._id
13✔
437

438
  // Adding random objectId ensures fileId is unpredictable by client
439
  const randomizedFileId = `${String(new ObjectId())}-${fileId}`
13✔
440

441
  return (
13✔
442
    // Step 1: Retrieve currently logged in user.
443
    UserService.getPopulatedUserById(sessionUserId)
444
      .andThen((user) =>
445
        // Step 2: Check whether user has write permissions to form
446
        AuthService.getFormAfterPermissionChecks({
11✔
447
          user,
448
          formId,
449
          level: PermissionLevel.Write,
450
        }),
451
      )
452
      // Step 3: Has write permissions, generate presigned POST URL.
453
      .andThen(() =>
454
        AdminFormService.createPresignedPostUrlForImages({
6✔
455
          fileId: randomizedFileId,
456
          fileMd5Hash,
457
          fileType,
458
        }),
459
      )
460
      .map((presignedPostUrl) => res.json(presignedPostUrl))
3✔
461
      .mapErr((error) => {
462
        logger.error({
10✔
463
          message: 'Presigning post data encountered an error',
464
          meta: {
465
            action: 'createPresignedPostUrlForImages',
466
            ...createReqMeta(req),
467
          },
468
          error,
469
        })
470

471
        const { statusCode, errorMessage } = mapRouteError(error)
10✔
472
        return res.status(statusCode).json({ message: errorMessage })
10✔
473
      })
474
  )
475
}
476

477
export const handleCreatePresignedPostUrlForImages = [
8✔
478
  fileUploadValidator,
479
  createPresignedPostUrlForImages,
480
] as ControllerHandler[]
481

482
/**
483
 * Handler for POST /:formId([a-fA-F0-9]{24})/adminform/logos.
484
 * @security session
485
 *
486
 * @returns 200 with presigned POST URL object
487
 * @returns 400 when error occurs whilst creating presigned POST URL object
488
 * @returns 403 when user does not have write permissions for form
489
 * @returns 404 when form cannot be found
490
 * @returns 410 when form is archived
491
 * @returns 422 when user in session cannot be retrieved from the database
492
 */
493
export const createPresignedPostUrlForLogos: ControllerHandler<
8✔
494
  { formId: string },
495
  unknown,
496
  {
497
    fileId: string
498
    fileMd5Hash: string
499
    fileType: string
500
  }
501
> = async (req, res) => {
13✔
502
  const { formId } = req.params
13✔
503
  const { fileId, fileMd5Hash, fileType } = req.body
13✔
504
  const sessionUserId = (req.session as AuthedSessionData).user._id
13✔
505

506
  // Adding random objectId ensures fileId is unpredictable by client
507
  const randomizedFileId = `${String(new ObjectId())}-${fileId}`
13✔
508

509
  return (
13✔
510
    // Step 1: Retrieve currently logged in user.
511
    UserService.getPopulatedUserById(sessionUserId)
512
      .andThen((user) =>
513
        // Step 2: Check whether user has write permissions to form
514
        AuthService.getFormAfterPermissionChecks({
11✔
515
          user,
516
          formId,
517
          level: PermissionLevel.Write,
518
        }),
519
      )
520
      // Step 3: Has write permissions, generate presigned POST URL.
521
      .andThen(() =>
522
        AdminFormService.createPresignedPostUrlForLogos({
6✔
523
          fileId: randomizedFileId,
524
          fileMd5Hash,
525
          fileType,
526
        }),
527
      )
528
      .map((presignedPostUrl) => res.json(presignedPostUrl))
3✔
529
      .mapErr((error) => {
530
        logger.error({
10✔
531
          message: 'Presigning post data encountered an error',
532
          meta: {
533
            action: 'createPresignedPostUrlForLogos',
534
            ...createReqMeta(req),
535
          },
536
          error,
537
        })
538

539
        const { statusCode, errorMessage } = mapRouteError(error)
10✔
540
        return res.status(statusCode).json({ message: errorMessage })
10✔
541
      })
542
  )
543
}
544

545
export const handleCreatePresignedPostUrlForLogos = [
8✔
546
  fileUploadValidator,
547
  createPresignedPostUrlForLogos,
548
] as ControllerHandler[]
549

550
// Validates that the ending date >= starting date
551
const validateDateRange = celebrate({
8✔
552
  [Segments.QUERY]: Joi.object()
553
    .keys({
554
      startDate: Joi.date().format('YYYY-MM-DD').raw(),
555
      endDate: Joi.date().format('YYYY-MM-DD').min(Joi.ref('startDate')).raw(),
556
    })
557
    .and('startDate', 'endDate'),
558
})
559

560
/**
561
 * NOTE: This is exported solely for testing
562
 * @security session
563
 *
564
 * @returns 200 with submission counts of given form
565
 * @returns 400 when query.startDate or query.endDate is malformed
566
 * @returns 403 when user does not have permissions to access form
567
 * @returns 404 when form cannot be found
568
 * @returns 410 when form is archived
569
 * @returns 422 when user in session cannot be retrieved from the database
570
 * @returns 500 when database error occurs
571
 */
572
export const countFormSubmissions: ControllerHandler<
8✔
573
  { formId: string },
574
  ErrorDto | number,
575
  unknown,
576
  SubmissionCountQueryDto
577
> = async (req, res) => {
20✔
578
  const { formId } = req.params
20✔
579
  const dateRange = req.query
20✔
580
  const sessionUserId = (req.session as AuthedSessionData).user._id
20✔
581

582
  const logMeta = {
20✔
583
    action: 'handleCountFormSubmissions',
584
    ...createReqMeta(req),
585
    userId: sessionUserId,
586
    formId,
587
  }
588

589
  // Step 1: Retrieve currently logged in user.
590
  const formResult = await UserService.getPopulatedUserById(
20✔
591
    sessionUserId,
592
  ).andThen((user) =>
593
    // Step 2: Check whether user has read permissions to form
594
    AuthService.getFormAfterPermissionChecks({
17✔
595
      user,
596
      formId,
597
      level: PermissionLevel.Read,
598
    }),
599
  )
600

601
  if (formResult.isErr()) {
20✔
602
    logger.warn({
10✔
603
      message: 'Error occurred when checking user permissions for form',
604
      meta: logMeta,
605
      error: formResult.error,
606
    })
607
    const { errorMessage, statusCode } = mapRouteError(formResult.error)
10✔
608
    return res.status(statusCode).json({ message: errorMessage })
10✔
609
  }
610

611
  // Step 3: Has permissions, continue to retrieve submission counts.
612
  return SubmissionService.getFormSubmissionsCount(formId, dateRange)
10✔
613
    .map((count) => res.json(count))
8✔
614
    .mapErr((error) => {
615
      logger.error({
2✔
616
        message: 'Error retrieving form submission count',
617
        meta: {
618
          action: 'handleCountFormSubmissions',
619
          ...createReqMeta(req),
620
          userId: sessionUserId,
621
          formId,
622
        },
623
        error,
624
      })
625
      const { errorMessage, statusCode } = mapRouteError(error)
2✔
626
      return res.status(statusCode).json({ message: errorMessage })
2✔
627
    })
628
}
629

630
// Handler for GET /admin/forms/:formId/submissions/count
631
export const handleCountFormSubmissions = [
8✔
632
  validateDateRange,
633
  countFormSubmissions,
634
] as ControllerHandler[]
635

636
/**
637
 * Handler for GET /{formId}/adminform/feedback/count.
638
 * @security session
639
 *
640
 * @returns 200 with feedback counts of given form
641
 * @returns 403 when user does not have permissions to access form
642
 * @returns 404 when form cannot be found
643
 * @returns 410 when form is archived
644
 * @returns 422 when user in session cannot be retrieved from the database
645
 * @returns 500 when database error occurs
646
 */
647
export const handleCountFormFeedback: ControllerHandler<
8✔
648
  { formId: string },
649
  number | ErrorDto
650
> = async (req, res) => {
15✔
651
  const { formId } = req.params
15✔
652
  const sessionUserId = (req.session as AuthedSessionData).user._id
15✔
653

654
  return (
15✔
655
    // Step 1: Retrieve currently logged in user.
656
    UserService.getPopulatedUserById(sessionUserId)
657
      .andThen((user) =>
658
        // Step 2: Check whether user has read permissions to form
659
        AuthService.getFormAfterPermissionChecks({
12✔
660
          user,
661
          formId,
662
          level: PermissionLevel.Read,
663
        }),
664
      )
665
      // Step 3: Retrieve form feedback counts.
666
      .andThen(() => FeedbackService.getFormFeedbackCount(formId))
5✔
667
      .map((feedbackCount) => res.json(feedbackCount))
3✔
668
      // Some error occurred earlier in the chain.
669
      .mapErr((error) => {
670
        logger.error({
12✔
671
          message: 'Error retrieving form feedback count',
672
          meta: {
673
            action: 'handleCountFormFeedback',
674
            ...createReqMeta(req),
675
            userId: sessionUserId,
676
            formId,
677
          },
678
          error,
679
        })
680
        const { errorMessage, statusCode } = mapRouteError(error)
12✔
681
        return res.status(statusCode).json({ message: errorMessage })
12✔
682
      })
683
  )
684
}
685

686
/**
687
 * Handler for GET /{formId}/adminform/feedback/download.
688
 * @security session
689
 *
690
 * @returns 200 with feedback stream
691
 * @returns 403 when user does not have permissions to access form
692
 * @returns 404 when form cannot be found
693
 * @returns 410 when form is archived
694
 * @returns 422 when user in session cannot be retrieved from the database
695
 * @returns 500 when database or stream error occurs
696
 */
697
export const handleStreamFormFeedback: ControllerHandler<{
8✔
698
  formId: string
699
}> = async (req, res) => {
12✔
700
  const { formId } = req.params
12✔
701
  const sessionUserId = (req.session as AuthedSessionData).user._id
12✔
702

703
  // Step 1: Retrieve currently logged in user.
704
  const hasReadPermissionResult = await UserService.getPopulatedUserById(
12✔
705
    sessionUserId,
706
  ).andThen((user) =>
707
    // Step 2: Check whether user has read permissions to form
708
    AuthService.getFormAfterPermissionChecks({
10✔
709
      user,
710
      formId,
711
      level: PermissionLevel.Read,
712
    }),
713
  )
714

715
  const logMeta = {
12✔
716
    action: 'handleStreamFormFeedback',
717
    ...createReqMeta(req),
718
    userId: sessionUserId,
719
    formId,
720
  }
721

722
  if (hasReadPermissionResult.isErr()) {
12✔
723
    logger.error({
9✔
724
      message: 'Error occurred whilst verifying user permissions',
725
      meta: logMeta,
726
      error: hasReadPermissionResult.error,
727
    })
728
    const { errorMessage, statusCode } = mapRouteError(
9✔
729
      hasReadPermissionResult.error,
730
    )
731
    return res.status(statusCode).json({ message: errorMessage })
9✔
732
  }
733

734
  // No errors, start stream.
735
  const cursor = FeedbackService.getFormFeedbackStream(formId)
3✔
736

737
  cursor
3✔
738
    .on('error', (error) => {
739
      logger.error({
×
740
        message: 'Error streaming feedback from MongoDB',
741
        meta: logMeta,
742
        error,
743
      })
744
      return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
×
745
        message: 'Error retrieving from database.',
746
      })
747
    })
748
    .pipe(JSONStream.stringify())
749
    .on('error', (error) => {
750
      logger.error({
×
751
        message: 'Error converting feedback to JSON',
752
        meta: logMeta,
753
        error,
754
      })
755
      return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
×
756
        message: 'Error converting feedback to JSON',
757
      })
758
    })
759
    .pipe(res.type('json'))
760
    .on('error', (error) => {
761
      logger.error({
×
762
        message: 'Error writing feedback to HTTP stream',
763
        meta: logMeta,
764
        error,
765
      })
766
      return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
×
767
        message: 'Error writing feedback to HTTP stream',
768
      })
769
    })
770
    .on('close', () => {
771
      logger.info({
2✔
772
        message: 'Stream feedback closed',
773
        meta: logMeta,
774
      })
775

776
      return res.end()
2✔
777
    })
778
}
779

780
/**
781
 * Handler for GET /{formId}/adminform/feedback.
782
 * @security session
783
 *
784
 * @returns 200 with feedback response
785
 * @returns 403 when user does not have permissions to access form
786
 * @returns 404 when form cannot be found
787
 * @returns 410 when form is archived
788
 * @returns 422 when user in session cannot be retrieved from the database
789
 * @returns 500 when database error occurs
790
 */
791
export const handleGetFormFeedback: ControllerHandler<
8✔
792
  { formId: string },
793
  FormFeedbackMetaDto | ErrorDto
794
> = (req, res) => {
8✔
795
  const { formId } = req.params
16✔
796
  const sessionUserId = (req.session as AuthedSessionData).user._id
16✔
797

798
  return UserService.getPopulatedUserById(sessionUserId)
16✔
799
    .andThen((user) =>
800
      AuthService.getFormAfterPermissionChecks({
12✔
801
        user,
802
        formId,
803
        level: PermissionLevel.Read,
804
      }),
805
    )
806
    .andThen(() => FeedbackService.getFormFeedbacks(formId))
5✔
807
    .map((fbResponse) => res.json(fbResponse))
3✔
808
    .mapErr((error) => {
809
      logger.error({
13✔
810
        message: 'Error retrieving form feedbacks',
811
        meta: {
812
          action: 'handleGetFormFeedback',
813
          ...createReqMeta(req),
814
          userId: sessionUserId,
815
          formId,
816
        },
817
        error,
818
      })
819
      const { errorMessage, statusCode } = mapRouteError(error)
13✔
820
      return res.status(statusCode).json({ message: errorMessage })
13✔
821
    })
822
}
823

824
/**
825
 * Handler for DELETE /{formId}/adminform.
826
 * @security session
827
 *
828
 * @returns 200 with success message when successfully archived
829
 * @returns 403 when user does not have permissions to archive form
830
 * @returns 404 when form cannot be found
831
 * @returns 410 when form is already archived
832
 * @returns 422 when user in session cannot be retrieved from the database
833
 * @returns 500 when database error occurs
834
 */
835
export const handleArchiveForm: ControllerHandler<{ formId: string }> = async (
8✔
836
  req,
837
  res,
838
) => {
14✔
839
  const { formId } = req.params
14✔
840
  const sessionUserId = (req.session as AuthedSessionData).user._id
14✔
841

842
  return (
14✔
843
    // Step 1: Retrieve currently logged in user.
844
    UserService.getPopulatedUserById(sessionUserId)
845
      .andThen((user) =>
846
        // Step 2: Check whether user has delete permissions for form.
847
        AuthService.getFormAfterPermissionChecks({
11✔
848
          user,
849
          formId,
850
          level: PermissionLevel.Delete,
851
        }),
852
      )
853
      // Step 3: Currently logged in user has permissions to archive form.
854
      .andThen((formToArchive) => AdminFormService.archiveForm(formToArchive))
4✔
855
      .andThen((archivedForm) => {
856
        // Step 4: For each collaborator, remove the form from their workspaces
857
        archivedForm.permissionList?.forEach(async (permissions) => {
2✔
858
          await UserService.findUserByEmail(permissions.email).map(
×
859
            async (user) =>
×
860
              await removeFormsFromAllWorkspaces({
×
861
                formIds: [formId],
862
                userId: user._id,
863
              }),
864
          )
865
        })
866
        // Step 5: remove form from workspace of current user
867
        return removeFormsFromAllWorkspaces({
2✔
868
          formIds: [formId],
869
          userId: sessionUserId,
870
        })
871
      })
872
      .map(() => res.json({ message: 'Form has been archived' }))
2✔
873
      .mapErr((error) => {
874
        logger.warn({
12✔
875
          message: 'Error occurred when archiving form',
876
          meta: {
877
            action: 'handleArchiveForm',
878
            ...createReqMeta(req),
879
            userId: sessionUserId,
880
            formId,
881
          },
882
          error,
883
        })
884
        const { errorMessage, statusCode } = mapRouteError(error)
12✔
885
        return res.status(statusCode).json({ message: errorMessage })
12✔
886
      })
887
  )
888
}
889

890
/**
891
 * Handler for POST /:formId/adminform
892
 * Duplicates the form corresponding to the formId. The currently logged in user
893
 * must have read permissions to the form being copied.
894
 * @note Even if current user is not admin of the form, the current user will be the admin of the new form
895
 * @security session
896
 *
897
 * @returns 200 with the duplicate form dashboard view
898
 * @returns 403 when user does not have permissions to access form
899
 * @returns 404 when form cannot be found
900
 * @returns 410 when form is archived
901
 * @returns 422 when user in session cannot be retrieved from the database
902
 * @returns 500 when database error occurs
903
 */
904
export const duplicateAdminForm: ControllerHandler<
8✔
905
  { formId: string },
906
  unknown,
907
  DuplicateFormBodyDto
908
> = (req, res) => {
8✔
909
  const { formId } = req.params
16✔
910
  const userId = (req.session as AuthedSessionData).user._id
16✔
911
  const { workspaceId, ...overrideParams } = req.body
16✔
912

913
  return (
16✔
914
    // Step 1: Retrieve currently logged in user.
915
    UserService.getPopulatedUserById(userId)
916
      .andThen((user) =>
917
        // Step 2: Check if current user has permissions to read form.
918
        AuthService.getFormAfterPermissionChecks({
13✔
919
          user,
920
          formId,
921
          level: PermissionLevel.Read,
922
        })
923
          .andThen((originalForm) =>
924
            // Step 3: Duplicate form.
925
            AdminFormService.duplicateForm(
7✔
926
              originalForm,
927
              userId,
928
              overrideParams,
929
              workspaceId,
930
            ),
931
          )
932
          // Step 4: Retrieve dashboard view of duplicated form.
933
          .map((duplicatedForm) => duplicatedForm.getDashboardView(user)),
5✔
934
      )
935
      // Success; return duplicated form's dashboard view.
936
      .map((dupedDashView) => res.json(dupedDashView))
5✔
937
      // Error; some error occurred in the chain.
938
      .mapErr((error) => {
939
        logger.error({
11✔
940
          message: 'Error duplicating form',
941
          meta: {
942
            action: 'duplicateAdminForm',
943
            ...createReqMeta(req),
944
            userId,
945
            formId,
946
          },
947
          error,
948
        })
949
        const { errorMessage, statusCode } = mapRouteError(error)
11✔
950
        return res.status(statusCode).json({ message: errorMessage })
11✔
951
      })
952
  )
953
}
954

955
export const handleDuplicateAdminForm = [
8✔
956
  duplicateFormValidator,
957
  duplicateAdminForm,
958
] as ControllerHandler[]
959

960
/**
961
 * Handler for GET /:formId/adminform/template
962
 * Handler for GET /api/v3/admin/forms/:formId/use-template
963
 * @security session
964
 *
965
 * @returns 200 with target form's template view
966
 * @returns 403 when the target form is private
967
 * @returns 404 when form cannot be found
968
 * @returns 410 when form is archived
969
 * @returns 500 when database error occurs
970
 */
971
export const handleGetTemplateForm: ControllerHandler<
8✔
972
  { formId: string },
973
  PreviewFormViewDto | ErrorDto | PrivateFormErrorDto
974
> = (req, res) => {
8✔
975
  const { formId } = req.params
5✔
976
  const userId = (req.session as AuthedSessionData).user._id
5✔
977

978
  return (
5✔
979
    // Step 1: Retrieve form only if form is currently public.
980
    AuthService.getFormIfPublic(formId)
981
      // Step 2: Remove private form details before being returned.
982
      .map((populatedForm) => populatedForm.getPublicView())
1✔
983
      .map((scrubbedForm) =>
984
        res
1✔
985
          .status(StatusCodes.OK)
986
          .json({ form: scrubbedForm as PublicFormDto }),
987
      )
988
      .mapErr((error) => {
989
        logger.error({
4✔
990
          message: 'Error retrieving form template',
991
          meta: {
992
            action: 'handleGetTemplateForm',
993
            ...createReqMeta(req),
994
            userId,
995
            formId,
996
          },
997
          error,
998
        })
999
        const { errorMessage, statusCode } = mapRouteError(error)
4✔
1000

1001
        // Specialized error response for PrivateFormError.
1002
        if (error instanceof PrivateFormError) {
4✔
1003
          return res.status(statusCode).json({
1✔
1004
            message: error.message,
1005
            // Flag to prevent default 404 subtext ("please check link") from
1006
            // showing.
1007
            isPageFound: true,
1008
            formTitle: error.formTitle,
1009
          })
1010
        }
1011
        return res.status(statusCode).json({ message: errorMessage })
3✔
1012
      })
1013
  )
1014
}
1015

1016
/**
1017
 * Handler for POST /:formId/adminform/use-template
1018
 * Duplicates the form corresponding to the formId. The form must be public to
1019
 * be copied.
1020
 * @note The current user will be the admin of the new duplicated form
1021
 * @security session
1022
 *
1023
 * @returns 200 with the new form dashboard view
1024
 * @returns 403 when form is private
1025
 * @returns 404 when form cannot be found
1026
 * @returns 410 when form is archived
1027
 * @returns 422 when user in session cannot be retrieved from the database
1028
 * @returns 500 when database error occurs
1029
 */
1030
export const handleCopyTemplateForm: ControllerHandler<
8✔
1031
  { formId: string },
1032
  AdminDashboardFormMetaDto | ErrorDto,
1033
  DuplicateFormBodyDto
1034
> = (req, res) => {
8✔
1035
  const { formId } = req.params
8✔
1036
  const userId = (req.session as AuthedSessionData).user._id
8✔
1037
  const overrideParams = req.body
8✔
1038

1039
  return (
8✔
1040
    // Step 1: Retrieve currently logged in user.
1041
    UserService.getPopulatedUserById(userId)
1042
      .andThen((user) =>
1043
        // Step 2: Check if form is currently public.
1044
        AuthService.getFormIfPublic(formId).andThen((originalForm) =>
6✔
1045
          // Step 3: Duplicate form.
1046
          AdminFormService.duplicateForm(originalForm, userId, overrideParams)
3✔
1047
            // Step 4: Retrieve dashboard view of duplicated form.
1048
            .map((duplicatedForm) => duplicatedForm.getDashboardView(user)),
1✔
1049
        ),
1050
      )
1051
      // Success; return new form's dashboard view.
1052
      .map((dupedDashView) => res.json(dupedDashView))
1✔
1053
      // Error; some error occurred in the chain.
1054
      .mapErr((error) => {
1055
        logger.error({
7✔
1056
          message: 'Error copying template form',
1057
          meta: {
1058
            action: 'handleCopyTemplateForm',
1059
            ...createReqMeta(req),
1060
            userId: userId,
1061
            formId,
1062
          },
1063
          error,
1064
        })
1065
        const { errorMessage, statusCode } = mapRouteError(error)
7✔
1066

1067
        // Specialized error response for PrivateFormError.
1068
        if (error instanceof PrivateFormError) {
7✔
1069
          return res.status(statusCode).json({
1✔
1070
            message: 'Form must be public to be copied',
1071
          })
1072
        }
1073
        return res.status(statusCode).json({ message: errorMessage })
6✔
1074
      })
1075
  )
1076
}
1077

1078
/**
1079
 * Handler for POST /admin/forms/all-transfer-owner.
1080
 * @security session
1081
 *
1082
 * @returns 200 with true if transfer was successful
1083
 * @returns 400 when new owner is not in the database yet
1084
 * @returns 400 when new owner is already current owner
1085
 * @returns 422 when user in session cannot be retrieved from the database
1086
 * @returns 500 when database error occurs
1087
 */
1088
export const transferAllFormsOwnership: ControllerHandler<
8✔
1089
  unknown,
1090
  unknown,
1091
  { email: string }
1092
> = (req, res) => {
8✔
1093
  const { email: newOwnerEmail } = req.body
11✔
1094
  const sessionUserId = (req.session as AuthedSessionData).user._id
11✔
1095

1096
  return (
11✔
1097
    // Step 1: Retrieve currently logged in user.
1098
    UserService.getPopulatedUserById(sessionUserId)
1099
      .andThen((user) =>
1100
        // Step 2: Transfer all forms to new owner
1101
        AdminFormService.transferAllFormsOwnership(user, newOwnerEmail),
8✔
1102
      )
1103
      .map((data) => {
1104
        return res.status(StatusCodes.OK).json(data)
2✔
1105
      })
1106
      // Some error occurred earlier in the chain.
1107
      .mapErr((error) => {
1108
        logger.error({
9✔
1109
          message: 'Error occurred whilst transferring all forms ownership',
1110
          meta: {
1111
            action: 'transferAllFormsOwnership',
1112
            ...createReqMeta(req),
1113
            userId: sessionUserId,
1114
            newOwnerEmail,
1115
          },
1116
          error,
1117
        })
1118
        const { errorMessage, statusCode } = mapRouteError(error)
9✔
1119
        return res.status(statusCode).json({ message: errorMessage })
9✔
1120
      })
1121
  )
1122
}
1123

1124
export const handleTransferAllFormsOwnership = [
8✔
1125
  transferFormOwnershipValidator,
1126
  transferAllFormsOwnership,
1127
] as ControllerHandler[]
1128

1129
/**
1130
 * Handler for POST /{formId}/adminform/transfer-owner.
1131
 * @security session
1132
 *
1133
 * @returns 200 with updated form with transferred owners
1134
 * @returns 400 when new owner is not in the database yet
1135
 * @returns 400 when new owner is already current owner
1136
 * @returns 403 when user is not the current owner of the form
1137
 * @returns 404 when form cannot be found
1138
 * @returns 410 when form is archived
1139
 * @returns 422 when user in session cannot be retrieved from the database
1140
 * @returns 500 when database error occurs
1141
 */
1142
export const transferFormOwnership: ControllerHandler<
8✔
1143
  { formId: string },
1144
  unknown,
1145
  { email: string }
1146
> = (req, res) => {
8✔
1147
  const { formId } = req.params
16✔
1148
  const { email: newOwnerEmail } = req.body
16✔
1149
  const sessionUserId = (req.session as AuthedSessionData).user._id
16✔
1150

1151
  return (
16✔
1152
    // Step 1: Retrieve currently logged in user.
1153
    UserService.getPopulatedUserById(sessionUserId)
1154
      .andThen((user) =>
1155
        // Step 2: Retrieve form with delete permission check.
1156
        AuthService.getFormAfterPermissionChecks({
13✔
1157
          user,
1158
          formId,
1159
          level: PermissionLevel.Delete,
1160
        }),
1161
      )
1162
      // Step 3: User has permissions, transfer form ownership.
1163
      .andThen((retrievedForm) =>
1164
        AdminFormService.transferFormOwnership(retrievedForm, newOwnerEmail),
6✔
1165
      )
1166
      // Success, return updated form.
1167
      .map((updatedPopulatedForm) => res.json({ form: updatedPopulatedForm }))
2✔
1168
      // Some error occurred earlier in the chain.
1169
      .mapErr((error) => {
1170
        logger.error({
14✔
1171
          message: 'Error occurred whilst transferring form ownership',
1172
          meta: {
1173
            action: 'transferFormOwnership',
1174
            ...createReqMeta(req),
1175
            userId: sessionUserId,
1176
            formId,
1177
            newOwnerEmail,
1178
          },
1179
          error,
1180
        })
1181
        const { errorMessage, statusCode } = mapRouteError(error)
14✔
1182
        return res.status(statusCode).json({ message: errorMessage })
14✔
1183
      })
1184
  )
1185
}
1186

1187
export const handleTransferFormOwnership = [
8✔
1188
  transferFormOwnershipValidator,
1189
  transferFormOwnership,
1190
] as ControllerHandler[]
1191

1192
/**
1193
 * Handler for POST /adminform.
1194
 * @security session
1195
 *
1196
 * @returns 200 with newly created form
1197
 * @returns 409 when a database conflict error occurs
1198
 * @returns 413 when payload for created form exceeds size limit
1199
 * @returns 422 when user of given id cannnot be found in the database, or when form parameters are invalid
1200
 * @returns 500 when database error occurs
1201
 */
1202
export const createForm: ControllerHandler<
8✔
1203
  unknown,
1204
  DeserializeTransform<FormDto> | ErrorDto,
1205
  { form: CreateFormBodyDto }
1206
> = async (req, res) => {
15✔
1207
  const {
1208
    form: { workspaceId, ...formParams },
15✔
1209
  } = req.body
15✔
1210
  const sessionUserId = (req.session as AuthedSessionData).user._id
15✔
1211

1212
  return (
15✔
1213
    // Step 1: Retrieve currently logged in user.
1214
    UserService.findUserById(sessionUserId)
1215
      // Step 2: Create form with given params and set admin to logged in user.
1216
      .andThen((user) =>
1217
        AdminFormService.createForm(
12✔
1218
          {
1219
            ...formParams,
1220
            admin: user._id,
1221
          },
1222
          workspaceId,
1223
        ),
1224
      )
1225
      .map((createdForm) => {
1226
        return res
5✔
1227
          .status(StatusCodes.OK)
1228
          .json(createdForm as DeserializeTransform<FormDto>)
1229
      })
1230
      .mapErr((error) => {
1231
        logger.error({
10✔
1232
          message: 'Error occurred when creating form',
1233
          meta: {
1234
            action: 'createForm',
1235
            ...createReqMeta(req),
1236
            userId: sessionUserId,
1237
          },
1238
          error,
1239
        })
1240
        const { errorMessage, statusCode } = mapRouteError(error)
10✔
1241
        return res.status(statusCode).json({ message: errorMessage })
10✔
1242
      })
1243
  )
1244
}
1245

1246
export const handleCreateForm = [
8✔
1247
  createFormValidator,
1248
  createForm,
1249
] as ControllerHandler[]
1250

1251
/**
1252
 * Handler for PUT /:formId/adminform.
1253
 * @security session
1254
 *
1255
 * @returns 200 with updated form
1256
 * @returns 400 when form field has invalid updates to be performed
1257
 * @returns 403 when current user does not have permissions to update form
1258
 * @returns 404 when form to update cannot be found
1259
 * @returns 409 when saving updated form incurs a conflict in the database
1260
 * @returns 410 when form to update is archived
1261
 * @returns 413 when updated form is too large to be saved in the database
1262
 * @returns 422 when an invalid update is attempted on the form
1263
 * @returns 422 when user in session cannot be retrieved from the database
1264
 * @returns 500 when database error occurs
1265
 */
1266
export const handleUpdateForm: ControllerHandler<
8✔
1267
  { formId: string },
1268
  unknown,
1269
  { form: FormUpdateParams }
1270
> = (req, res) => {
8✔
1271
  const { formId } = req.params
11✔
1272
  const { form: formUpdateParams } = req.body
11✔
1273
  const sessionUserId = (req.session as AuthedSessionData).user._id
11✔
1274

1275
  // Step 1: Retrieve currently logged in user.
1276
  return UserService.getPopulatedUserById(sessionUserId)
11✔
1277
    .andThen((user) =>
1278
      // Step 2: Retrieve form with write permission check.
1279
      AuthService.getFormAfterPermissionChecks({
10✔
1280
        user,
1281
        formId,
1282
        level: PermissionLevel.Write,
1283
      }),
1284
    )
1285
    .andThen((retrievedForm) => {
1286
      // Step 3: Update form or form fields depending on form update parameters
1287
      // passed in.
1288
      const { editFormField } = formUpdateParams
7✔
1289

1290
      // Use different service function depending on type of form update.
1291
      const updateFormResult: ResultAsync<
1292
        IPopulatedForm,
1293
        | EditFieldError
1294
        | DatabaseError
1295
        | DatabaseValidationError
1296
        | DatabaseConflictError
1297
        | DatabasePayloadSizeError
1298
      > = editFormField
7✔
1299
        ? AdminFormService.editFormFields(retrievedForm, editFormField)
1300
        : AdminFormService.updateForm(retrievedForm, formUpdateParams)
1301

1302
      return updateFormResult
7✔
1303
    })
1304
    .map((updatedForm) => res.status(StatusCodes.OK).json(updatedForm))
2✔
1305
    .mapErr((error) => {
1306
      logger.error({
9✔
1307
        message: 'Error occurred when updating form',
1308
        meta: {
1309
          action: 'handleUpdateForm',
1310
          ...createReqMeta(req),
1311
          userId: sessionUserId,
1312
          formId,
1313
          formUpdateParams,
1314
        },
1315
        error,
1316
      })
1317
      const { errorMessage, statusCode } = mapRouteError(error)
9✔
1318
      return res.status(statusCode).json({ message: errorMessage })
9✔
1319
    })
1320
}
1321

1322
/**
1323
 * Handler for POST /:formId/fields/:fieldId/duplicate
1324
 * @security session
1325
 *
1326
 * @returns 200 with duplicated field
1327
 * @returns 400 when form field has invalid updates to be performed
1328
 * @returns 403 when current user does not have permissions to update form
1329
 * @returns 404 when form or field to duplicate cannot be found
1330
 * @returns 409 when saving updated form field causes sms limit to be exceeded
1331
 * @returns 409 when saving updated form incurs a conflict in the database
1332
 * @returns 410 when form to update is archived
1333
 * @returns 413 when updated form is too large to be saved in the database
1334
 * @returns 422 when user in session cannot be retrieved from the database
1335
 * @returns 500 when database error occurs
1336
 */
1337
export const handleDuplicateFormField: ControllerHandler<
8✔
1338
  { formId: string; fieldId: string },
1339
  FormFieldDto | ErrorDto
1340
> = (req, res) => {
8✔
1341
  const { formId, fieldId } = req.params
14✔
1342
  const sessionUserId = (req.session as AuthedSessionData).user._id
14✔
1343

1344
  // Step 1: Retrieve currently logged in user.
1345
  return UserService.getPopulatedUserById(sessionUserId)
14✔
1346
    .andThen((user) =>
1347
      // Step 2: Retrieve form with write permission check.
1348
      AuthService.getFormAfterPermissionChecks({
12✔
1349
        user,
1350
        formId,
1351
        level: PermissionLevel.Write,
1352
      }),
1353
    )
1354
    .andThen((form) => AdminFormService.duplicateFormField(form, fieldId))
6✔
1355
    .map((duplicatedField) =>
1356
      res.status(StatusCodes.OK).json(duplicatedField as FormFieldDto),
2✔
1357
    )
1358
    .mapErr((error) => {
1359
      logger.error({
12✔
1360
        message: 'Error occurred when duplicating field',
1361
        meta: {
1362
          action: 'handleDuplicateFormField',
1363
          ...createReqMeta(req),
1364
          userId: sessionUserId,
1365
          formId,
1366
          fieldId,
1367
        },
1368
        error,
1369
      })
1370
      const { errorMessage, statusCode } = mapRouteError(error)
12✔
1371
      return res.status(statusCode).json({ message: errorMessage })
12✔
1372
    })
1373
}
1374

1375
export const _handleUpdateSettings: ControllerHandler<
8✔
1376
  { formId: string },
1377
  FormSettings | ErrorDto,
1378
  SettingsUpdateDto
1379
> = (req, res) => {
8✔
1380
  const { formId } = req.params
17✔
1381
  const sessionUserId = (req.session as AuthedSessionData).user._id
17✔
1382
  const settingsToPatch = req.body
17✔
1383

1384
  // Step 1: Retrieve currently logged in user.
1385
  return UserService.getPopulatedUserById(sessionUserId)
17✔
1386
    .andThen((user) =>
1387
      // Step 2: Retrieve form with write permission check.
1388
      AuthService.getFormAfterPermissionChecks({
15✔
1389
        user,
1390
        formId,
1391
        level: PermissionLevel.Write,
1392
      }),
1393
    )
1394
    .andThen((retrievedForm) =>
1395
      AdminFormService.updateFormSettings(retrievedForm, settingsToPatch),
9✔
1396
    )
1397
    .map((updatedSettings) => res.status(StatusCodes.OK).json(updatedSettings))
4✔
1398
    .mapErr((error) => {
1399
      logger.error({
13✔
1400
        message: 'Error occurred when updating form settings',
1401
        meta: {
1402
          action: 'handleUpdateSettings',
1403
          ...createReqMeta(req),
1404
          userId: sessionUserId,
1405
          formId,
1406
          settingsKeysToUpdate: Object.keys(settingsToPatch),
1407
        },
1408
        error,
1409
      })
1410
      const { errorMessage, statusCode } = mapRouteError(error)
13✔
1411
      return res.status(statusCode).json({ message: errorMessage })
13✔
1412
    })
1413
}
1414

1415
/**
1416
 * Handler for PATCH /forms/:formId/settings.
1417
 * @security session
1418
 *
1419
 * @returns 200 with updated form settings
1420
 * @returns 400 when body is malformed
1421
 * @returns 403 when current user does not have permissions to update form settings
1422
 * @returns 404 when form to update settings for cannot be found
1423
 * @returns 409 when saving form settings incurs a conflict in the database
1424
 * @returns 410 when updating settings for archived form
1425
 * @returns 413 when updating settings causes form to be too large to be saved in the database
1426
 * @returns 422 when an invalid settings update is attempted on the form
1427
 * @returns 422 when user in session cannot be retrieved from the database
1428
 * @returns 500 when database error occurs
1429
 */
1430
export const handleUpdateSettings = [
8✔
1431
  updateSettingsValidator,
1432
  _handleUpdateSettings,
1433
] as ControllerHandler[]
1434

1435
export const _handleUpdateWebhookSettings: ControllerHandler<
8✔
1436
  { formId: string },
1437
  FormWebhookSettings | ErrorDto,
1438
  WebhookSettingsUpdateDto
1439
> = (req, res) => {
8✔
1440
  const { formId } = req.params
×
1441
  const { userEmail, webhook: webhookSettings } = req.body
×
1442
  const authedUserId = (req.session as AuthedSessionData).user._id
×
1443

1444
  logger.info({
×
1445
    message: 'User attempting to update webhook settings',
1446
    meta: {
1447
      action: '_handleUpdateWebhookSettings',
1448
      ...createReqMeta(req),
1449
      reqBody: req.body,
1450
      formId,
1451
      userEmail,
1452
      webhookSettings,
1453
    },
1454
  })
1455

1456
  // Step 1: Retrieve currently logged in user.
1457
  return UserService.findUserById(authedUserId)
×
1458
    .andThen((user) =>
1459
      // Step 2: Retrieve form with write permission check.
1460
      AuthService.getFormAfterPermissionChecks({
×
1461
        user,
1462
        formId,
1463
        level: PermissionLevel.Write,
1464
      }),
1465
    )
1466
    .andThen((retrievedForm) =>
1467
      AdminFormService.updateFormSettings(retrievedForm, {
×
1468
        webhook: webhookSettings,
1469
      }),
1470
    )
1471
    .map((updatedSettings) => {
1472
      const webhookSettings = { webhook: updatedSettings.webhook }
×
1473
      res.status(StatusCodes.OK).json(webhookSettings)
×
1474
    })
1475
    .mapErr((error) => {
1476
      logger.error({
×
1477
        message: 'Error occurred when updating form settings',
1478
        meta: {
1479
          action: 'handleUpdateWebhookSettings',
1480
          ...createReqMeta(req),
1481
          userEmail,
1482
          formId,
1483
          settingsKeysToUpdate: Object.keys(webhookSettings),
1484
        },
1485
        error,
1486
      })
1487
      const { errorMessage, statusCode } = mapRouteError(error)
×
1488
      return res.status(statusCode).json({ message: errorMessage })
×
1489
    })
1490
}
1491

1492
/**
1493
 * Handler for PATCH api/public/v1/admin/forms/:formId/webhooksettings.
1494
 * @security session
1495
 *
1496
 * @returns 200 with updated form settings
1497
 * @returns 400 when body is malformed
1498
 * @returns 403 when user email does not have permissions to update form settings
1499
 * @returns 404 when form to update settings for cannot be found
1500
 * @returns 409 when saving form settings incurs a conflict in the database
1501
 * @returns 410 when updating settings for archived form
1502
 * @returns 413 when updating settings causes form to be too large to be saved in the database
1503
 * @returns 422 when an invalid settings update is attempted on the form
1504
 * @returns 422 when user from user email cannot be retrieved from the database
1505
 * @returns 500 when database error occurs
1506
 */
1507
export const handleUpdateWebhookSettings = [
8✔
1508
  updateWebhookSettingsValidator,
1509
  _handleUpdateWebhookSettings,
1510
] as ControllerHandler[]
1511

1512
export const _handleCreateWorkflowStep: ControllerHandler<
8✔
1513
  { formId: string },
1514
  FormWorkflowDto | ErrorDto,
1515
  FormWorkflowStepDto
1516
> = (req, res) => {
8✔
1517
  const { formId } = req.params
×
1518
  const workflowStepToCreate = req.body
×
1519
  const sessionUserId = (req.session as AuthedSessionData).user._id
×
1520

1521
  // Step 1: Retrieve currently logged in user.
1522
  return (
×
1523
    UserService.getPopulatedUserById(sessionUserId)
1524
      .andThen((user) =>
1525
        // Step 2: Retrieve form with write permission check.
1526
        AuthService.getFormAfterPermissionChecks({
×
1527
          user,
1528
          formId,
1529
          level: PermissionLevel.Write,
1530
        }),
1531
      )
1532
      // Step 3: User has permissions, proceed to create form field with provided body.
1533
      .andThen((form) =>
1534
        AdminFormService.createWorkflowStep(form, workflowStepToCreate),
×
1535
      )
1536
      .map((updatedWorkflow) =>
1537
        res.status(StatusCodes.OK).json(updatedWorkflow),
×
1538
      )
1539
      .mapErr((error) => {
1540
        logger.error({
×
1541
          message: 'Error occurred when creating form field',
1542
          meta: {
1543
            action: 'handleCreateFormField',
1544
            ...createReqMeta(req),
1545
            userId: sessionUserId,
1546
            formId,
1547
            workflowStepToCreate,
1548
          },
1549
          error,
1550
        })
1551
        const { errorMessage, statusCode } = mapRouteError(error)
×
1552
        return res.status(statusCode).json({ message: errorMessage })
×
1553
      })
1554
  )
1555
}
1556

1557
export const handleCreateWorkflowStep = [
8✔
1558
  createWorkflowStepValidator,
1559
  _handleCreateWorkflowStep,
1560
]
1561

1562
const _handleUpdateWorkflowStep: ControllerHandler<
1563
  {
1564
    formId: string
1565
    stepNumber: number
1566
  },
1567
  FormWorkflowDto | ErrorDto,
1568
  FormWorkflowStepDto
1569
> = (req, res) => {
8✔
1570
  const { formId, stepNumber } = req.params
×
1571
  const sessionUserId = (req.session as AuthedSessionData).user._id
×
1572
  const updatedWorkflowStep = req.body
×
1573

1574
  // Step 1: Retrieve currently logged in user.
1575
  return UserService.getPopulatedUserById(sessionUserId)
×
1576
    .andThen((user) =>
1577
      // Step 2: Retrieve form with write permission check.
1578
      AuthService.getFormAfterPermissionChecks({
×
1579
        user,
1580
        formId,
1581
        level: PermissionLevel.Write,
1582
      }),
1583
    )
1584
    .andThen((retrievedForm) =>
1585
      AdminFormService.updateFormWorkflowStep(
×
1586
        retrievedForm,
1587
        stepNumber,
1588
        updatedWorkflowStep,
1589
      ),
1590
    )
1591
    .map((updatedWorkflow) => res.status(StatusCodes.OK).json(updatedWorkflow))
×
1592
    .mapErr((error) => {
1593
      logger.error({
×
1594
        message: 'Error occurred when updating form workflow step',
1595
        meta: {
1596
          action: 'handleUpdateWorkflowStep',
1597
          ...createReqMeta(req),
1598
          userId: sessionUserId,
1599
          formId,
1600
          updatedWorkflowStep,
1601
        },
1602
        error,
1603
      })
1604
      const { errorMessage, statusCode } = mapRouteError(error)
×
1605
      return res.status(statusCode).json({ message: errorMessage })
×
1606
    })
1607
}
1608

1609
export const handleUpdateWorkflowStep = [
8✔
1610
  updateWorkflowStepValidator,
1611
  _handleUpdateWorkflowStep,
1612
] as ControllerHandler[]
1613

1614
export const handleDeleteWorkflowStep: ControllerHandler<
8✔
1615
  {
1616
    formId: string
1617
    stepNumber: number
1618
  },
1619
  FormWorkflowDto | ErrorDto
1620
> = (req, res) => {
8✔
1621
  const { formId, stepNumber } = req.params
×
1622
  const sessionUserId = (req.session as AuthedSessionData).user._id
×
1623

1624
  // Step 1: Retrieve currently logged in user.
1625
  return (
×
1626
    UserService.getPopulatedUserById(sessionUserId)
1627
      .andThen((user) =>
1628
        // Step 2: Retrieve form with write permission check.
1629
        AuthService.getFormAfterPermissionChecks({
×
1630
          user,
1631
          formId,
1632
          level: PermissionLevel.Write,
1633
        }),
1634
      )
1635
      // Step 3: Delete workflow step.
1636
      .andThen((retrievedForm) =>
1637
        AdminFormService.deleteFormWorkflowStep(retrievedForm, stepNumber),
×
1638
      )
1639
      .map((updatedWorkflow) =>
1640
        res.status(StatusCodes.OK).json(updatedWorkflow),
×
1641
      )
1642
      .mapErr((error) => {
1643
        logger.error({
×
1644
          message: 'Error occurred when deleting form workflow step',
1645
          meta: {
1646
            action: 'handleDeleteWorkflowStep',
1647
            ...createReqMeta(req),
1648
            userId: sessionUserId,
1649
            formId,
1650
            stepNumber,
1651
          },
1652
          error,
1653
        })
1654
        const { errorMessage, statusCode } = mapRouteError(error)
×
1655
        return res.status(statusCode).json({ message: errorMessage })
×
1656
      })
1657
  )
1658
}
1659

1660
const TWENTY_MB_IN_BYTES = 20 * 1024 * 1024
8✔
1661
const handleWhitelistSettingMultipartBody = multer({
8✔
1662
  limits: {
1663
    fieldSize: TWENTY_MB_IN_BYTES,
1664
    fields: 1, // only allow csv string field
1665
    files: 0,
1666
  },
1667
})
1668

1669
const _handleUpdateWhitelistSettingValidator = celebrate({
8✔
1670
  [Segments.PARAMS]: {
1671
    formId: Joi.string()
1672
      .required()
1673
      .pattern(/^[a-fA-F0-9]{24}$/)
1674
      .message('Your form ID is invalid.'),
1675
  },
1676
  [Segments.BODY]: {
1677
    whitelistCsvString: Joi.string()
1678
      .pattern(/^[a-zA-Z0-9,\r\n]+$/)
1679
      .messages({
1680
        'string.empty': 'Your csv is empty.',
1681
        'string.pattern.base': 'Your csv has one or more invalid characters.',
1682
      }),
1683
  },
1684
})
1685

1686
const _parseWhitelistCsvString = (whitelistCsvString: string | null) => {
8✔
1687
  if (!whitelistCsvString) {
3✔
1688
    return null
1✔
1689
  }
1690
  return whitelistCsvString.split('\r\n').map((entry: string) => entry.trim())
6✔
1691
}
1692

1693
const _handleUpdateWhitelistSetting: ControllerHandler<
1694
  { formId: string },
1695
  object,
1696
  { whitelistCsvString: string | null }
1697
> = async (req, res) => {
8✔
1698
  const { formId } = req.params
4✔
1699
  const sessionUserId = (req.session as AuthedSessionData).user._id
4✔
1700

1701
  const logMeta = {
4✔
1702
    action: '_handleUpdateWhitelistSetting',
1703
    ...createReqMeta(req),
1704
    userId: sessionUserId,
1705
    formId,
1706
  }
1707

1708
  // Step 1: Retrieve form only if currently logged in user has write permissions for form.
1709
  const formResult = await UserService.getPopulatedUserById(
4✔
1710
    sessionUserId,
1711
  ).andThen((user) =>
1712
    AuthService.getFormAfterPermissionChecks({
4✔
1713
      user,
1714
      formId,
1715
      level: PermissionLevel.Write,
1716
    }),
1717
  )
1718

1719
  if (formResult.isErr()) {
4✔
1720
    const { error } = formResult
1✔
1721
    logger.error({
1✔
1722
      message: 'Error occurred when updating form settings',
1723
      meta: logMeta,
1724
      error,
1725
    })
1726
    const { errorMessage, statusCode } = mapRouteError(error)
1✔
1727
    return res.status(statusCode).json({ message: errorMessage })
1✔
1728
  }
1729

1730
  const form = formResult.value
3✔
1731

1732
  const { whitelistCsvString } = req.body
3✔
1733
  const whitelistedSubmitterIds = _parseWhitelistCsvString(whitelistCsvString)
3✔
1734

1735
  const upperCaseWhitelistedSubmitterIds =
1736
    whitelistedSubmitterIds && whitelistedSubmitterIds.length > 0
3✔
1737
      ? whitelistedSubmitterIds.map((id) => id.toUpperCase())
6✔
1738
      : null
1739

1740
  // Step 2: perform validation on submitted whitelist setting
1741
  const isWhitelistSettingValid = AdminFormService.checkIsWhitelistSettingValid(
3✔
1742
    upperCaseWhitelistedSubmitterIds,
1743
  )
1744
  if (!isWhitelistSettingValid.isValid) {
3!
1745
    logger.error({
×
1746
      message: 'Invalid whitelist setting',
1747
      meta: logMeta,
1748
    })
1749
    return res.status(StatusCodes.UNPROCESSABLE_ENTITY).json({
×
1750
      message: isWhitelistSettingValid.invalidReason,
1751
    })
1752
  }
1753

1754
  // Step 3: Encrypt whitelist settings
1755
  if (!form.publicKey) {
3!
1756
    logger.error({
×
1757
      message: 'Form does not have a public key',
1758
      meta: logMeta,
1759
    })
1760
    return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
×
1761
      message: 'Form does not have a public key',
1762
    })
1763
  }
1764
  const formPublicKey = form.publicKey
3✔
1765
  const encryptedWhitelistSubmitterIdsContent = upperCaseWhitelistedSubmitterIds
3✔
1766
    ? encryptStringsMessage(upperCaseWhitelistedSubmitterIds, formPublicKey)
1767
    : null
1768

1769
  // Step 4: Update form with encrypted whitelist settings
1770
  return AdminFormService.updateFormWhitelistSetting(
3✔
1771
    form,
1772
    encryptedWhitelistSubmitterIdsContent,
1773
  )
1774
    .map((updatedSettings) => res.status(StatusCodes.OK).json(updatedSettings))
3✔
1775
    .mapErr((error) => {
1776
      logger.error({
×
1777
        message: 'Error occurred when updating form settings',
1778
        meta: {
1779
          action: 'handleUpdateSettings',
1780
          ...createReqMeta(req),
1781
          userId: sessionUserId,
1782
          formId,
1783
          // do not log the whitelist setting as it may contain sensitive data and be large in size
1784
        },
1785
        error,
1786
      })
1787
      const { errorMessage, statusCode } = mapRouteError(error)
×
1788
      return res.status(statusCode).json({ message: errorMessage })
×
1789
    })
1790
}
1791

1792
export const _handleUpdateWhitelistSettingForTest =
8✔
1793
  _handleUpdateWhitelistSetting
1794

1795
const loggerMiddleware =
1796
  (num: number) => async (req: Request, res: Response, next: NextFunction) => {
24✔
1797
    console.log(`XX102 request ${num}:`, req)
×
1798
    console.log(`XX103 request body ${num}:`, req?.body)
×
1799
    console.log(`XX104 response: ${num}`, res)
×
1800

1801
    next()
×
1802
  }
1803

1804
export const handleUpdateWhitelistSetting = [
8✔
1805
  loggerMiddleware(1),
1806
  handleWhitelistSettingMultipartBody.none(), // expecting string field
1807
  loggerMiddleware(2),
1808
  _handleUpdateWhitelistSettingValidator,
1809
  loggerMiddleware(3),
1810
  _handleUpdateWhitelistSetting,
1811
] as ControllerHandler[]
1812

1813
/**
1814
 * NOTE: Exported for testing.
1815
 * Private handler for PUT /forms/:formId/fields/:fieldId
1816
 * @precondition Must be preceded by request validation
1817
 */
1818
export const _handleUpdateFormField: ControllerHandler<
8✔
1819
  {
1820
    formId: string
1821
    fieldId: string
1822
  },
1823
  FormFieldDto | ErrorDto,
1824
  FieldUpdateDto
1825
> = (req, res) => {
8✔
1826
  const { formId, fieldId } = req.params
11✔
1827
  const updatedFormField = req.body
11✔
1828
  const sessionUserId = (req.session as AuthedSessionData).user._id
11✔
1829

1830
  // Step 1: Retrieve currently logged in user.
1831
  return (
11✔
1832
    UserService.getPopulatedUserById(sessionUserId)
1833
      .andThen((user) =>
1834
        // Step 2: Retrieve form with write permission check.
1835
        AuthService.getFormAfterPermissionChecks({
10✔
1836
          user,
1837
          formId,
1838
          level: PermissionLevel.Write,
1839
        }),
1840
      )
1841
      // Step 3: User has permissions, update form field of retrieved form.
1842
      .andThen((form) =>
1843
        AdminFormService.updateFormField(form, fieldId, updatedFormField),
7✔
1844
      )
1845
      .map((updatedFormField) =>
1846
        res.status(StatusCodes.OK).json(updatedFormField as FormFieldDto),
3✔
1847
      )
1848
      .mapErr((error) => {
1849
        logger.error({
8✔
1850
          message: 'Error occurred when updating form field',
1851
          meta: {
1852
            action: 'handleUpdateFormField',
1853
            ...createReqMeta(req),
1854
            userId: sessionUserId,
1855
            formId,
1856
            fieldId,
1857
            updateFieldBody: updatedFormField,
1858
          },
1859
          error,
1860
        })
1861
        const { errorMessage, statusCode } = mapRouteError(error)
8✔
1862
        return res.status(statusCode).json({ message: errorMessage })
8✔
1863
      })
1864
  )
1865
}
1866

1867
/**
1868
 * Handler for GET /form/:formId/settings.
1869
 * @security session
1870
 *
1871
 * @returns 200 with latest form settings on successful update
1872
 * @returns 403 when current user does not have permissions to obtain form settings
1873
 * @returns 404 when form to retrieve settings for cannot be found
1874
 * @returns 409 when saving form settings incurs a conflict in the database
1875
 * @returns 500 when database error occurs
1876
 */
1877
export const handleGetSettings: ControllerHandler<
8✔
1878
  { formId: string },
1879
  FormSettings | ErrorDto
1880
> = (req, res) => {
8✔
1881
  const { formId } = req.params
6✔
1882
  const sessionUserId = (req.session as AuthedSessionData).user._id
6✔
1883

1884
  return UserService.getPopulatedUserById(sessionUserId)
6✔
1885
    .andThen((user) =>
1886
      // Retrieve form for settings as well as for permissions checking
1887
      FormService.retrieveFullFormById(formId).map((form) => ({
6✔
1888
        form,
1889
        user,
1890
      })),
1891
    )
1892
    .andThen(AuthService.checkFormForPermissions(PermissionLevel.Read))
1893
    .map((form) => res.status(StatusCodes.OK).json(form.getSettings()))
1✔
1894
    .mapErr((error) => {
1895
      logger.error({
5✔
1896
        message: 'Error occurred when retrieving form settings',
1897
        meta: {
1898
          action: 'handleGetSettings',
1899
          ...createReqMeta(req),
1900
          userId: sessionUserId,
1901
          formId,
1902
        },
1903
        error,
1904
      })
1905
      const { errorMessage, statusCode } = mapRouteError(error)
5✔
1906
      return res.status(statusCode).json({ message: errorMessage })
5✔
1907
    })
1908
}
1909

1910
export const handleGetWhitelistSetting: ControllerHandler<
8✔
1911
  {
1912
    formId: string
1913
  },
1914
  | {
1915
      encryptedWhitelistedSubmitterIds: EncryptedStringsMessageContent | null
1916
    }
1917
  | ErrorDto
1918
> = (req, res) => {
8✔
1919
  const { formId } = req.params
2✔
1920
  const sessionUserId = (req.session as AuthedSessionData).user._id
2✔
1921

1922
  return UserService.getPopulatedUserById(sessionUserId)
2✔
1923
    .andThen((user) =>
1924
      // Retrieve form for settings as well as for permissions checking
1925
      FormService.retrieveFullFormById(formId).map((form) => ({
2✔
1926
        form,
1927
        user,
1928
      })),
1929
    )
1930
    .andThen(AuthService.checkFormForPermissions(PermissionLevel.Read))
1931
    .andThen((form) => EncryptSubmissionService.checkFormIsEncryptMode(form))
1✔
1932
    .map(async (form) => AdminFormService.getFormWhitelistSetting(form))
1✔
1933
    .andThen((formWhitelistedSubmitterIds) => formWhitelistedSubmitterIds)
1✔
1934
    .map((formWhitelistedSubmitterIds) => {
1935
      return res.status(StatusCodes.OK).json({
1✔
1936
        encryptedWhitelistedSubmitterIds: formWhitelistedSubmitterIds,
1937
      })
1938
    })
1939
    .mapErr((error: Error) => {
1940
      logger.error({
1✔
1941
        message: 'Error occurred when retrieving form whitelist settings',
1942
        meta: {
1943
          action: 'handleGetWhitelistSetting',
1944
          ...createReqMeta(req),
1945
          userId: sessionUserId,
1946
          formId,
1947
        },
1948
        error,
1949
      })
1950
      const { errorMessage, statusCode } = mapRouteError(error)
1✔
1951
      return res.status(statusCode).json({ message: errorMessage })
1✔
1952
    })
1953
}
1954

1955
/**
1956
 * Handler for POST api/public/v1/admin/forms/:formId/webhooksettings.
1957
 *
1958
 * @returns 200 with latest webhook and response mode settings
1959
 * @returns 401 when current user is not logged in
1960
 * @returns 403 when current user does not have permissions to obtain form settings
1961
 * @returns 404 when form to retrieve settings for cannot be found
1962
 * @returns 422 when user from user email cannot be retrieved from the database
1963
 * @returns 500 when database error occurs
1964
 */
1965
export const _handleGetWebhookSettings: ControllerHandler<
8✔
1966
  { formId: string },
1967
  FormWebhookResponseModeSettings | ErrorDto,
1968
  { userEmail: string }
1969
> = (req, res) => {
8✔
1970
  const { formId } = req.params
×
1971
  const { userEmail } = req.body
×
1972
  const authedUserId = (req.session as AuthedSessionData).user._id
×
1973

1974
  const logMeta = {
×
1975
    action: 'handleGetWebhookSettings',
1976
    ...createReqMeta(req),
1977
    userEmail,
1978
    formId,
1979
  }
1980

1981
  logger.info({
×
1982
    message: 'User attempting to get webhook settings',
1983
    meta: logMeta,
1984
  })
1985

1986
  return UserService.findUserById(authedUserId)
×
1987
    .mapErr((error) => {
1988
      logger.error({
×
1989
        message: 'Error occurred when retrieving user from database',
1990
        meta: logMeta,
1991
        error,
1992
      })
1993
      return error
×
1994
    })
1995
    .andThen((user) =>
1996
      // Retrieve form for settings as well as for permissions checking
1997
      FormService.retrieveFullFormById(formId).map((form) => ({
×
1998
        form,
1999
        user,
2000
      })),
2001
    )
2002
    .andThen(AuthService.checkFormForPermissions(PermissionLevel.Read))
2003
    .map((form) =>
2004
      res.status(StatusCodes.OK).json(form.getWebhookAndResponseModeSettings()),
×
2005
    )
2006
    .mapErr((error) => {
2007
      logger.error({
×
2008
        message: 'Error occurred when retrieving form settings',
2009
        meta: logMeta,
2010
        error,
2011
      })
2012
      const { errorMessage, statusCode } = mapRouteError(error)
×
2013
      return res.status(statusCode).json({ message: errorMessage })
×
2014
    })
2015
}
2016

2017
/**
2018
 * Handler for POST api/public/v1/admin/forms/:formId/webhooksettings.
2019
 * @security session
2020
 *
2021
 * @returns 200 with latest webhook and response mode settings
2022
 * @returns 400 when body is malformed
2023
 * @returns 401 when current user is not logged in
2024
 * @returns 403 when user email does not have permissions to obtain form settings
2025
 * @returns 404 when form to retrieve settings for cannot be found
2026
 * @returns 422 when user from user email cannot be retrieved from the database
2027
 * @returns 500 when database error occurs
2028
 */
2029
export const handleGetWebhookSettings = [
8✔
2030
  getWebhookSettingsValidator,
2031
  _handleGetWebhookSettings,
2032
] as ControllerHandler[]
2033

2034
/**
2035
 * Handler for POST /v2/submissions/encrypt/preview/:formId.
2036
 * @security session
2037
 *
2038
 * @returns 200 with a mock submission ID
2039
 * @returns 400 when body is malformed; e.g. invalid plaintext responses or encoding for encrypted content
2040
 * @returns 403 when current user does not have read permissions to given form
2041
 * @returns 404 when given form ID does not exist
2042
 * @returns 410 when given form has been deleted
2043
 * @returns 422 when user ID in session is not found in database
2044
 * @returns 500 when database error occurs
2045
 */
2046
export const submitEncryptPreview: ControllerHandler<
8✔
2047
  { formId: string },
2048
  { message: string; submissionId: string } | ErrorDto,
2049
  EncryptSubmissionDto
2050
> = async (req, res) => {
10✔
2051
  const { formId } = req.params
10✔
2052
  const sessionUserId = (req.session as AuthedSessionData).user._id
10✔
2053
  // No need to process attachments as we don't do anything with them
2054
  const logMeta = {
10✔
2055
    action: 'submitEncryptPreview',
2056
    formId,
2057
  }
2058

2059
  // eslint-disable-next-line typesafe/no-await-without-trycatch
2060
  return UserService.getPopulatedUserById(sessionUserId)
10✔
2061
    .andThen((user) =>
2062
      // Step 2: Retrieve form with write permission check.
2063
      AuthService.getFormAfterPermissionChecks({
8✔
2064
        user,
2065
        formId,
2066
        level: PermissionLevel.Read,
2067
      }),
2068
    )
2069
    .andThen((form) =>
2070
      EncryptSubmissionService.checkFormIsEncryptMode(form).mapErr((error) => {
4✔
2071
        logger.error({
1✔
2072
          message: 'Error while retrieving form for preview submission',
2073
          meta: logMeta,
2074
          error,
2075
        })
2076
        return error
1✔
2077
      }),
2078
    )
2079
    .map(() => {
2080
      const fakeSubmissionId = new ObjectId().toString()
3✔
2081
      // Return the reply early to the submitter
2082
      return res.json({
3✔
2083
        message: 'Form submission successful.',
2084
        submissionId: fakeSubmissionId,
2085
      })
2086
    })
2087
    .mapErr((error) => {
2088
      const { errorMessage, statusCode } = mapSubmissionError(error)
7✔
2089
      return res.status(statusCode).json({ message: errorMessage })
7✔
2090
    })
2091
}
2092

2093
export const handleEncryptPreviewSubmission = [
8✔
2094
  submitEncryptPreview,
2095
] as ControllerHandler[]
2096

2097
/**
2098
 * Handler for POST /v2/submissions/email/preview/:formId.
2099
 * @security session
2100
 *
2101
 * @returns 200 with a mock submission ID
2102
 * @returns 400 when body is malformed; e.g. invalid responses, or when admin email fails to be sent
2103
 * @returns 403 when current user does not have read permissions to given form
2104
 * @returns 404 when given form ID does not exist
2105
 * @returns 410 when given form has been deleted
2106
 * @returns 422 when user ID in session is not found in database
2107
 * @returns 500 when database error occurs
2108
 */
2109
export const submitEmailPreview: ControllerHandler<
8✔
2110
  { formId: string },
2111
  { message: string; submissionId?: string },
2112
  ParsedEmailModeSubmissionBody,
2113
  { captchaResponse?: unknown }
2114
> = async (req, res) => {
22✔
2115
  const { formId } = req.params
22✔
2116
  const sessionUserId = (req.session as AuthedSessionData).user._id
22✔
2117
  // No need to process attachments as we don't do anything with them
2118
  const { responses } = req.body
22✔
2119
  const logMeta = {
22✔
2120
    action: 'submitEmailPreview',
2121
    formId,
2122
    ...createReqMeta(req),
2123
  }
2124

2125
  const formResult = await UserService.getPopulatedUserById(sessionUserId)
22✔
2126
    .andThen((user) =>
2127
      AuthService.getFormAfterPermissionChecks({
20✔
2128
        user,
2129
        formId,
2130
        level: PermissionLevel.Read,
2131
      }),
2132
    )
2133
    .andThen(EmailSubmissionService.checkFormIsEmailMode)
2134
  if (formResult.isErr()) {
22✔
2135
    logger.error({
7✔
2136
      message: 'Error while retrieving form for preview submission',
2137
      meta: logMeta,
2138
      error: formResult.error,
2139
    })
2140
    const { errorMessage, statusCode } = mapEmailSubmissionError(
7✔
2141
      formResult.error,
2142
    )
2143
    return res.status(statusCode).json({ message: errorMessage })
7✔
2144
  }
2145
  const form = formResult.value
15✔
2146

2147
  const parsedResponsesResult = await SubmissionService.validateAttachments(
15✔
2148
    responses,
2149
    form.responseMode,
2150
  ).andThen(() => ParsedResponsesObject.parseResponses(form, responses))
13✔
2151
  if (parsedResponsesResult.isErr()) {
15✔
2152
    logger.error({
5✔
2153
      message: 'Error while parsing responses for preview submission',
2154
      meta: logMeta,
2155
      error: parsedResponsesResult.error,
2156
    })
2157
    const { errorMessage, statusCode } = mapEmailSubmissionError(
5✔
2158
      parsedResponsesResult.error,
2159
    )
2160
    return res.status(statusCode).json({ message: errorMessage })
5✔
2161
  }
2162
  const parsedResponses = parsedResponsesResult.value
10✔
2163
  const attachments = mapAttachmentsFromResponses(req.body.responses)
10✔
2164

2165
  // Handle SingPass, CorpPass and MyInfo authentication and validation
2166
  const { authType } = form
10✔
2167
  if (authType === FormAuthType.SP || authType === FormAuthType.MyInfo) {
10!
2168
    parsedResponses.addNdiResponses({
×
2169
      authType,
2170
      uinFin: PREVIEW_SINGPASS_UINFIN,
2171
    })
2172
  } else if (authType === FormAuthType.CP) {
10!
2173
    parsedResponses.addNdiResponses({
×
2174
      authType,
2175
      uinFin: PREVIEW_CORPPASS_UINFIN,
2176
      userInfo: PREVIEW_CORPPASS_UID,
2177
    })
2178
  }
2179

2180
  const emailData = new SubmissionEmailObj(
10✔
2181
    parsedResponses.getAllResponses(),
2182
    // All MyInfo fields are verified in preview
2183
    new Set(AdminFormService.extractMyInfoFieldIds(form.form_fields)),
2184
    form.authType,
2185
  )
2186
  const submission = EmailSubmissionService.createEmailSubmissionWithoutSave(
10✔
2187
    form,
2188
    // Don't need to care about response hash or salt
2189
    '',
2190
    '',
2191
  )
2192

2193
  const sendAdminEmailResult = await MailService.sendSubmissionToAdmin({
10✔
2194
    replyToEmails: EmailSubmissionService.extractEmailAnswers(
2195
      parsedResponses.getAllResponses(),
2196
    ),
2197
    form,
2198
    submission,
2199
    attachments,
2200
    dataCollationData: emailData.dataCollationData,
2201
    formData: emailData.formData,
2202
  })
2203
  if (sendAdminEmailResult.isErr()) {
10✔
2204
    logger.error({
2✔
2205
      message: 'Error sending submission to admin',
2206
      meta: logMeta,
2207
      error: sendAdminEmailResult.error,
2208
    })
2209
    const { statusCode, errorMessage } = mapEmailSubmissionError(
2✔
2210
      sendAdminEmailResult.error,
2211
    )
2212
    return res.status(statusCode).json({
2✔
2213
      message: errorMessage,
2214
    })
2215
  }
2216

2217
  // Don't await on email confirmations, so submission is successful even if
2218
  // this fails
2219
  void SubmissionService.sendEmailConfirmations({
8✔
2220
    form,
2221
    submission,
2222
    attachments,
2223
    responsesData: emailData.autoReplyData,
2224
    recipientData: extractEmailConfirmationData(
2225
      parsedResponses.getAllResponses(),
2226
      form.form_fields,
2227
    ),
2228
  }).mapErr((error) => {
2229
    logger.error({
1✔
2230
      message: 'Error while sending email confirmations',
2231
      meta: logMeta,
2232
      error,
2233
    })
2234
  })
2235

2236
  return res.json({
8✔
2237
    message: 'Form submission successful.',
2238
    submissionId: submission.id,
2239
  })
2240
}
2241

2242
export const handleEmailPreviewSubmission = [
8✔
2243
  ReceiverMiddleware.receiveEmailSubmission,
2244
  EmailSubmissionMiddleware.validateResponseParams,
2245
  submitEmailPreview,
2246
] as ControllerHandler[]
2247

2248
/**
2249
 * Handler for PUT /forms/:formId/fields/:fieldId
2250
 * @security session
2251
 *
2252
 * @returns 200 with updated form field
2253
 * @returns 403 when current user does not have permissions to update form field
2254
 * @returns 404 when form cannot be found
2255
 * @returns 404 when form field cannot be found
2256
 * @returns 409 when form field update conflicts with database state
2257
 * @returns 410 when updating form field of an archived form
2258
 * @returns 413 when updating form field causes form to be too large to be saved in the database
2259
 * @returns 422 when an invalid form field update is attempted on the form
2260
 * @returns 422 when user in session cannot be retrieved from the database
2261
 * @returns 500 when database error occurs
2262
 */
2263
export const handleUpdateFormField = [
8✔
2264
  celebrate(
2265
    {
2266
      [Segments.BODY]: Joi.object({
2267
        // Ensures given field is same as accessed field.
2268
        _id: Joi.string().valid(Joi.ref('$params.fieldId')).required(),
2269
        fieldType: Joi.string()
2270
          .valid(...Object.values(BasicField))
2271
          .required(),
2272
        description: Joi.string().allow('').required(),
2273
        required: Joi.boolean().required(),
2274
        title: Joi.string().trim().required(),
2275
        disabled: Joi.boolean().required(),
2276
        // Allow other field related key-values to be provided and let the model
2277
        // layer handle the validation.
2278
      })
2279
        .unknown(true)
2280
        .custom((value, helpers) => verifyValidUnicodeString(value, helpers)),
3✔
2281
    },
2282
    undefined,
2283
    // Required so req.body can be validated against values in req.params.
2284
    // See https://github.com/arb/celebrate#celebrateschema-joioptions-opts.
2285
    { reqContext: true },
2286
  ),
2287
  _handleUpdateFormField,
2288
]
2289

2290
const _handleUpdateOptionsToRecipientsMap: ControllerHandler<
2291
  {
2292
    formId: string
2293
    fieldId: string
2294
  },
2295
  FormFieldDto | ErrorDto,
2296
  { optionsToRecipientsMap: Record<string, string[]> }
2297
> = (req, res) => {
8✔
2298
  const { formId, fieldId } = req.params
×
2299
  const { optionsToRecipientsMap } = req.body
×
2300
  const sessionUserId = (req.session as AuthedSessionData).user._id
×
2301

2302
  // Step 1: Retrieve currently logged in user.
2303
  return UserService.getPopulatedUserById(sessionUserId)
×
2304
    .andThen((user) =>
2305
      // Step 2: Retrieve form with write permission check.
2306
      AuthService.getFormAfterPermissionChecks({
×
2307
        user,
2308
        formId,
2309
        level: PermissionLevel.Write,
2310
      }),
2311
    )
2312
    .andThen((form) => {
2313
      return AdminFormService.updateOptionsToRecipientsMap(
×
2314
        form,
2315
        fieldId,
2316
        optionsToRecipientsMap,
2317
      )
2318
    })
2319
    .map((updatedFormField) =>
2320
      res.status(StatusCodes.OK).json(updatedFormField as FormFieldDto),
×
2321
    )
2322
    .mapErr((error) => {
2323
      logger.error({
×
2324
        message: 'Error occurred when updating options to recipients map',
2325
        meta: {
2326
          action: '_handleUpdateOptionsToRecipientsMap',
2327
          ...createReqMeta(req),
2328
          userId: sessionUserId,
2329
          formId,
2330
          fieldId,
2331
          optionsToRecipientsMap,
2332
        },
2333
        error,
2334
      })
2335
      const { errorMessage, statusCode } = mapRouteError(error)
×
2336
      return res.status(statusCode).json({ message: errorMessage })
×
2337
    })
2338
}
2339

2340
export const handleUpdateOptionsToRecipientsMap = [
8✔
2341
  celebrate({
2342
    [Segments.BODY]: Joi.object({
2343
      optionsToRecipientsMap: Joi.object(),
2344
    }),
2345
    [Segments.PARAMS]: Joi.object({
2346
      formId: Joi.string().required(),
2347
      fieldId: Joi.string().required(),
2348
    }),
2349
  }),
2350
  _handleUpdateOptionsToRecipientsMap,
2351
] as ControllerHandler[]
2352

2353
/**
2354
 * NOTE: Exported for testing.
2355
 * Private handler for POST /forms/:formId/fields
2356
 * @precondition Must be preceded by request validation
2357
 * @security session
2358
 *
2359
 * @returns 200 with created form field
2360
 * @returns 403 when current user does not have permissions to create a form field
2361
 * @returns 404 when form cannot be found
2362
 * @returns 409 when form field update conflicts with database state
2363
 * @returns 410 when creating form field for an archived form
2364
 * @returns 413 when creating form field causes form to be too large to be saved in the database
2365
 * @returns 422 when an invalid form field creation is attempted on the form
2366
 * @returns 422 when user in session cannot be retrieved from the database
2367
 * @returns 500 when database error occurs
2368
 */
2369
export const _handleCreateFormField: ControllerHandler<
8✔
2370
  { formId: string },
2371
  FormFieldDto | ErrorDto,
2372
  FieldCreateDto,
2373
  { to?: number }
2374
> = (req, res) => {
8✔
2375
  const { formId } = req.params
19✔
2376
  const { to } = req.query
19✔
2377
  const formFieldToCreate = req.body
19✔
2378
  const sessionUserId = (req.session as AuthedSessionData).user._id
19✔
2379

2380
  // Step 1: Retrieve currently logged in user.
2381
  return (
19✔
2382
    UserService.getPopulatedUserById(sessionUserId)
2383
      .andThen((user) =>
2384
        // Step 2: Retrieve form with write permission check.
2385
        AuthService.getFormAfterPermissionChecks({
17✔
2386
          user,
2387
          formId,
2388
          level: PermissionLevel.Write,
2389
        }),
2390
      )
2391
      // Step 3: User has permissions, proceed to create form field with provided body.
2392
      .andThen((form) =>
2393
        AdminFormService.createFormField(form, formFieldToCreate, to),
13✔
2394
      )
2395
      .map((createdFormField) =>
2396
        res.status(StatusCodes.OK).json(createdFormField as FormFieldDto),
10✔
2397
      )
2398
      .mapErr((error) => {
2399
        logger.error({
9✔
2400
          message: 'Error occurred when creating form field',
2401
          meta: {
2402
            action: '_handleCreateFormField',
2403
            ...createReqMeta(req),
2404
            userId: sessionUserId,
2405
            formId,
2406
            createFieldBody: formFieldToCreate,
2407
          },
2408
          error,
2409
        })
2410
        const { errorMessage, statusCode } = mapRouteError(error)
9✔
2411
        return res.status(statusCode).json({ message: errorMessage })
9✔
2412
      })
2413
  )
2414
}
2415

2416
/**
2417
 * NOTE: Exported for testing.
2418
 * Private handler for POST /forms/:formId/logic
2419
 * @precondition Must be preceded by request validation
2420
 * @security session
2421
 *
2422
 * @returns 200 with created logic object when successfully created
2423
 * @returns 403 when user does not have permissions to create logic
2424
 * @returns 404 when form cannot be found
2425
 * @returns 422 when user in session cannot be retrieved from the database
2426
 * @returns 500 when database error occurs
2427
 */
2428
export const _handleCreateLogic: ControllerHandler<
8✔
2429
  { formId: string },
2430
  LogicDto | ErrorDto,
2431
  LogicDto
2432
> = (req, res) => {
8✔
2433
  const { formId } = req.params
5✔
2434
  const createLogicBody = req.body
5✔
2435
  const sessionUserId = (req.session as AuthedSessionData).user._id
5✔
2436

2437
  // Step 1: Retrieve currently logged in user.
2438
  return (
5✔
2439
    UserService.getPopulatedUserById(sessionUserId)
2440
      .andThen((user) =>
2441
        // Step 2: Retrieve form with write permission check.
2442
        AuthService.getFormAfterPermissionChecks({
3✔
2443
          user,
2444
          formId,
2445
          level: PermissionLevel.Write,
2446
        }),
2447
      )
2448
      // Step 3: Create form logic
2449
      .andThen((retrievedForm) =>
2450
        AdminFormService.createFormLogic(retrievedForm, createLogicBody),
1✔
2451
      )
2452
      .map((createdLogic) =>
2453
        res.status(StatusCodes.OK).json(createdLogic as LogicDto),
1✔
2454
      )
2455
      .mapErr((error) => {
2456
        logger.error({
4✔
2457
          message: 'Error occurred when creating form logic',
2458
          meta: {
2459
            action: 'handleCreateLogic',
2460
            ...createReqMeta(req),
2461
            userId: sessionUserId,
2462
            formId,
2463
            createLogicBody,
2464
          },
2465
          error,
2466
        })
2467
        const { errorMessage, statusCode } = mapRouteError(error)
4✔
2468
        return res.status(statusCode).json({ message: errorMessage })
4✔
2469
      })
2470
  )
2471
}
2472

2473
/**
2474
 * Shape of request body used for joi validation for create and update logic
2475
 */
2476
const joiLogicBody = {
8✔
2477
  logicType: Joi.string()
2478
    .valid(...Object.values(LogicType))
2479
    .required(),
2480
  conditions: Joi.array()
2481
    .items(
2482
      Joi.object({
2483
        field: Joi.string().required(),
2484
        state: Joi.string()
2485
          .valid(...Object.values(LogicConditionState))
2486
          .required(),
2487
        value: Joi.alternatives()
2488
          .try(
2489
            Joi.number(),
2490
            Joi.string(),
2491
            Joi.array().items(Joi.string()),
2492
            Joi.array().items(Joi.number()),
2493
          )
2494
          .required(),
2495
        ifValueType: Joi.string()
2496
          .valid(...Object.values(LogicIfValue))
2497
          .required(),
2498
      }).unknown(true),
2499
    )
2500
    .required(),
2501
  show: Joi.alternatives().conditional('logicType', {
2502
    is: LogicType.ShowFields,
2503
    then: Joi.array().items(Joi.string()).required(),
2504
  }),
2505
  preventSubmitMessage: Joi.alternatives().conditional('logicType', {
2506
    is: LogicType.PreventSubmit,
2507
    then: Joi.string().required(),
2508
  }),
2509
  preventSubmitMessageTranslations: Joi.alternatives().conditional(
2510
    'logicType',
2511
    {
2512
      is: LogicType.PreventSubmit,
2513
      then: Joi.array()
2514
        .items(
2515
          Joi.object({
2516
            language: Joi.string()
2517
              .valid(...Object.values(Language))
2518
              .required(),
2519
            translation: Joi.string().required(),
2520
          }),
2521
        )
2522
        .optional()
2523
        .default([]),
2524
    },
2525
  ),
2526
}
2527

2528
/**
2529
 * Handler for POST /forms/:formId/logic
2530
 */
2531
export const handleCreateLogic = [
8✔
2532
  celebrate({
2533
    [Segments.BODY]: joiLogicBody,
2534
  }),
2535
  _handleCreateLogic,
2536
] as ControllerHandler[]
2537

2538
/**
2539
 * Handler for DELETE /forms/:formId/logic/:logicId
2540
 * @security session
2541
 *
2542
 * @returns 200 with success message when successfully deleted
2543
 * @returns 403 when user does not have permissions to delete logic
2544
 * @returns 404 when form cannot be found
2545
 * @returns 422 when user in session cannot be retrieved from the database
2546
 * @returns 500 when database error occurs
2547
 */
2548
export const handleDeleteLogic: ControllerHandler<{
8✔
2549
  formId: string
2550
  logicId: string
2551
}> = (req, res) => {
8✔
2552
  const { formId, logicId } = req.params
12✔
2553
  const sessionUserId = (req.session as AuthedSessionData).user._id
12✔
2554

2555
  // Step 1: Retrieve currently logged in user.
2556
  return (
12✔
2557
    UserService.getPopulatedUserById(sessionUserId)
2558
      .andThen((user) =>
2559
        // Step 2: Retrieve form with write permission check.
2560
        AuthService.getFormAfterPermissionChecks({
9✔
2561
          user,
2562
          formId,
2563
          level: PermissionLevel.Write,
2564
        }),
2565
      )
2566

2567
      // Step 3: Delete form logic
2568
      .andThen((retrievedForm) =>
2569
        AdminFormService.deleteFormLogic(retrievedForm, logicId),
5✔
2570
      )
2571
      .map(() => res.sendStatus(StatusCodes.OK))
3✔
2572
      .mapErr((error) => {
2573
        logger.error({
9✔
2574
          message: 'Error occurred when deleting form logic',
2575
          meta: {
2576
            action: 'handleDeleteLogic',
2577
            ...createReqMeta(req),
2578
            userId: sessionUserId,
2579
            formId,
2580
            logicId,
2581
          },
2582
          error,
2583
        })
2584
        const { errorMessage, statusCode } = mapRouteError(error)
9✔
2585
        return res.status(statusCode).json({ message: errorMessage })
9✔
2586
      })
2587
  )
2588
}
2589

2590
/**
2591
 * Handler for POST /forms/:formId/fields
2592
 */
2593
export const handleCreateFormField = [
8✔
2594
  celebrate({
2595
    [Segments.BODY]: Joi.object({
2596
      // Ensures id is not provided.
2597
      _id: Joi.any().forbidden(),
2598
      globalId: Joi.any().forbidden(),
2599
      fieldType: Joi.string()
2600
        .valid(...Object.values(BasicField))
2601
        .required(),
2602
      title: Joi.string().trim().required(),
2603
      description: Joi.string().allow(''),
2604
      required: Joi.boolean(),
2605
      disabled: Joi.boolean(),
2606
      // Allow other field related key-values to be provided and let the model
2607
      // layer handle the validation.
2608
    })
2609
      .unknown(true)
2610
      .custom((value, helpers) => verifyValidUnicodeString(value, helpers)),
9✔
2611
    [Segments.QUERY]: {
2612
      // Optional index to insert the field at.
2613
      to: Joi.number().min(0),
2614
    },
2615
  }),
2616
  _handleCreateFormField,
2617
]
2618

2619
/**
2620
 * NOTE: Exported for testing.
2621
 * Private handler for POST /forms/:formId/fields/:fieldId/reorder
2622
 * @precondition Must be preceded by request validation
2623
 * @security session
2624
 *
2625
 * @returns 200 with new ordering of form fields
2626
 * @returns 403 when current user does not have permissions to create a form field
2627
 * @returns 404 when form cannot be found
2628
 * @returns 404 when given fieldId cannot be found in form
2629
 * @returns 410 when reordering form fields for an archived form
2630
 * @returns 422 when user in session cannot be retrieved from the database
2631
 * @returns 500 when database error occurs
2632
 */
2633
export const _handleReorderFormField: ControllerHandler<
8✔
2634
  { formId: string; fieldId: string },
2635
  FormFieldDto[] | ErrorDto,
2636
  unknown,
2637
  { to: number }
2638
> = (req, res) => {
8✔
2639
  const { formId, fieldId } = req.params
11✔
2640
  const { to } = req.query
11✔
2641
  const sessionUserId = (req.session as AuthedSessionData).user._id
11✔
2642

2643
  // Step 1: Retrieve currently logged in user.
2644
  return (
11✔
2645
    UserService.getPopulatedUserById(sessionUserId)
2646
      .andThen((user) =>
2647
        // Step 2: Retrieve form with write permission check.
2648
        AuthService.getFormAfterPermissionChecks({
9✔
2649
          user,
2650
          formId,
2651
          level: PermissionLevel.Write,
2652
        }),
2653
      )
2654
      // Step 3: User has permissions, proceed to reorder field
2655
      .andThen((form) => AdminFormService.reorderFormField(form, fieldId, to))
5✔
2656
      .map((reorderedFormFields) =>
2657
        res.status(StatusCodes.OK).json(reorderedFormFields as FormFieldDto[]),
1✔
2658
      )
2659
      .mapErr((error) => {
2660
        logger.error({
10✔
2661
          message: 'Error occurred when reordering form field',
2662
          meta: {
2663
            action: '_handleReorderFormField',
2664
            ...createReqMeta(req),
2665
            userId: sessionUserId,
2666
            formId,
2667
            fieldId,
2668
            reqQuery: req.query,
2669
          },
2670
          error,
2671
        })
2672
        const { errorMessage, statusCode } = mapRouteError(error)
10✔
2673
        return res.status(statusCode).json({ message: errorMessage })
10✔
2674
      })
2675
  )
2676
}
2677

2678
/**
2679
 * Handler for POST /forms/:formId/fields/:fieldId/reorder
2680
 */
2681
export const handleReorderFormField = [
8✔
2682
  celebrate({
2683
    [Segments.QUERY]: {
2684
      to: Joi.number().min(0).required(),
2685
    },
2686
  }),
2687
  _handleReorderFormField,
2688
] as ControllerHandler[]
2689

2690
/**
2691
 * NOTE: Exported for testing.
2692
 * Private handler for PUT /forms/:formId/logic/:logicId
2693
 * @precondition Must be preceded by request validation
2694
 * @security session
2695
 *
2696
 * @returns 200 with updated logic object when successfully updated
2697
 * @returns 403 when user does not have permissions to update logic
2698
 * @returns 404 when form cannot be found
2699
 * @returns 422 when user in session cannot be retrieved from the database
2700
 * @returns 500 when database error occurs
2701
 */
2702
export const _handleUpdateLogic: ControllerHandler<
8✔
2703
  { formId: string; logicId: string },
2704
  LogicDto | ErrorDto,
2705
  LogicDto
2706
> = (req, res) => {
8✔
2707
  const { formId, logicId } = req.params
12✔
2708
  const updatedLogic = { ...req.body }
12✔
2709
  const sessionUserId = (req.session as AuthedSessionData).user._id
12✔
2710

2711
  // Step 1: Retrieve currently logged in user.
2712
  return (
12✔
2713
    UserService.getPopulatedUserById(sessionUserId)
2714
      .andThen((user) =>
2715
        // Step 2: Retrieve form with write permission check.
2716
        AuthService.getFormAfterPermissionChecks({
9✔
2717
          user,
2718
          formId,
2719
          level: PermissionLevel.Write,
2720
        }),
2721
      )
2722
      // Step 3: Update form logic
2723
      .andThen((retrievedForm) =>
2724
        AdminFormService.updateFormLogic(retrievedForm, logicId, updatedLogic),
5✔
2725
      )
2726
      .map((updatedLogic) =>
2727
        res.status(StatusCodes.OK).json(updatedLogic as LogicDto),
3✔
2728
      )
2729
      .mapErr((error) => {
2730
        logger.error({
9✔
2731
          message: 'Error occurred when updating form logic',
2732
          meta: {
2733
            action: 'handleUpdateLogic',
2734
            ...createReqMeta(req),
2735
            userId: sessionUserId,
2736
            formId,
2737
            logicId,
2738
            updatedLogic,
2739
          },
2740
          error,
2741
        })
2742
        const { errorMessage, statusCode } = mapRouteError(error)
9✔
2743
        return res.status(statusCode).json({ message: errorMessage })
9✔
2744
      })
2745
  )
2746
}
2747

2748
/**
2749
 * Handler for PUT /forms/:formId/logic/:logicId
2750
 */
2751
export const handleUpdateLogic = [
8✔
2752
  celebrate(
2753
    {
2754
      [Segments.BODY]: Joi.object({
2755
        // Ensures given logic is same as accessed logic
2756
        _id: Joi.string().valid(Joi.ref('$params.logicId')).required(),
2757
        ...joiLogicBody,
2758
      }),
2759
    },
2760
    undefined,
2761
    // Required so req.body can be validated against values in req.params.
2762
    // See https://github.com/arb/celebrate#celebrateschema-joioptions-opts.
2763
    { reqContext: true },
2764
  ),
2765
  _handleUpdateLogic,
2766
] as ControllerHandler[]
2767

2768
/**
2769
 * Handler for DELETE /forms/:formId/fields/:fieldId
2770
 * @security session
2771
 *
2772
 * @returns 204 when deletion is successful
2773
 * @returns 403 when current user does not have permissions to delete form field
2774
 * @returns 404 when form cannot be found
2775
 * @returns 404 when form field to delete cannot be found
2776
 * @returns 410 when deleting form field of an archived form
2777
 * @returns 422 when user in session cannot be retrieved from the database
2778
 * @returns 500 when database error occurs during deletion
2779
 */
2780
export const handleDeleteFormField: ControllerHandler<
8✔
2781
  { formId: string; fieldId: string },
2782
  ErrorDto | void
2783
> = (req, res) => {
8✔
2784
  const { formId, fieldId } = req.params
7✔
2785
  const sessionUserId = (req.session as AuthedSessionData).user._id
7✔
2786

2787
  return (
7✔
2788
    // Step 1: Retrieve currently logged in user.
2789
    UserService.getPopulatedUserById(sessionUserId)
2790
      .andThen((user) =>
2791
        // Step 2: Retrieve form with write permission check.
2792
        AuthService.getFormAfterPermissionChecks({
6✔
2793
          user,
2794
          formId,
2795
          level: PermissionLevel.Write,
2796
        }),
2797
      )
2798
      // Step 3: Delete form field.
2799
      .andThen((form) => AdminFormService.deleteFormField(form, fieldId))
3✔
2800
      .map(() => res.sendStatus(StatusCodes.NO_CONTENT))
1✔
2801
      .mapErr((error) => {
2802
        logger.error({
6✔
2803
          message: 'Error occurred when deleting form field',
2804
          meta: {
2805
            action: 'handleDeleteFormField',
2806
            ...createReqMeta(req),
2807
            userId: sessionUserId,
2808
            formId,
2809
            fieldId,
2810
          },
2811
          error,
2812
        })
2813
        const { errorMessage, statusCode } = mapRouteError(error)
6✔
2814
        return res.status(statusCode).json({ message: errorMessage })
6✔
2815
      })
2816
  )
2817
}
2818

2819
/**
2820
 * NOTE: Exported for testing.
2821
 * Private handler for PUT /forms/:formId/end-page
2822
 * @precondition Must be preceded by request validation
2823
 * @security session
2824
 *
2825
 * @returns 200 with updated end page
2826
 * @returns 400 when end page form field has invalid updates to be performed
2827
 * @returns 403 when current user does not have permissions to update the end page
2828
 * @returns 404 when form cannot be found
2829
 * @returns 410 when updating the end page for an archived form
2830
 * @returns 422 when user in session cannot be retrieved from the database
2831
 * @returns 500 when database error occurs
2832
 */
2833
export const _handleUpdateEndPage: ControllerHandler<
8✔
2834
  { formId: string },
2835
  IFormDocument['endPage'] | ErrorDto,
2836
  EndPageUpdateDto
2837
> = (req, res) => {
8✔
2838
  const { formId } = req.params
12✔
2839
  const sessionUserId = (req.session as AuthedSessionData).user._id
12✔
2840

2841
  // Step 1: Retrieve currently logged in user.
2842
  return (
12✔
2843
    UserService.getPopulatedUserById(sessionUserId)
2844
      .andThen((user) =>
2845
        // Step 2: Retrieve form with write permission check.
2846
        AuthService.getFormAfterPermissionChecks({
10✔
2847
          user,
2848
          formId,
2849
          level: PermissionLevel.Write,
2850
        }),
2851
      )
2852
      // Step 3: User has permissions, proceed to allow updating of end page
2853
      .andThen(() => AdminFormService.updateEndPage(formId, req.body))
6✔
2854
      .map((updatedEndPage) => res.status(StatusCodes.OK).json(updatedEndPage))
3✔
2855
      .mapErr((error) => {
2856
        logger.error({
9✔
2857
          message: 'Error occurred when updating end page',
2858
          meta: {
2859
            action: '_handleUpdateEndPage',
2860
            ...createReqMeta(req),
2861
            userId: sessionUserId,
2862
            formId,
2863
            body: req.body,
2864
          },
2865
          error,
2866
        })
2867
        const { errorMessage, statusCode } = mapRouteError(error)
9✔
2868
        return res.status(statusCode).json({ message: errorMessage })
9✔
2869
      })
2870
  )
2871
}
2872

2873
/**
2874
 * Handler for PUT /forms/:formId/end-page
2875
 */
2876
export const handleUpdateEndPage = [
8✔
2877
  celebrate({
2878
    [Segments.BODY]: Joi.object({
2879
      title: Joi.string(),
2880
      paragraph: Joi.string().allow(''),
2881
      buttonLink: Joi.string()
2882
        .uri({ scheme: ['http', 'https'] })
2883
        .allow('')
2884
        .message('Please enter a valid HTTP or HTTPS URI'),
2885
      buttonText: Joi.string().allow(''),
2886
      // TODO(#1895): Remove when deprecated `buttons` key is removed from all forms in the database
2887
      titleTranslations: Joi.array()
2888
        .items(
2889
          Joi.object({
2890
            language: Joi.string()
2891
              .valid(...Object.values(Language))
2892
              .required(),
2893
            translation: Joi.string().required(),
2894
          }),
2895
        )
2896
        .optional()
2897
        .default([]),
2898
      paragraphTranslations: Joi.array()
2899
        .items(
2900
          Joi.object({
2901
            language: Joi.string()
2902
              .valid(...Object.values(Language))
2903
              .required(),
2904
            translation: Joi.string().required(),
2905
          }),
2906
        )
2907
        .optional()
2908
        .default([]),
2909
    }).unknown(true),
2910
  }),
2911
  _handleUpdateEndPage,
2912
] as ControllerHandler[]
2913

2914
/**
2915
 * Handler for GET /admin/forms/:formId/fields/:fieldId
2916
 * @security session
2917
 *
2918
 * @returns 200 with form field when retrieval is successful
2919
 * @returns 403 when current user does not have permissions to retrieve form field
2920
 * @returns 404 when form cannot be found
2921
 * @returns 404 when form field cannot be found
2922
 * @returns 410 when retrieving form field of an archived form
2923
 * @returns 422 when user in session cannot be retrieved from the database
2924
 * @returns 500 when database error occurs
2925
 */
2926
export const handleGetFormField: ControllerHandler<
8✔
2927
  {
2928
    formId: string
2929
    fieldId: string
2930
  },
2931
  ErrorDto | FormFieldDto
2932
> = (req, res) => {
8✔
2933
  const { formId, fieldId } = req.params
14✔
2934
  const sessionUserId = (req.session as AuthedSessionData).user._id
14✔
2935

2936
  return (
14✔
2937
    // Step 1: Retrieve currently logged in user.
2938
    UserService.getPopulatedUserById(sessionUserId)
2939
      .andThen((user) =>
2940
        // Step 2: Retrieve form with read permission check.
2941
        AuthService.getFormAfterPermissionChecks({
11✔
2942
          user,
2943
          formId,
2944
          level: PermissionLevel.Read,
2945
        }),
2946
      )
2947
      .andThen((form) => AdminFormService.getFormField(form, fieldId))
5✔
2948
      .map((formField) =>
2949
        res.status(StatusCodes.OK).json(formField as FormFieldDto),
2✔
2950
      )
2951
      .mapErr((error) => {
2952
        logger.error({
12✔
2953
          message: 'Error occurred when retrieving form field',
2954
          meta: {
2955
            action: 'handleGetFormField',
2956
            ...createReqMeta(req),
2957
            userId: sessionUserId,
2958
            formId,
2959
            fieldId,
2960
          },
2961
          error,
2962
        })
2963
        const { errorMessage, statusCode } = mapRouteError(error)
12✔
2964
        return res.status(statusCode).json({ message: errorMessage })
12✔
2965
      })
2966
  )
2967
}
2968

2969
/**
2970
 * NOTE: Exported for testing.
2971
 * Private handler for PUT /api/v3/admin/forms/:formId/collaborators
2972
 * @precondition Must be preceded by request validation
2973
 * @security session
2974
 *
2975
 * @returns 200 with updated collaborators and permissions
2976
 * @returns 403 when current user does not havße permissions to update the collaborators
2977
 * @returns 404 when form cannot be found
2978
 * @returns 410 when updating collaborators for an archived form
2979
 * @returns 422 when user in session cannot be retrieved from the database
2980
 * @returns 500 when database error occurs
2981
 */
2982
export const _handleUpdateCollaborators: ControllerHandler<
8✔
2983
  { formId: string },
2984
  PermissionsUpdateDto | ErrorDto,
2985
  PermissionsUpdateDto
2986
> = (req, res) => {
8✔
2987
  const { formId } = req.params
12✔
2988
  const sessionUserId = (req.session as AuthedSessionData).user._id
12✔
2989
  // Step 1: Get the form after permission checks
2990
  return (
12✔
2991
    UserService.getPopulatedUserById(sessionUserId)
2992
      .andThen((user) =>
2993
        // Step 2: Retrieve form with write permission check.
2994
        AuthService.getFormAfterPermissionChecks({
8✔
2995
          user,
2996
          formId,
2997
          level: PermissionLevel.Write,
2998
        }),
2999
      )
3000
      // Step 2: Update the form collaborators
3001
      .andThen((form) =>
3002
        AdminFormService.updateFormCollaborators(form, req.body),
2✔
3003
      )
3004
      .map((updatedCollaborators) =>
3005
        res.status(StatusCodes.OK).json(updatedCollaborators),
2✔
3006
      )
3007
      .mapErr((error) => {
3008
        logger.error({
10✔
3009
          message: 'Error occurred when updating collaborators',
3010
          meta: {
3011
            action: '_handleUpdateCollaborators',
3012
            ...createReqMeta(req),
3013
            userId: sessionUserId,
3014
            formId,
3015
            formCollaborators: req.body,
3016
          },
3017
          error,
3018
        })
3019
        const { errorMessage, statusCode } = mapRouteError(error)
10✔
3020
        return res.status(statusCode).json({ message: errorMessage })
10✔
3021
      })
3022
  )
3023
}
3024

3025
/**
3026
 * Handler for PUT /api/v3/admin/forms/:formId/collaborators
3027
 */
3028
export const handleUpdateCollaborators = [
8✔
3029
  celebrate({
3030
    [Segments.BODY]: Joi.array().items(
3031
      Joi.object({
3032
        email: Joi.string()
3033
          .required()
3034
          .email()
3035
          .message('Please enter a valid email')
3036
          .lowercase(),
3037
        write: Joi.bool().optional(),
3038
        _id: Joi.string().optional(),
3039
      }),
3040
    ),
3041
  }),
3042
  _handleUpdateCollaborators,
3043
] as ControllerHandler[]
3044

3045
/**
3046
 * Handler for DELETE /api/v3/admin/forms/:formId/collaborators/self
3047
 * @precondition Must be preceded by request validation
3048
 * @security session
3049
 *
3050
 * @returns 200 with updated collaborators and permissions
3051
 * @returns 403 when current user does not have permissions to remove themselves from the collaborators list
3052
 * @returns 404 when form cannot be found
3053
 * @returns 410 when updating collaborators for an archived form
3054
 * @returns 422 when user in session cannot be retrieved from the database
3055
 * @returns 500 when database error occurs
3056
 */
3057
export const handleRemoveSelfFromCollaborators: ControllerHandler<
8✔
3058
  { formId: string },
3059
  PermissionsUpdateDto | ErrorDto
3060
> = (req, res) => {
8✔
3061
  const { formId } = req.params
7✔
3062
  const sessionUserId = (req.session as AuthedSessionData).user._id
7✔
3063
  let currentUserEmail = ''
7✔
3064
  // Step 1: Get the form after permission checks
3065
  return (
7✔
3066
    UserService.getPopulatedUserById(sessionUserId)
3067
      .andThen((user) => {
3068
        // Step 2: Retrieve form with read permission check, since we are only removing the user themselves
3069
        currentUserEmail = user.email
5✔
3070
        return AuthService.getFormAfterPermissionChecks({
5✔
3071
          user,
3072
          formId,
3073
          level: PermissionLevel.Read,
3074
        })
3075
      })
3076
      // Step 3: Update the form collaborators
3077
      .andThen((form) => {
3078
        const updatedPermissionList = form.permissionList.filter(
2✔
3079
          (user) => user.email.toLowerCase() !== currentUserEmail.toLowerCase(),
4✔
3080
        )
3081
        return AdminFormService.updateFormCollaborators(
2✔
3082
          form,
3083
          updatedPermissionList,
3084
        )
3085
      })
3086
      .map((updatedCollaborators) =>
3087
        res.status(StatusCodes.OK).json(updatedCollaborators),
2✔
3088
      )
3089
      .mapErr((error) => {
3090
        logger.error({
5✔
3091
          message: 'Error occurred when updating collaborators',
3092
          meta: {
3093
            action: 'handleRemoveSelfFromCollaborators',
3094
            ...createReqMeta(req),
3095
            userId: sessionUserId,
3096
            formId,
3097
            formCollaborators: req.body,
3098
          },
3099
          error,
3100
        })
3101
        const { errorMessage, statusCode } = mapRouteError(error)
5✔
3102
        return res.status(statusCode).json({ message: errorMessage })
5✔
3103
      })
3104
  )
3105
}
3106

3107
/**
3108
 * NOTE: Exported for testing.
3109
 * Private handler for PUT /forms/:formId/start-page
3110
 * @precondition Must be preceded by request validation
3111
 * @security session
3112
 *
3113
 * @returns 200 with updated start page
3114
 * @returns 403 when current user does not have permissions to update the start page
3115
 * @returns 404 when form cannot be found
3116
 * @returns 410 when updating the start page for an archived form
3117
 * @returns 422 when user in session cannot be retrieved from the database
3118
 * @returns 500 when database error occurs
3119
 */
3120
export const _handleUpdateStartPage: ControllerHandler<
8✔
3121
  { formId: string },
3122
  IFormDocument['startPage'] | ErrorDto,
3123
  StartPageUpdateDto
3124
> = (req, res) => {
8✔
3125
  const { formId } = req.params
16✔
3126
  const sessionUserId = (req.session as AuthedSessionData).user._id
16✔
3127

3128
  // Step 1: Retrieve currently logged in user.
3129
  return (
16✔
3130
    UserService.getPopulatedUserById(sessionUserId)
3131
      .andThen((user) =>
3132
        // Step 2: Retrieve form with write permission check.
3133
        AuthService.getFormAfterPermissionChecks({
13✔
3134
          user,
3135
          formId,
3136
          level: PermissionLevel.Write,
3137
        }),
3138
      )
3139
      // Step 3: User has permissions, proceed to allow updating of start page
3140
      .andThen(() => AdminFormService.updateStartPage(formId, req.body))
6✔
3141
      .map((updatedStartPage) =>
3142
        res.status(StatusCodes.OK).json(updatedStartPage),
2✔
3143
      )
3144
      .mapErr((error) => {
3145
        logger.error({
14✔
3146
          message: 'Error occurred when updating start page',
3147
          meta: {
3148
            action: '_handleUpdateStartPage',
3149
            ...createReqMeta(req),
3150
            userId: sessionUserId,
3151
            formId,
3152
            body: req.body,
3153
          },
3154
          error,
3155
        })
3156
        const { errorMessage, statusCode } = mapRouteError(error)
14✔
3157
        return res.status(statusCode).json({ message: errorMessage })
14✔
3158
      })
3159
  )
3160
}
3161

3162
/**
3163
 * Handler for PUT /forms/:formId/start-page
3164
 */
3165
export const handleUpdateStartPage = [
8✔
3166
  celebrate({
3167
    [Segments.BODY]: {
3168
      paragraph: Joi.string().allow('').optional(),
3169
      estTimeTaken: Joi.number().min(1).max(1000).required(),
3170
      colorTheme: Joi.string()
3171
        .valid(...Object.values(FormColorTheme))
3172
        .required(),
3173
      logo: Joi.object({
3174
        state: Joi.string().valid(...Object.values(FormLogoState)),
3175
        fileId: Joi.when('state', {
3176
          is: FormLogoState.Custom,
3177
          then: Joi.string().required(),
3178
          otherwise: Joi.any().forbidden(),
3179
        }),
3180
        fileName: Joi.when('state', {
3181
          is: FormLogoState.Custom,
3182
          then: Joi.string()
3183
            // Captures only the extensions below regardless of their case
3184
            // Refer to https://regex101.com/ with the below regex for a full explanation
3185
            .pattern(/\.(gif|png|jpeg|jpg|jfif)$/im)
3186
            .required(),
3187
          otherwise: Joi.any().forbidden(),
3188
        }),
3189
        fileSizeInBytes: Joi.when('state', {
3190
          is: FormLogoState.Custom,
3191
          then: Joi.number().max(MAX_UPLOAD_FILE_SIZE).required(),
3192
          otherwise: Joi.any().forbidden(),
3193
        }),
3194
      }).required(),
3195
      paragraphTranslations: Joi.array()
3196
        .items(
3197
          Joi.object({
3198
            language: Joi.string()
3199
              .valid(...Object.values(Language))
3200
              .required(),
3201
            translation: Joi.string().required(),
3202
          }),
3203
        )
3204
        .optional()
3205
        .default([]),
3206
    },
3207
  }),
3208
  _handleUpdateStartPage,
3209
] as ControllerHandler[]
3210

3211
export const handleGetGoLinkSuffix: ControllerHandler<{ formId: string }> = (
8✔
3212
  req,
3213
  res,
3214
) => {
3215
  const { formId } = req.params
×
3216
  const sessionUserId = (req.session as AuthedSessionData).user._id
×
3217

3218
  // Step 1: Get the form after permission checks
3219
  return (
×
3220
    UserService.getPopulatedUserById(sessionUserId)
3221
      .andThen((user) => {
3222
        return AuthService.getFormAfterPermissionChecks({
×
3223
          user,
3224
          formId,
3225
          level: PermissionLevel.Read,
3226
        })
3227
      })
3228
      // Step 2: After permission checks, get the GoGov link suffix
3229
      .andThen(() => {
3230
        return AdminFormService.getGoLinkSuffix(formId)
×
3231
      })
3232
      .map((goLinkSuffix) => res.status(StatusCodes.OK).json(goLinkSuffix))
×
3233
      .mapErr((error) => {
3234
        const { errorMessage, statusCode } = mapRouteError(error)
×
3235
        // Don't log 404 errors as they are expected for most forms
3236
        if (statusCode !== StatusCodes.NOT_FOUND) {
×
3237
          logger.error({
×
3238
            message: 'Error occurred when getting GoGov link suffix',
3239
            meta: {
3240
              action: 'handleGetGoLinkSuffix',
3241
              ...createReqMeta(req),
3242
              userId: sessionUserId,
3243
              formId,
3244
            },
3245
            error,
3246
          })
3247
        }
3248
        return res.status(statusCode).json({ message: errorMessage })
×
3249
      })
3250
  )
3251
}
3252

3253
export const handleSetGoLinkSuffix: ControllerHandler<
8✔
3254
  { formId: string },
3255
  unknown,
3256
  { linkSuffix: string; adminEmail: string }
3257
> = (req, res) => {
8✔
3258
  const goGovBaseUrl = goGovConfig.goGovBaseUrl
×
3259
  const { formId } = req.params
×
3260
  const { linkSuffix, adminEmail } = req.body
×
3261
  const sessionUserId = (req.session as AuthedSessionData).user._id
×
3262

3263
  // Step 1: Get the form after permission checks
3264
  return (
×
3265
    UserService.getPopulatedUserById(sessionUserId)
3266
      .andThen((user) => {
3267
        return AuthService.getFormAfterPermissionChecks({
×
3268
          user,
3269
          formId,
3270
          level: PermissionLevel.Write,
3271
        })
3272
      })
3273
      // Step 2: After permission checks, try to get GoGov link
3274
      .andThen(() => {
3275
        return ResultAsync.fromPromise(
×
3276
          axios.post(
3277
            `${goGovBaseUrl}/api/v1/admin/urls`,
3278
            {
3279
              longUrl: `${process.env.APP_URL}/${formId}`,
3280
              shortUrl: linkSuffix,
3281
              email: adminEmail,
3282
            },
3283
            {
3284
              headers: {
3285
                Authorization: `Bearer ${goGovConfig.goGovAPIKey}`,
3286
                // Required due to bug introduced in axios 1.2.1: https://github.com/axios/axios/issues/5346
3287
                // TODO: remove when axios is upgraded to 1.2.2
3288
                'Accept-Encoding': 'gzip,deflate,compress',
3289
              },
3290
            },
3291
          ),
3292
          (error) => {
3293
            if (axios.isAxiosError(error)) {
×
3294
              return mapGoGovErrors(error)
×
3295
            }
3296

3297
            return new GoGovServerError()
×
3298
          },
3299
        )
3300
      })
3301
      // Step 3: After obtaining GoGov link, save it to the form
3302
      .andThen(() => AdminFormService.setGoLinkSuffix(formId, linkSuffix))
×
3303
      .map(() => res.sendStatus(StatusCodes.OK))
×
3304
      .mapErr((error) => {
3305
        logger.error({
×
3306
          message: 'Error occurred when setting GoGov link suffix',
3307
          meta: {
3308
            action: 'handleSetGoLinkSuffix',
3309
            ...createReqMeta(req),
3310
            userId: sessionUserId,
3311
            formId,
3312
          },
3313
          error,
3314
        })
3315
        const { errorMessage, statusCode } = mapRouteError(error)
×
3316
        return res.status(statusCode).json({ message: errorMessage })
×
3317
      })
3318
  )
3319
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc