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

opengovsg / FormSG / 12883141290

21 Jan 2025 08:56AM UTC coverage: 72.463% (-0.002%) from 72.465%
12883141290

push

github

g-tejas
feat: move banner to growthbook

2729 of 4594 branches covered (59.4%)

Branch coverage included in aggregate %.

9968 of 12928 relevant lines covered (77.1%)

46.62 hits per line

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

83.93
/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 { AuthedSessionData } from 'express-session'
6
import { StatusCodes } from 'http-status-codes'
8✔
7
import JSONStream from 'JSONStream'
8✔
8
import { ResultAsync } from 'neverthrow'
8✔
9

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

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

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

118
const logger = createLoggerWithLabel(module)
8✔
119

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

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

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

228
const fileUploadValidator = celebrate({
8✔
229
  [Segments.BODY]: {
230
    fileId: Joi.string().required(),
231
    fileMd5Hash: Joi.string().base64().required(),
232
    fileType: Joi.string()
233
      .valid(...VALID_UPLOAD_FILE_TYPES)
234
      .required(),
235
    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.
236
  },
237
})
238

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1659
const LIMIT_IN_KB = 250
8✔
1660
const STRING_MAX_LENGTH = LIMIT_IN_KB * KB
8✔
1661
const _handleUpdateWhitelistSettingValidator = celebrate({
8✔
1662
  [Segments.PARAMS]: Joi.object({
1663
    formId: Joi.string()
1664
      .required()
1665
      .pattern(/^[a-fA-F0-9]{24}$/)
1666
      .message('Your form ID is invalid.'),
1667
  }),
1668
  [Segments.BODY]: Joi.object({
1669
    whitelistCsvString: Joi.string()
1670
      .allow(null) // for removal of whitelist
1671
      .max(STRING_MAX_LENGTH)
1672
      .pattern(/^[a-zA-Z0-9,\r\n]+$/)
1673
      .messages({
1674
        'string.empty': 'Your csv is empty.',
1675
        'string.pattern.base': 'Your csv has one or more invalid characters.',
1676
        'string.max': `You have exceeded the file size limit, please upload a file below ${LIMIT_IN_KB} kB.`,
1677
      }),
1678
  }),
1679
})
1680

1681
const _parseWhitelistCsvString = (whitelistCsvString: string | null) => {
8✔
1682
  if (!whitelistCsvString) {
3✔
1683
    return null
1✔
1684
  }
1685
  return whitelistCsvString.split('\r\n').map((entry: string) => entry.trim())
6✔
1686
}
1687

1688
const _handleUpdateWhitelistSetting: ControllerHandler<
1689
  { formId: string },
1690
  object,
1691
  { whitelistCsvString: string | null }
1692
> = async (req, res) => {
8✔
1693
  const { formId } = req.params
4✔
1694
  const sessionUserId = (req.session as AuthedSessionData).user._id
4✔
1695

1696
  const logMeta = {
4✔
1697
    action: '_handleUpdateWhitelistSetting',
1698
    ...createReqMeta(req),
1699
    userId: sessionUserId,
1700
    formId,
1701
  }
1702

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

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

1725
  const form = formResult.value
3✔
1726

1727
  const { whitelistCsvString } = req.body
3✔
1728
  const whitelistedSubmitterIds = _parseWhitelistCsvString(whitelistCsvString)
3✔
1729

1730
  const upperCaseWhitelistedSubmitterIds =
1731
    whitelistedSubmitterIds && whitelistedSubmitterIds.length > 0
3✔
1732
      ? whitelistedSubmitterIds.map((id) => id.toUpperCase())
6✔
1733
      : null
1734

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

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

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

1787
export const _handleUpdateWhitelistSettingForTest =
8✔
1788
  _handleUpdateWhitelistSetting
1789

1790
export const handleUpdateWhitelistSetting = [
8✔
1791
  _handleUpdateWhitelistSettingValidator,
1792
  _handleUpdateWhitelistSetting,
1793
] as ControllerHandler[]
1794

1795
/**
1796
 * NOTE: Exported for testing.
1797
 * Private handler for PUT /forms/:formId/fields/:fieldId
1798
 * @precondition Must be preceded by request validation
1799
 */
1800
export const _handleUpdateFormField: ControllerHandler<
8✔
1801
  {
1802
    formId: string
1803
    fieldId: string
1804
  },
1805
  FormFieldDto | ErrorDto,
1806
  FieldUpdateDto
1807
> = (req, res) => {
8✔
1808
  const { formId, fieldId } = req.params
11✔
1809
  const updatedFormField = req.body
11✔
1810
  const sessionUserId = (req.session as AuthedSessionData).user._id
11✔
1811

1812
  // Step 1: Retrieve currently logged in user.
1813
  return (
11✔
1814
    UserService.getPopulatedUserById(sessionUserId)
1815
      .andThen((user) =>
1816
        // Step 2: Retrieve form with write permission check.
1817
        AuthService.getFormAfterPermissionChecks({
10✔
1818
          user,
1819
          formId,
1820
          level: PermissionLevel.Write,
1821
        }),
1822
      )
1823
      // Step 3: User has permissions, update form field of retrieved form.
1824
      .andThen((form) =>
1825
        AdminFormService.updateFormField(form, fieldId, updatedFormField),
7✔
1826
      )
1827
      .map((updatedFormField) =>
1828
        res.status(StatusCodes.OK).json(updatedFormField as FormFieldDto),
3✔
1829
      )
1830
      .mapErr((error) => {
1831
        logger.error({
8✔
1832
          message: 'Error occurred when updating form field',
1833
          meta: {
1834
            action: 'handleUpdateFormField',
1835
            ...createReqMeta(req),
1836
            userId: sessionUserId,
1837
            formId,
1838
            fieldId,
1839
            updateFieldBody: updatedFormField,
1840
          },
1841
          error,
1842
        })
1843
        const { errorMessage, statusCode } = mapRouteError(error)
8✔
1844
        return res.status(statusCode).json({ message: errorMessage })
8✔
1845
      })
1846
  )
1847
}
1848

1849
/**
1850
 * Handler for GET /form/:formId/settings.
1851
 * @security session
1852
 *
1853
 * @returns 200 with latest form settings on successful update
1854
 * @returns 403 when current user does not have permissions to obtain form settings
1855
 * @returns 404 when form to retrieve settings for cannot be found
1856
 * @returns 409 when saving form settings incurs a conflict in the database
1857
 * @returns 500 when database error occurs
1858
 */
1859
export const handleGetSettings: ControllerHandler<
8✔
1860
  { formId: string },
1861
  FormSettings | ErrorDto
1862
> = (req, res) => {
8✔
1863
  const { formId } = req.params
6✔
1864
  const sessionUserId = (req.session as AuthedSessionData).user._id
6✔
1865

1866
  return UserService.getPopulatedUserById(sessionUserId)
6✔
1867
    .andThen((user) =>
1868
      // Retrieve form for settings as well as for permissions checking
1869
      FormService.retrieveFullFormById(formId).map((form) => ({
6✔
1870
        form,
1871
        user,
1872
      })),
1873
    )
1874
    .andThen(AuthService.checkFormForPermissions(PermissionLevel.Read))
1875
    .map((form) => res.status(StatusCodes.OK).json(form.getSettings()))
1✔
1876
    .mapErr((error) => {
1877
      logger.error({
5✔
1878
        message: 'Error occurred when retrieving form settings',
1879
        meta: {
1880
          action: 'handleGetSettings',
1881
          ...createReqMeta(req),
1882
          userId: sessionUserId,
1883
          formId,
1884
        },
1885
        error,
1886
      })
1887
      const { errorMessage, statusCode } = mapRouteError(error)
5✔
1888
      return res.status(statusCode).json({ message: errorMessage })
5✔
1889
    })
1890
}
1891

1892
export const handleGetWhitelistSetting: ControllerHandler<
8✔
1893
  {
1894
    formId: string
1895
  },
1896
  | {
1897
      encryptedWhitelistedSubmitterIds: EncryptedStringsMessageContent | null
1898
    }
1899
  | ErrorDto
1900
> = (req, res) => {
8✔
1901
  const { formId } = req.params
2✔
1902
  const sessionUserId = (req.session as AuthedSessionData).user._id
2✔
1903

1904
  return UserService.getPopulatedUserById(sessionUserId)
2✔
1905
    .andThen((user) =>
1906
      // Retrieve form for settings as well as for permissions checking
1907
      FormService.retrieveFullFormById(formId).map((form) => ({
2✔
1908
        form,
1909
        user,
1910
      })),
1911
    )
1912
    .andThen(AuthService.checkFormForPermissions(PermissionLevel.Read))
1913
    .andThen((form) => EncryptSubmissionService.checkFormIsEncryptMode(form))
1✔
1914
    .map(async (form) => AdminFormService.getFormWhitelistSetting(form))
1✔
1915
    .andThen((formWhitelistedSubmitterIds) => formWhitelistedSubmitterIds)
1✔
1916
    .map((formWhitelistedSubmitterIds) => {
1917
      return res.status(StatusCodes.OK).json({
1✔
1918
        encryptedWhitelistedSubmitterIds: formWhitelistedSubmitterIds,
1919
      })
1920
    })
1921
    .mapErr((error: Error) => {
1922
      logger.error({
1✔
1923
        message: 'Error occurred when retrieving form whitelist settings',
1924
        meta: {
1925
          action: 'handleGetWhitelistSetting',
1926
          ...createReqMeta(req),
1927
          userId: sessionUserId,
1928
          formId,
1929
        },
1930
        error,
1931
      })
1932
      const { errorMessage, statusCode } = mapRouteError(error)
1✔
1933
      return res.status(statusCode).json({ message: errorMessage })
1✔
1934
    })
1935
}
1936

1937
/**
1938
 * Handler for POST api/public/v1/admin/forms/:formId/webhooksettings.
1939
 *
1940
 * @returns 200 with latest webhook and response mode settings
1941
 * @returns 401 when current user is not logged in
1942
 * @returns 403 when current user does not have permissions to obtain form settings
1943
 * @returns 404 when form to retrieve settings for cannot be found
1944
 * @returns 422 when user from user email cannot be retrieved from the database
1945
 * @returns 500 when database error occurs
1946
 */
1947
export const _handleGetWebhookSettings: ControllerHandler<
8✔
1948
  { formId: string },
1949
  FormWebhookResponseModeSettings | ErrorDto,
1950
  { userEmail: string }
1951
> = (req, res) => {
8✔
1952
  const { formId } = req.params
×
1953
  const { userEmail } = req.body
×
1954
  const authedUserId = (req.session as AuthedSessionData).user._id
×
1955

1956
  const logMeta = {
×
1957
    action: 'handleGetWebhookSettings',
1958
    ...createReqMeta(req),
1959
    userEmail,
1960
    formId,
1961
  }
1962

1963
  logger.info({
×
1964
    message: 'User attempting to get webhook settings',
1965
    meta: logMeta,
1966
  })
1967

1968
  return UserService.findUserById(authedUserId)
×
1969
    .mapErr((error) => {
1970
      logger.error({
×
1971
        message: 'Error occurred when retrieving user from database',
1972
        meta: logMeta,
1973
        error,
1974
      })
1975
      return error
×
1976
    })
1977
    .andThen((user) =>
1978
      // Retrieve form for settings as well as for permissions checking
1979
      FormService.retrieveFullFormById(formId).map((form) => ({
×
1980
        form,
1981
        user,
1982
      })),
1983
    )
1984
    .andThen(AuthService.checkFormForPermissions(PermissionLevel.Read))
1985
    .map((form) =>
1986
      res.status(StatusCodes.OK).json(form.getWebhookAndResponseModeSettings()),
×
1987
    )
1988
    .mapErr((error) => {
1989
      logger.error({
×
1990
        message: 'Error occurred when retrieving form settings',
1991
        meta: logMeta,
1992
        error,
1993
      })
1994
      const { errorMessage, statusCode } = mapRouteError(error)
×
1995
      return res.status(statusCode).json({ message: errorMessage })
×
1996
    })
1997
}
1998

1999
/**
2000
 * Handler for POST api/public/v1/admin/forms/:formId/webhooksettings.
2001
 * @security session
2002
 *
2003
 * @returns 200 with latest webhook and response mode settings
2004
 * @returns 400 when body is malformed
2005
 * @returns 401 when current user is not logged in
2006
 * @returns 403 when user email does not have permissions to obtain form settings
2007
 * @returns 404 when form to retrieve settings for cannot be found
2008
 * @returns 422 when user from user email cannot be retrieved from the database
2009
 * @returns 500 when database error occurs
2010
 */
2011
export const handleGetWebhookSettings = [
8✔
2012
  getWebhookSettingsValidator,
2013
  _handleGetWebhookSettings,
2014
] as ControllerHandler[]
2015

2016
/**
2017
 * Handler for POST /v2/submissions/encrypt/preview/:formId.
2018
 * @security session
2019
 *
2020
 * @returns 200 with a mock submission ID
2021
 * @returns 400 when body is malformed; e.g. invalid plaintext responses or encoding for encrypted content
2022
 * @returns 403 when current user does not have read permissions to given form
2023
 * @returns 404 when given form ID does not exist
2024
 * @returns 410 when given form has been deleted
2025
 * @returns 422 when user ID in session is not found in database
2026
 * @returns 500 when database error occurs
2027
 */
2028
export const submitEncryptPreview: ControllerHandler<
8✔
2029
  { formId: string },
2030
  { message: string; submissionId: string } | ErrorDto,
2031
  EncryptSubmissionDto
2032
> = async (req, res) => {
10✔
2033
  const { formId } = req.params
10✔
2034
  const sessionUserId = (req.session as AuthedSessionData).user._id
10✔
2035
  // No need to process attachments as we don't do anything with them
2036
  const logMeta = {
10✔
2037
    action: 'submitEncryptPreview',
2038
    formId,
2039
  }
2040

2041
  // eslint-disable-next-line typesafe/no-await-without-trycatch
2042
  return UserService.getPopulatedUserById(sessionUserId)
10✔
2043
    .andThen((user) =>
2044
      // Step 2: Retrieve form with write permission check.
2045
      AuthService.getFormAfterPermissionChecks({
8✔
2046
        user,
2047
        formId,
2048
        level: PermissionLevel.Read,
2049
      }),
2050
    )
2051
    .andThen((form) =>
2052
      EncryptSubmissionService.checkFormIsEncryptMode(form).mapErr((error) => {
4✔
2053
        logger.error({
1✔
2054
          message: 'Error while retrieving form for preview submission',
2055
          meta: logMeta,
2056
          error,
2057
        })
2058
        return error
1✔
2059
      }),
2060
    )
2061
    .map(() => {
2062
      const fakeSubmissionId = new ObjectId().toString()
3✔
2063
      // Return the reply early to the submitter
2064
      return res.json({
3✔
2065
        message: 'Form submission successful.',
2066
        submissionId: fakeSubmissionId,
2067
      })
2068
    })
2069
    .mapErr((error) => {
2070
      const { errorMessage, statusCode } = mapSubmissionError(error)
7✔
2071
      return res.status(statusCode).json({ message: errorMessage })
7✔
2072
    })
2073
}
2074

2075
export const handleEncryptPreviewSubmission = [
8✔
2076
  submitEncryptPreview,
2077
] as ControllerHandler[]
2078

2079
/**
2080
 * Handler for POST /v2/submissions/email/preview/:formId.
2081
 * @security session
2082
 *
2083
 * @returns 200 with a mock submission ID
2084
 * @returns 400 when body is malformed; e.g. invalid responses, or when admin email fails to be sent
2085
 * @returns 403 when current user does not have read permissions to given form
2086
 * @returns 404 when given form ID does not exist
2087
 * @returns 410 when given form has been deleted
2088
 * @returns 422 when user ID in session is not found in database
2089
 * @returns 500 when database error occurs
2090
 */
2091
export const submitEmailPreview: ControllerHandler<
8✔
2092
  { formId: string },
2093
  { message: string; submissionId?: string },
2094
  ParsedEmailModeSubmissionBody,
2095
  { captchaResponse?: unknown }
2096
> = async (req, res) => {
22✔
2097
  const { formId } = req.params
22✔
2098
  const sessionUserId = (req.session as AuthedSessionData).user._id
22✔
2099
  // No need to process attachments as we don't do anything with them
2100
  const { responses } = req.body
22✔
2101
  const logMeta = {
22✔
2102
    action: 'submitEmailPreview',
2103
    formId,
2104
    ...createReqMeta(req),
2105
  }
2106

2107
  const formResult = await UserService.getPopulatedUserById(sessionUserId)
22✔
2108
    .andThen((user) =>
2109
      AuthService.getFormAfterPermissionChecks({
20✔
2110
        user,
2111
        formId,
2112
        level: PermissionLevel.Read,
2113
      }),
2114
    )
2115
    .andThen(EmailSubmissionService.checkFormIsEmailMode)
2116
  if (formResult.isErr()) {
22✔
2117
    logger.error({
7✔
2118
      message: 'Error while retrieving form for preview submission',
2119
      meta: logMeta,
2120
      error: formResult.error,
2121
    })
2122
    const { errorMessage, statusCode } = mapEmailSubmissionError(
7✔
2123
      formResult.error,
2124
    )
2125
    return res.status(statusCode).json({ message: errorMessage })
7✔
2126
  }
2127
  const form = formResult.value
15✔
2128

2129
  const parsedResponsesResult = await SubmissionService.validateAttachments(
15✔
2130
    responses,
2131
    form.responseMode,
2132
  ).andThen(() => ParsedResponsesObject.parseResponses(form, responses))
13✔
2133
  if (parsedResponsesResult.isErr()) {
15✔
2134
    logger.error({
5✔
2135
      message: 'Error while parsing responses for preview submission',
2136
      meta: logMeta,
2137
      error: parsedResponsesResult.error,
2138
    })
2139
    const { errorMessage, statusCode } = mapEmailSubmissionError(
5✔
2140
      parsedResponsesResult.error,
2141
    )
2142
    return res.status(statusCode).json({ message: errorMessage })
5✔
2143
  }
2144
  const parsedResponses = parsedResponsesResult.value
10✔
2145
  const attachments = mapAttachmentsFromResponses(req.body.responses)
10✔
2146

2147
  // Handle SingPass, CorpPass and MyInfo authentication and validation
2148
  const { authType } = form
10✔
2149
  if (authType === FormAuthType.SP || authType === FormAuthType.MyInfo) {
10!
2150
    parsedResponses.addNdiResponses({
×
2151
      authType,
2152
      uinFin: PREVIEW_SINGPASS_UINFIN,
2153
    })
2154
  } else if (authType === FormAuthType.CP) {
10!
2155
    parsedResponses.addNdiResponses({
×
2156
      authType,
2157
      uinFin: PREVIEW_CORPPASS_UINFIN,
2158
      userInfo: PREVIEW_CORPPASS_UID,
2159
    })
2160
  }
2161

2162
  const emailData = new SubmissionEmailObj(
10✔
2163
    parsedResponses.getAllResponses(),
2164
    // All MyInfo fields are verified in preview
2165
    new Set(AdminFormService.extractMyInfoFieldIds(form.form_fields)),
2166
    form.authType,
2167
  )
2168
  const submission = EmailSubmissionService.createEmailSubmissionWithoutSave(
10✔
2169
    form,
2170
    // Don't need to care about response hash or salt
2171
    '',
2172
    '',
2173
  )
2174

2175
  const sendAdminEmailResult = await MailService.sendSubmissionToAdmin({
10✔
2176
    replyToEmails: EmailSubmissionService.extractEmailAnswers(
2177
      parsedResponses.getAllResponses(),
2178
    ),
2179
    form,
2180
    submission,
2181
    attachments,
2182
    dataCollationData: emailData.dataCollationData,
2183
    formData: emailData.formData,
2184
  })
2185
  if (sendAdminEmailResult.isErr()) {
10✔
2186
    logger.error({
2✔
2187
      message: 'Error sending submission to admin',
2188
      meta: logMeta,
2189
      error: sendAdminEmailResult.error,
2190
    })
2191
    const { statusCode, errorMessage } = mapEmailSubmissionError(
2✔
2192
      sendAdminEmailResult.error,
2193
    )
2194
    return res.status(statusCode).json({
2✔
2195
      message: errorMessage,
2196
    })
2197
  }
2198

2199
  // Don't await on email confirmations, so submission is successful even if
2200
  // this fails
2201
  void SubmissionService.sendEmailConfirmations({
8✔
2202
    form,
2203
    submission,
2204
    attachments,
2205
    responsesData: emailData.autoReplyData,
2206
    recipientData: extractEmailConfirmationData(
2207
      parsedResponses.getAllResponses(),
2208
      form.form_fields,
2209
    ),
2210
  }).mapErr((error) => {
2211
    logger.error({
1✔
2212
      message: 'Error while sending email confirmations',
2213
      meta: logMeta,
2214
      error,
2215
    })
2216
  })
2217

2218
  return res.json({
8✔
2219
    message: 'Form submission successful.',
2220
    submissionId: submission.id,
2221
  })
2222
}
2223

2224
export const handleEmailPreviewSubmission = [
8✔
2225
  ReceiverMiddleware.receiveEmailSubmission,
2226
  EmailSubmissionMiddleware.validateResponseParams,
2227
  submitEmailPreview,
2228
] as ControllerHandler[]
2229

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

2272
const _handleUpdateOptionsToRecipientsMap: ControllerHandler<
2273
  {
2274
    formId: string
2275
    fieldId: string
2276
  },
2277
  FormFieldDto | ErrorDto,
2278
  { optionsToRecipientsMap: Record<string, string[]> }
2279
> = (req, res) => {
8✔
2280
  const { formId, fieldId } = req.params
×
2281
  const { optionsToRecipientsMap } = req.body
×
2282
  const sessionUserId = (req.session as AuthedSessionData).user._id
×
2283

2284
  // Step 1: Retrieve currently logged in user.
2285
  return UserService.getPopulatedUserById(sessionUserId)
×
2286
    .andThen((user) =>
2287
      // Step 2: Retrieve form with write permission check.
2288
      AuthService.getFormAfterPermissionChecks({
×
2289
        user,
2290
        formId,
2291
        level: PermissionLevel.Write,
2292
      }),
2293
    )
2294
    .andThen((form) => {
2295
      return AdminFormService.updateOptionsToRecipientsMap(
×
2296
        form,
2297
        fieldId,
2298
        optionsToRecipientsMap,
2299
      )
2300
    })
2301
    .map((updatedFormField) =>
2302
      res.status(StatusCodes.OK).json(updatedFormField as FormFieldDto),
×
2303
    )
2304
    .mapErr((error) => {
2305
      logger.error({
×
2306
        message: 'Error occurred when updating options to recipients map',
2307
        meta: {
2308
          action: '_handleUpdateOptionsToRecipientsMap',
2309
          ...createReqMeta(req),
2310
          userId: sessionUserId,
2311
          formId,
2312
          fieldId,
2313
          optionsToRecipientsMap,
2314
        },
2315
        error,
2316
      })
2317
      const { errorMessage, statusCode } = mapRouteError(error)
×
2318
      return res.status(statusCode).json({ message: errorMessage })
×
2319
    })
2320
}
2321

2322
export const handleUpdateOptionsToRecipientsMap = [
8✔
2323
  celebrate({
2324
    [Segments.BODY]: Joi.object({
2325
      optionsToRecipientsMap: Joi.object(),
2326
    }),
2327
    [Segments.PARAMS]: Joi.object({
2328
      formId: Joi.string().required(),
2329
      fieldId: Joi.string().required(),
2330
    }),
2331
  }),
2332
  _handleUpdateOptionsToRecipientsMap,
2333
] as ControllerHandler[]
2334

2335
/**
2336
 * NOTE: Exported for testing.
2337
 * Private handler for POST /forms/:formId/fields
2338
 * @precondition Must be preceded by request validation
2339
 * @security session
2340
 *
2341
 * @returns 200 with created form field
2342
 * @returns 403 when current user does not have permissions to create a form field
2343
 * @returns 404 when form cannot be found
2344
 * @returns 409 when form field update conflicts with database state
2345
 * @returns 410 when creating form field for an archived form
2346
 * @returns 413 when creating form field causes form to be too large to be saved in the database
2347
 * @returns 422 when an invalid form field creation is attempted on the form
2348
 * @returns 422 when user in session cannot be retrieved from the database
2349
 * @returns 500 when database error occurs
2350
 */
2351
export const _handleCreateFormField: ControllerHandler<
8✔
2352
  { formId: string },
2353
  FormFieldDto | ErrorDto,
2354
  FieldCreateDto,
2355
  { to?: number }
2356
> = (req, res) => {
8✔
2357
  const { formId } = req.params
19✔
2358
  const { to } = req.query
19✔
2359
  const formFieldToCreate = req.body
19✔
2360
  const sessionUserId = (req.session as AuthedSessionData).user._id
19✔
2361

2362
  // Step 1: Retrieve currently logged in user.
2363
  return (
19✔
2364
    UserService.getPopulatedUserById(sessionUserId)
2365
      .andThen((user) =>
2366
        // Step 2: Retrieve form with write permission check.
2367
        AuthService.getFormAfterPermissionChecks({
17✔
2368
          user,
2369
          formId,
2370
          level: PermissionLevel.Write,
2371
        }),
2372
      )
2373
      // Step 3: User has permissions, proceed to create form field with provided body.
2374
      .andThen((form) =>
2375
        AdminFormService.createFormField(form, formFieldToCreate, to),
13✔
2376
      )
2377
      .map((createdFormField) =>
2378
        res.status(StatusCodes.OK).json(createdFormField as FormFieldDto),
10✔
2379
      )
2380
      .mapErr((error) => {
2381
        logger.error({
9✔
2382
          message: 'Error occurred when creating form field',
2383
          meta: {
2384
            action: '_handleCreateFormField',
2385
            ...createReqMeta(req),
2386
            userId: sessionUserId,
2387
            formId,
2388
            createFieldBody: formFieldToCreate,
2389
          },
2390
          error,
2391
        })
2392
        const { errorMessage, statusCode } = mapRouteError(error)
9✔
2393
        return res.status(statusCode).json({ message: errorMessage })
9✔
2394
      })
2395
  )
2396
}
2397

2398
/**
2399
 * NOTE: Exported for testing.
2400
 * Private handler for POST /forms/:formId/logic
2401
 * @precondition Must be preceded by request validation
2402
 * @security session
2403
 *
2404
 * @returns 200 with created logic object when successfully created
2405
 * @returns 403 when user does not have permissions to create logic
2406
 * @returns 404 when form cannot be found
2407
 * @returns 422 when user in session cannot be retrieved from the database
2408
 * @returns 500 when database error occurs
2409
 */
2410
export const _handleCreateLogic: ControllerHandler<
8✔
2411
  { formId: string },
2412
  LogicDto | ErrorDto,
2413
  LogicDto
2414
> = (req, res) => {
8✔
2415
  const { formId } = req.params
5✔
2416
  const createLogicBody = req.body
5✔
2417
  const sessionUserId = (req.session as AuthedSessionData).user._id
5✔
2418

2419
  // Step 1: Retrieve currently logged in user.
2420
  return (
5✔
2421
    UserService.getPopulatedUserById(sessionUserId)
2422
      .andThen((user) =>
2423
        // Step 2: Retrieve form with write permission check.
2424
        AuthService.getFormAfterPermissionChecks({
3✔
2425
          user,
2426
          formId,
2427
          level: PermissionLevel.Write,
2428
        }),
2429
      )
2430
      // Step 3: Create form logic
2431
      .andThen((retrievedForm) =>
2432
        AdminFormService.createFormLogic(retrievedForm, createLogicBody),
1✔
2433
      )
2434
      .map((createdLogic) =>
2435
        res.status(StatusCodes.OK).json(createdLogic as LogicDto),
1✔
2436
      )
2437
      .mapErr((error) => {
2438
        logger.error({
4✔
2439
          message: 'Error occurred when creating form logic',
2440
          meta: {
2441
            action: 'handleCreateLogic',
2442
            ...createReqMeta(req),
2443
            userId: sessionUserId,
2444
            formId,
2445
            createLogicBody,
2446
          },
2447
          error,
2448
        })
2449
        const { errorMessage, statusCode } = mapRouteError(error)
4✔
2450
        return res.status(statusCode).json({ message: errorMessage })
4✔
2451
      })
2452
  )
2453
}
2454

2455
/**
2456
 * Shape of request body used for joi validation for create and update logic
2457
 */
2458
const joiLogicBody = {
8✔
2459
  logicType: Joi.string()
2460
    .valid(...Object.values(LogicType))
2461
    .required(),
2462
  conditions: Joi.array()
2463
    .items(
2464
      Joi.object({
2465
        field: Joi.string().required(),
2466
        state: Joi.string()
2467
          .valid(...Object.values(LogicConditionState))
2468
          .required(),
2469
        value: Joi.alternatives()
2470
          .try(
2471
            Joi.number(),
2472
            Joi.string(),
2473
            Joi.array().items(Joi.string()),
2474
            Joi.array().items(Joi.number()),
2475
          )
2476
          .required(),
2477
        ifValueType: Joi.string()
2478
          .valid(...Object.values(LogicIfValue))
2479
          .required(),
2480
      }).unknown(true),
2481
    )
2482
    .required(),
2483
  show: Joi.alternatives().conditional('logicType', {
2484
    is: LogicType.ShowFields,
2485
    then: Joi.array().items(Joi.string()).required(),
2486
  }),
2487
  preventSubmitMessage: Joi.alternatives().conditional('logicType', {
2488
    is: LogicType.PreventSubmit,
2489
    then: Joi.string().required(),
2490
  }),
2491
  preventSubmitMessageTranslations: Joi.alternatives().conditional(
2492
    'logicType',
2493
    {
2494
      is: LogicType.PreventSubmit,
2495
      then: Joi.array()
2496
        .items(
2497
          Joi.object({
2498
            language: Joi.string()
2499
              .valid(...Object.values(Language))
2500
              .required(),
2501
            translation: Joi.string().required(),
2502
          }),
2503
        )
2504
        .optional()
2505
        .default([]),
2506
    },
2507
  ),
2508
}
2509

2510
/**
2511
 * Handler for POST /forms/:formId/logic
2512
 */
2513
export const handleCreateLogic = [
8✔
2514
  celebrate({
2515
    [Segments.BODY]: joiLogicBody,
2516
  }),
2517
  _handleCreateLogic,
2518
] as ControllerHandler[]
2519

2520
/**
2521
 * Handler for DELETE /forms/:formId/logic/:logicId
2522
 * @security session
2523
 *
2524
 * @returns 200 with success message when successfully deleted
2525
 * @returns 403 when user does not have permissions to delete logic
2526
 * @returns 404 when form cannot be found
2527
 * @returns 422 when user in session cannot be retrieved from the database
2528
 * @returns 500 when database error occurs
2529
 */
2530
export const handleDeleteLogic: ControllerHandler<{
8✔
2531
  formId: string
2532
  logicId: string
2533
}> = (req, res) => {
8✔
2534
  const { formId, logicId } = req.params
12✔
2535
  const sessionUserId = (req.session as AuthedSessionData).user._id
12✔
2536

2537
  // Step 1: Retrieve currently logged in user.
2538
  return (
12✔
2539
    UserService.getPopulatedUserById(sessionUserId)
2540
      .andThen((user) =>
2541
        // Step 2: Retrieve form with write permission check.
2542
        AuthService.getFormAfterPermissionChecks({
9✔
2543
          user,
2544
          formId,
2545
          level: PermissionLevel.Write,
2546
        }),
2547
      )
2548

2549
      // Step 3: Delete form logic
2550
      .andThen((retrievedForm) =>
2551
        AdminFormService.deleteFormLogic(retrievedForm, logicId),
5✔
2552
      )
2553
      .map(() => res.sendStatus(StatusCodes.OK))
3✔
2554
      .mapErr((error) => {
2555
        logger.error({
9✔
2556
          message: 'Error occurred when deleting form logic',
2557
          meta: {
2558
            action: 'handleDeleteLogic',
2559
            ...createReqMeta(req),
2560
            userId: sessionUserId,
2561
            formId,
2562
            logicId,
2563
          },
2564
          error,
2565
        })
2566
        const { errorMessage, statusCode } = mapRouteError(error)
9✔
2567
        return res.status(statusCode).json({ message: errorMessage })
9✔
2568
      })
2569
  )
2570
}
2571

2572
/**
2573
 * Handler for POST /forms/:formId/fields
2574
 */
2575
export const handleCreateFormField = [
8✔
2576
  celebrate({
2577
    [Segments.BODY]: Joi.object({
2578
      // Ensures id is not provided.
2579
      _id: Joi.any().forbidden(),
2580
      globalId: Joi.any().forbidden(),
2581
      fieldType: Joi.string()
2582
        .valid(...Object.values(BasicField))
2583
        .required(),
2584
      title: Joi.string().trim().required(),
2585
      description: Joi.string().allow(''),
2586
      required: Joi.boolean(),
2587
      disabled: Joi.boolean(),
2588
      // Allow other field related key-values to be provided and let the model
2589
      // layer handle the validation.
2590
    })
2591
      .unknown(true)
2592
      .custom((value, helpers) => verifyValidUnicodeString(value, helpers)),
9✔
2593
    [Segments.QUERY]: {
2594
      // Optional index to insert the field at.
2595
      to: Joi.number().min(0),
2596
    },
2597
  }),
2598
  _handleCreateFormField,
2599
]
2600

2601
/**
2602
 * NOTE: Exported for testing.
2603
 * Private handler for POST /forms/:formId/fields/:fieldId/reorder
2604
 * @precondition Must be preceded by request validation
2605
 * @security session
2606
 *
2607
 * @returns 200 with new ordering of form fields
2608
 * @returns 403 when current user does not have permissions to create a form field
2609
 * @returns 404 when form cannot be found
2610
 * @returns 404 when given fieldId cannot be found in form
2611
 * @returns 410 when reordering form fields for an archived form
2612
 * @returns 422 when user in session cannot be retrieved from the database
2613
 * @returns 500 when database error occurs
2614
 */
2615
export const _handleReorderFormField: ControllerHandler<
8✔
2616
  { formId: string; fieldId: string },
2617
  FormFieldDto[] | ErrorDto,
2618
  unknown,
2619
  { to: number }
2620
> = (req, res) => {
8✔
2621
  const { formId, fieldId } = req.params
11✔
2622
  const { to } = req.query
11✔
2623
  const sessionUserId = (req.session as AuthedSessionData).user._id
11✔
2624

2625
  // Step 1: Retrieve currently logged in user.
2626
  return (
11✔
2627
    UserService.getPopulatedUserById(sessionUserId)
2628
      .andThen((user) =>
2629
        // Step 2: Retrieve form with write permission check.
2630
        AuthService.getFormAfterPermissionChecks({
9✔
2631
          user,
2632
          formId,
2633
          level: PermissionLevel.Write,
2634
        }),
2635
      )
2636
      // Step 3: User has permissions, proceed to reorder field
2637
      .andThen((form) => AdminFormService.reorderFormField(form, fieldId, to))
5✔
2638
      .map((reorderedFormFields) =>
2639
        res.status(StatusCodes.OK).json(reorderedFormFields as FormFieldDto[]),
1✔
2640
      )
2641
      .mapErr((error) => {
2642
        logger.error({
10✔
2643
          message: 'Error occurred when reordering form field',
2644
          meta: {
2645
            action: '_handleReorderFormField',
2646
            ...createReqMeta(req),
2647
            userId: sessionUserId,
2648
            formId,
2649
            fieldId,
2650
            reqQuery: req.query,
2651
          },
2652
          error,
2653
        })
2654
        const { errorMessage, statusCode } = mapRouteError(error)
10✔
2655
        return res.status(statusCode).json({ message: errorMessage })
10✔
2656
      })
2657
  )
2658
}
2659

2660
/**
2661
 * Handler for POST /forms/:formId/fields/:fieldId/reorder
2662
 */
2663
export const handleReorderFormField = [
8✔
2664
  celebrate({
2665
    [Segments.QUERY]: {
2666
      to: Joi.number().min(0).required(),
2667
    },
2668
  }),
2669
  _handleReorderFormField,
2670
] as ControllerHandler[]
2671

2672
/**
2673
 * NOTE: Exported for testing.
2674
 * Private handler for PUT /forms/:formId/logic/:logicId
2675
 * @precondition Must be preceded by request validation
2676
 * @security session
2677
 *
2678
 * @returns 200 with updated logic object when successfully updated
2679
 * @returns 403 when user does not have permissions to update logic
2680
 * @returns 404 when form cannot be found
2681
 * @returns 422 when user in session cannot be retrieved from the database
2682
 * @returns 500 when database error occurs
2683
 */
2684
export const _handleUpdateLogic: ControllerHandler<
8✔
2685
  { formId: string; logicId: string },
2686
  LogicDto | ErrorDto,
2687
  LogicDto
2688
> = (req, res) => {
8✔
2689
  const { formId, logicId } = req.params
12✔
2690
  const updatedLogic = { ...req.body }
12✔
2691
  const sessionUserId = (req.session as AuthedSessionData).user._id
12✔
2692

2693
  // Step 1: Retrieve currently logged in user.
2694
  return (
12✔
2695
    UserService.getPopulatedUserById(sessionUserId)
2696
      .andThen((user) =>
2697
        // Step 2: Retrieve form with write permission check.
2698
        AuthService.getFormAfterPermissionChecks({
9✔
2699
          user,
2700
          formId,
2701
          level: PermissionLevel.Write,
2702
        }),
2703
      )
2704
      // Step 3: Update form logic
2705
      .andThen((retrievedForm) =>
2706
        AdminFormService.updateFormLogic(retrievedForm, logicId, updatedLogic),
5✔
2707
      )
2708
      .map((updatedLogic) =>
2709
        res.status(StatusCodes.OK).json(updatedLogic as LogicDto),
3✔
2710
      )
2711
      .mapErr((error) => {
2712
        logger.error({
9✔
2713
          message: 'Error occurred when updating form logic',
2714
          meta: {
2715
            action: 'handleUpdateLogic',
2716
            ...createReqMeta(req),
2717
            userId: sessionUserId,
2718
            formId,
2719
            logicId,
2720
            updatedLogic,
2721
          },
2722
          error,
2723
        })
2724
        const { errorMessage, statusCode } = mapRouteError(error)
9✔
2725
        return res.status(statusCode).json({ message: errorMessage })
9✔
2726
      })
2727
  )
2728
}
2729

2730
/**
2731
 * Handler for PUT /forms/:formId/logic/:logicId
2732
 */
2733
export const handleUpdateLogic = [
8✔
2734
  celebrate(
2735
    {
2736
      [Segments.BODY]: Joi.object({
2737
        // Ensures given logic is same as accessed logic
2738
        _id: Joi.string().valid(Joi.ref('$params.logicId')).required(),
2739
        ...joiLogicBody,
2740
      }),
2741
    },
2742
    undefined,
2743
    // Required so req.body can be validated against values in req.params.
2744
    // See https://github.com/arb/celebrate#celebrateschema-joioptions-opts.
2745
    { reqContext: true },
2746
  ),
2747
  _handleUpdateLogic,
2748
] as ControllerHandler[]
2749

2750
/**
2751
 * Handler for DELETE /forms/:formId/fields/:fieldId
2752
 * @security session
2753
 *
2754
 * @returns 204 when deletion is successful
2755
 * @returns 403 when current user does not have permissions to delete form field
2756
 * @returns 404 when form cannot be found
2757
 * @returns 404 when form field to delete cannot be found
2758
 * @returns 410 when deleting form field of an archived form
2759
 * @returns 422 when user in session cannot be retrieved from the database
2760
 * @returns 500 when database error occurs during deletion
2761
 */
2762
export const handleDeleteFormField: ControllerHandler<
8✔
2763
  { formId: string; fieldId: string },
2764
  ErrorDto | void
2765
> = (req, res) => {
8✔
2766
  const { formId, fieldId } = req.params
7✔
2767
  const sessionUserId = (req.session as AuthedSessionData).user._id
7✔
2768

2769
  return (
7✔
2770
    // Step 1: Retrieve currently logged in user.
2771
    UserService.getPopulatedUserById(sessionUserId)
2772
      .andThen((user) =>
2773
        // Step 2: Retrieve form with write permission check.
2774
        AuthService.getFormAfterPermissionChecks({
6✔
2775
          user,
2776
          formId,
2777
          level: PermissionLevel.Write,
2778
        }),
2779
      )
2780
      // Step 3: Delete form field.
2781
      .andThen((form) => AdminFormService.deleteFormField(form, fieldId))
3✔
2782
      .map(() => res.sendStatus(StatusCodes.NO_CONTENT))
1✔
2783
      .mapErr((error) => {
2784
        logger.error({
6✔
2785
          message: 'Error occurred when deleting form field',
2786
          meta: {
2787
            action: 'handleDeleteFormField',
2788
            ...createReqMeta(req),
2789
            userId: sessionUserId,
2790
            formId,
2791
            fieldId,
2792
          },
2793
          error,
2794
        })
2795
        const { errorMessage, statusCode } = mapRouteError(error)
6✔
2796
        return res.status(statusCode).json({ message: errorMessage })
6✔
2797
      })
2798
  )
2799
}
2800

2801
/**
2802
 * NOTE: Exported for testing.
2803
 * Private handler for PUT /forms/:formId/end-page
2804
 * @precondition Must be preceded by request validation
2805
 * @security session
2806
 *
2807
 * @returns 200 with updated end page
2808
 * @returns 400 when end page form field has invalid updates to be performed
2809
 * @returns 403 when current user does not have permissions to update the end page
2810
 * @returns 404 when form cannot be found
2811
 * @returns 410 when updating the end page for an archived form
2812
 * @returns 422 when user in session cannot be retrieved from the database
2813
 * @returns 500 when database error occurs
2814
 */
2815
export const _handleUpdateEndPage: ControllerHandler<
8✔
2816
  { formId: string },
2817
  IFormDocument['endPage'] | ErrorDto,
2818
  EndPageUpdateDto
2819
> = (req, res) => {
8✔
2820
  const { formId } = req.params
12✔
2821
  const sessionUserId = (req.session as AuthedSessionData).user._id
12✔
2822

2823
  // Step 1: Retrieve currently logged in user.
2824
  return (
12✔
2825
    UserService.getPopulatedUserById(sessionUserId)
2826
      .andThen((user) =>
2827
        // Step 2: Retrieve form with write permission check.
2828
        AuthService.getFormAfterPermissionChecks({
10✔
2829
          user,
2830
          formId,
2831
          level: PermissionLevel.Write,
2832
        }),
2833
      )
2834
      // Step 3: User has permissions, proceed to allow updating of end page
2835
      .andThen(() => AdminFormService.updateEndPage(formId, req.body))
6✔
2836
      .map((updatedEndPage) => res.status(StatusCodes.OK).json(updatedEndPage))
3✔
2837
      .mapErr((error) => {
2838
        logger.error({
9✔
2839
          message: 'Error occurred when updating end page',
2840
          meta: {
2841
            action: '_handleUpdateEndPage',
2842
            ...createReqMeta(req),
2843
            userId: sessionUserId,
2844
            formId,
2845
            body: req.body,
2846
          },
2847
          error,
2848
        })
2849
        const { errorMessage, statusCode } = mapRouteError(error)
9✔
2850
        return res.status(statusCode).json({ message: errorMessage })
9✔
2851
      })
2852
  )
2853
}
2854

2855
/**
2856
 * Handler for PUT /forms/:formId/end-page
2857
 */
2858
export const handleUpdateEndPage = [
8✔
2859
  celebrate({
2860
    [Segments.BODY]: Joi.object({
2861
      title: Joi.string(),
2862
      paragraph: Joi.string().allow(''),
2863
      buttonLink: Joi.string()
2864
        .uri({ scheme: ['http', 'https'] })
2865
        .allow('')
2866
        .message('Please enter a valid HTTP or HTTPS URI'),
2867
      buttonText: Joi.string().allow(''),
2868
      // TODO(#1895): Remove when deprecated `buttons` key is removed from all forms in the database
2869
      titleTranslations: Joi.array()
2870
        .items(
2871
          Joi.object({
2872
            language: Joi.string()
2873
              .valid(...Object.values(Language))
2874
              .required(),
2875
            translation: Joi.string().required(),
2876
          }),
2877
        )
2878
        .optional()
2879
        .default([]),
2880
      paragraphTranslations: Joi.array()
2881
        .items(
2882
          Joi.object({
2883
            language: Joi.string()
2884
              .valid(...Object.values(Language))
2885
              .required(),
2886
            translation: Joi.string().required(),
2887
          }),
2888
        )
2889
        .optional()
2890
        .default([]),
2891
    }).unknown(true),
2892
  }),
2893
  _handleUpdateEndPage,
2894
] as ControllerHandler[]
2895

2896
/**
2897
 * Handler for GET /admin/forms/:formId/fields/:fieldId
2898
 * @security session
2899
 *
2900
 * @returns 200 with form field when retrieval is successful
2901
 * @returns 403 when current user does not have permissions to retrieve form field
2902
 * @returns 404 when form cannot be found
2903
 * @returns 404 when form field cannot be found
2904
 * @returns 410 when retrieving form field of an archived form
2905
 * @returns 422 when user in session cannot be retrieved from the database
2906
 * @returns 500 when database error occurs
2907
 */
2908
export const handleGetFormField: ControllerHandler<
8✔
2909
  {
2910
    formId: string
2911
    fieldId: string
2912
  },
2913
  ErrorDto | FormFieldDto
2914
> = (req, res) => {
8✔
2915
  const { formId, fieldId } = req.params
14✔
2916
  const sessionUserId = (req.session as AuthedSessionData).user._id
14✔
2917

2918
  return (
14✔
2919
    // Step 1: Retrieve currently logged in user.
2920
    UserService.getPopulatedUserById(sessionUserId)
2921
      .andThen((user) =>
2922
        // Step 2: Retrieve form with read permission check.
2923
        AuthService.getFormAfterPermissionChecks({
11✔
2924
          user,
2925
          formId,
2926
          level: PermissionLevel.Read,
2927
        }),
2928
      )
2929
      .andThen((form) => AdminFormService.getFormField(form, fieldId))
5✔
2930
      .map((formField) =>
2931
        res.status(StatusCodes.OK).json(formField as FormFieldDto),
2✔
2932
      )
2933
      .mapErr((error) => {
2934
        logger.error({
12✔
2935
          message: 'Error occurred when retrieving form field',
2936
          meta: {
2937
            action: 'handleGetFormField',
2938
            ...createReqMeta(req),
2939
            userId: sessionUserId,
2940
            formId,
2941
            fieldId,
2942
          },
2943
          error,
2944
        })
2945
        const { errorMessage, statusCode } = mapRouteError(error)
12✔
2946
        return res.status(statusCode).json({ message: errorMessage })
12✔
2947
      })
2948
  )
2949
}
2950

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

3007
/**
3008
 * Handler for PUT /api/v3/admin/forms/:formId/collaborators
3009
 */
3010
export const handleUpdateCollaborators = [
8✔
3011
  celebrate({
3012
    [Segments.BODY]: Joi.array().items(
3013
      Joi.object({
3014
        email: Joi.string()
3015
          .required()
3016
          .email()
3017
          .message('Please enter a valid email')
3018
          .lowercase(),
3019
        write: Joi.bool().optional(),
3020
        _id: Joi.string().optional(),
3021
      }),
3022
    ),
3023
  }),
3024
  _handleUpdateCollaborators,
3025
] as ControllerHandler[]
3026

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

3089
/**
3090
 * NOTE: Exported for testing.
3091
 * Private handler for PUT /forms/:formId/start-page
3092
 * @precondition Must be preceded by request validation
3093
 * @security session
3094
 *
3095
 * @returns 200 with updated start page
3096
 * @returns 403 when current user does not have permissions to update the start page
3097
 * @returns 404 when form cannot be found
3098
 * @returns 410 when updating the start page for an archived form
3099
 * @returns 422 when user in session cannot be retrieved from the database
3100
 * @returns 500 when database error occurs
3101
 */
3102
export const _handleUpdateStartPage: ControllerHandler<
8✔
3103
  { formId: string },
3104
  IFormDocument['startPage'] | ErrorDto,
3105
  StartPageUpdateDto
3106
> = (req, res) => {
8✔
3107
  const { formId } = req.params
16✔
3108
  const sessionUserId = (req.session as AuthedSessionData).user._id
16✔
3109

3110
  // Step 1: Retrieve currently logged in user.
3111
  return (
16✔
3112
    UserService.getPopulatedUserById(sessionUserId)
3113
      .andThen((user) =>
3114
        // Step 2: Retrieve form with write permission check.
3115
        AuthService.getFormAfterPermissionChecks({
13✔
3116
          user,
3117
          formId,
3118
          level: PermissionLevel.Write,
3119
        }),
3120
      )
3121
      // Step 3: User has permissions, proceed to allow updating of start page
3122
      .andThen(() => AdminFormService.updateStartPage(formId, req.body))
6✔
3123
      .map((updatedStartPage) =>
3124
        res.status(StatusCodes.OK).json(updatedStartPage),
2✔
3125
      )
3126
      .mapErr((error) => {
3127
        logger.error({
14✔
3128
          message: 'Error occurred when updating start page',
3129
          meta: {
3130
            action: '_handleUpdateStartPage',
3131
            ...createReqMeta(req),
3132
            userId: sessionUserId,
3133
            formId,
3134
            body: req.body,
3135
          },
3136
          error,
3137
        })
3138
        const { errorMessage, statusCode } = mapRouteError(error)
14✔
3139
        return res.status(statusCode).json({ message: errorMessage })
14✔
3140
      })
3141
  )
3142
}
3143

3144
/**
3145
 * Handler for PUT /forms/:formId/start-page
3146
 */
3147
export const handleUpdateStartPage = [
8✔
3148
  celebrate({
3149
    [Segments.BODY]: {
3150
      paragraph: Joi.string().allow('').optional(),
3151
      estTimeTaken: Joi.number().min(1).max(1000).required(),
3152
      colorTheme: Joi.string()
3153
        .valid(...Object.values(FormColorTheme))
3154
        .required(),
3155
      logo: Joi.object({
3156
        state: Joi.string().valid(...Object.values(FormLogoState)),
3157
        fileId: Joi.when('state', {
3158
          is: FormLogoState.Custom,
3159
          then: Joi.string().required(),
3160
          otherwise: Joi.any().forbidden(),
3161
        }),
3162
        fileName: Joi.when('state', {
3163
          is: FormLogoState.Custom,
3164
          then: Joi.string()
3165
            // Captures only the extensions below regardless of their case
3166
            // Refer to https://regex101.com/ with the below regex for a full explanation
3167
            .pattern(/\.(gif|png|jpeg|jpg|jfif)$/im)
3168
            .required(),
3169
          otherwise: Joi.any().forbidden(),
3170
        }),
3171
        fileSizeInBytes: Joi.when('state', {
3172
          is: FormLogoState.Custom,
3173
          then: Joi.number().max(MAX_UPLOAD_FILE_SIZE).required(),
3174
          otherwise: Joi.any().forbidden(),
3175
        }),
3176
      }).required(),
3177
      paragraphTranslations: Joi.array()
3178
        .items(
3179
          Joi.object({
3180
            language: Joi.string()
3181
              .valid(...Object.values(Language))
3182
              .required(),
3183
            translation: Joi.string().required(),
3184
          }),
3185
        )
3186
        .optional()
3187
        .default([]),
3188
    },
3189
  }),
3190
  _handleUpdateStartPage,
3191
] as ControllerHandler[]
3192

3193
export const handleGetGoLinkSuffix: ControllerHandler<{ formId: string }> = (
8✔
3194
  req,
3195
  res,
3196
) => {
3197
  const { formId } = req.params
×
3198
  const sessionUserId = (req.session as AuthedSessionData).user._id
×
3199

3200
  // Step 1: Get the form after permission checks
3201
  return (
×
3202
    UserService.getPopulatedUserById(sessionUserId)
3203
      .andThen((user) => {
3204
        return AuthService.getFormAfterPermissionChecks({
×
3205
          user,
3206
          formId,
3207
          level: PermissionLevel.Read,
3208
        })
3209
      })
3210
      // Step 2: After permission checks, get the GoGov link suffix
3211
      .andThen(() => {
3212
        return AdminFormService.getGoLinkSuffix(formId)
×
3213
      })
3214
      .map((goLinkSuffix) => res.status(StatusCodes.OK).json(goLinkSuffix))
×
3215
      .mapErr((error) => {
3216
        const { errorMessage, statusCode } = mapRouteError(error)
×
3217
        // Don't log 404 errors as they are expected for most forms
3218
        if (statusCode !== StatusCodes.NOT_FOUND) {
×
3219
          logger.error({
×
3220
            message: 'Error occurred when getting GoGov link suffix',
3221
            meta: {
3222
              action: 'handleGetGoLinkSuffix',
3223
              ...createReqMeta(req),
3224
              userId: sessionUserId,
3225
              formId,
3226
            },
3227
            error,
3228
          })
3229
        }
3230
        return res.status(statusCode).json({ message: errorMessage })
×
3231
      })
3232
  )
3233
}
3234

3235
export const handleSetGoLinkSuffix: ControllerHandler<
8✔
3236
  { formId: string },
3237
  unknown,
3238
  { linkSuffix: string; adminEmail: string }
3239
> = (req, res) => {
8✔
3240
  const goGovBaseUrl = goGovConfig.goGovBaseUrl
×
3241
  const { formId } = req.params
×
3242
  const { linkSuffix, adminEmail } = req.body
×
3243
  const sessionUserId = (req.session as AuthedSessionData).user._id
×
3244

3245
  // Step 1: Get the form after permission checks
3246
  return (
×
3247
    UserService.getPopulatedUserById(sessionUserId)
3248
      .andThen((user) => {
3249
        return AuthService.getFormAfterPermissionChecks({
×
3250
          user,
3251
          formId,
3252
          level: PermissionLevel.Write,
3253
        })
3254
      })
3255
      // Step 2: After permission checks, try to get GoGov link
3256
      .andThen(() => {
3257
        return ResultAsync.fromPromise(
×
3258
          axios.post(
3259
            `${goGovBaseUrl}/api/v1/admin/urls`,
3260
            {
3261
              longUrl: `${process.env.APP_URL}/${formId}`,
3262
              shortUrl: linkSuffix,
3263
              email: adminEmail,
3264
            },
3265
            {
3266
              headers: {
3267
                Authorization: `Bearer ${goGovConfig.goGovAPIKey}`,
3268
                // Required due to bug introduced in axios 1.2.1: https://github.com/axios/axios/issues/5346
3269
                // TODO: remove when axios is upgraded to 1.2.2
3270
                'Accept-Encoding': 'gzip,deflate,compress',
3271
              },
3272
            },
3273
          ),
3274
          (error) => {
3275
            if (axios.isAxiosError(error)) {
×
3276
              return mapGoGovErrors(error)
×
3277
            }
3278

3279
            return new GoGovServerError()
×
3280
          },
3281
        )
3282
      })
3283
      // Step 3: After obtaining GoGov link, save it to the form
3284
      .andThen(() => AdminFormService.setGoLinkSuffix(formId, linkSuffix))
×
3285
      .map(() => res.sendStatus(StatusCodes.OK))
×
3286
      .mapErr((error) => {
3287
        logger.error({
×
3288
          message: 'Error occurred when setting GoGov link suffix',
3289
          meta: {
3290
            action: 'handleSetGoLinkSuffix',
3291
            ...createReqMeta(req),
3292
            userId: sessionUserId,
3293
            formId,
3294
          },
3295
          error,
3296
        })
3297
        const { errorMessage, statusCode } = mapRouteError(error)
×
3298
        return res.status(statusCode).json({ message: errorMessage })
×
3299
      })
3300
  )
3301
}
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