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

opengovsg / FormSG / 14049170525

25 Mar 2025 01:13AM UTC coverage: 72.481% (+0.06%) from 72.421%
14049170525

push

github

scottheng96
removed ununsed imports and comments

2812 of 4713 branches covered (59.66%)

Branch coverage included in aggregate %.

10131 of 13144 relevant lines covered (77.08%)

47.17 hits per line

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

82.8
/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.array().items(Joi.string().email()).required(),
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
17✔
909
  const userId = (req.session as AuthedSessionData).user._id
17✔
910
  const { workspaceId, ...overrideParams } = req.body
17✔
911

912
  return (
17✔
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({
14✔
918
          user,
919
          formId,
920
          level: PermissionLevel.Read,
921
        })
922
          .andThen((originalForm) =>
923
            // Step 3: Duplicate form.
924
            AdminFormService.duplicateForm(
8✔
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)),
6✔
933
      )
934
      // Success; return duplicated form's dashboard view.
935
      .map((dupedDashView) => res.json(dupedDashView))
6✔
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(',').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
  {
2279
    optionsToRecipientsMap: Record<string, string[]>
2280
    fieldOptions: string[]
2281
  }
2282
> = (req, res) => {
8✔
2283
  const { formId, fieldId } = req.params
×
2284
  const { optionsToRecipientsMap, fieldOptions } = req.body
×
2285
  const sessionUserId = (req.session as AuthedSessionData).user._id
×
2286

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

2326
export const handleUpdateOptionsToRecipientsMap = [
8✔
2327
  celebrate({
2328
    [Segments.BODY]: Joi.object({
2329
      optionsToRecipientsMap: Joi.object(),
2330
      fieldOptions: Joi.array().items(Joi.string()).required(),
2331
    }),
2332
    [Segments.PARAMS]: Joi.object({
2333
      formId: Joi.string().required(),
2334
      fieldId: Joi.string().required(),
2335
    }),
2336
  }),
2337
  _handleUpdateOptionsToRecipientsMap,
2338
] as ControllerHandler[]
2339

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

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

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

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

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

2515
/**
2516
 * Handler for POST /forms/:formId/logic
2517
 */
2518
export const handleCreateLogic = [
8✔
2519
  celebrate({
2520
    [Segments.BODY]: joiLogicBody,
2521
  }),
2522
  _handleCreateLogic,
2523
] as ControllerHandler[]
2524

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

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

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

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

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

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

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

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

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

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

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

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

2806
/**
2807
 * Handler for POST /forms/:formId/fields/delete
2808
 * @security session
2809
 *
2810
 * @returns 204 when deletion is successful
2811
 * @returns 403 when current user does not have permissions to delete form fields
2812
 * @returns 404 when form cannot be found
2813
 * @returns 410 when deleting fields of an archived form
2814
 * @returns 422 when user in session cannot be retrieved from the database
2815
 * @returns 500 when database error occurs during deletion
2816
 */
2817
export const handleDeleteFormFields: ControllerHandler<
8✔
2818
  { formId: string },
2819
  { message: string } | ErrorDto,
2820
  { fieldIds: string[] }
2821
> = (req, res) => {
8✔
2822
  const { formId } = req.params
×
2823
  const { fieldIds } = req.body
×
2824
  const sessionUserId = (req.session as AuthedSessionData).user._id
×
2825

2826
  return (
×
2827
    // Step 1: Retrieve currently logged in user.
2828
    UserService.getPopulatedUserById(sessionUserId)
2829
      .andThen((user) =>
2830
        // Step 2: Retrieve form with write permission check.
2831
        AuthService.getFormAfterPermissionChecks({
×
2832
          user,
2833
          formId,
2834
          level: PermissionLevel.Write,
2835
        }),
2836
      )
2837
      // Step 3: Delete form fields.
2838
      .andThen((form) => AdminFormService.deleteFormFields(form, fieldIds))
×
2839
      .map(() => res.sendStatus(StatusCodes.NO_CONTENT))
×
2840
      .mapErr((error) => {
2841
        logger.error({
×
2842
          message: 'Error occurred when deleting form fields',
2843
          meta: {
2844
            action: 'handleDeleteFormFields',
2845
            ...createReqMeta(req),
2846
            userId: sessionUserId,
2847
            formId,
2848
            fieldIds,
2849
          },
2850
          error,
2851
        })
2852
        const { errorMessage, statusCode } = mapRouteError(error)
×
2853
        return res.status(statusCode).json({ message: errorMessage })
×
2854
      })
2855
  )
2856
}
2857

2858
/**
2859
 * NOTE: Exported for testing.
2860
 * Private handler for PUT /forms/:formId/end-page
2861
 * @precondition Must be preceded by request validation
2862
 * @security session
2863
 *
2864
 * @returns 200 with updated end page
2865
 * @returns 400 when end page form field has invalid updates to be performed
2866
 * @returns 403 when current user does not have permissions to update the end page
2867
 * @returns 404 when form cannot be found
2868
 * @returns 410 when updating the end page for an archived form
2869
 * @returns 422 when user in session cannot be retrieved from the database
2870
 * @returns 500 when database error occurs
2871
 */
2872
export const _handleUpdateEndPage: ControllerHandler<
8✔
2873
  { formId: string },
2874
  IFormDocument['endPage'] | ErrorDto,
2875
  EndPageUpdateDto
2876
> = (req, res) => {
8✔
2877
  const { formId } = req.params
12✔
2878
  const sessionUserId = (req.session as AuthedSessionData).user._id
12✔
2879

2880
  // Step 1: Retrieve currently logged in user.
2881
  return (
12✔
2882
    UserService.getPopulatedUserById(sessionUserId)
2883
      .andThen((user) =>
2884
        // Step 2: Retrieve form with write permission check.
2885
        AuthService.getFormAfterPermissionChecks({
10✔
2886
          user,
2887
          formId,
2888
          level: PermissionLevel.Write,
2889
        }),
2890
      )
2891
      // Step 3: User has permissions, proceed to allow updating of end page
2892
      .andThen(() => AdminFormService.updateEndPage(formId, req.body))
6✔
2893
      .map((updatedEndPage) => res.status(StatusCodes.OK).json(updatedEndPage))
3✔
2894
      .mapErr((error) => {
2895
        logger.error({
9✔
2896
          message: 'Error occurred when updating end page',
2897
          meta: {
2898
            action: '_handleUpdateEndPage',
2899
            ...createReqMeta(req),
2900
            userId: sessionUserId,
2901
            formId,
2902
            body: req.body,
2903
          },
2904
          error,
2905
        })
2906
        const { errorMessage, statusCode } = mapRouteError(error)
9✔
2907
        return res.status(statusCode).json({ message: errorMessage })
9✔
2908
      })
2909
  )
2910
}
2911

2912
/**
2913
 * Handler for PUT /forms/:formId/end-page
2914
 */
2915
export const handleUpdateEndPage = [
8✔
2916
  celebrate({
2917
    [Segments.BODY]: Joi.object({
2918
      title: Joi.string(),
2919
      paragraph: Joi.string().allow(''),
2920
      buttonLink: Joi.string()
2921
        .uri({ scheme: ['http', 'https'] })
2922
        .allow('')
2923
        .message('Please enter a valid HTTP or HTTPS URI'),
2924
      buttonText: Joi.string().allow(''),
2925
      // TODO(#1895): Remove when deprecated `buttons` key is removed from all forms in the database
2926
      titleTranslations: Joi.array()
2927
        .items(
2928
          Joi.object({
2929
            language: Joi.string()
2930
              .valid(...Object.values(Language))
2931
              .required(),
2932
            translation: Joi.string().required(),
2933
          }),
2934
        )
2935
        .optional()
2936
        .default([]),
2937
      paragraphTranslations: Joi.array()
2938
        .items(
2939
          Joi.object({
2940
            language: Joi.string()
2941
              .valid(...Object.values(Language))
2942
              .required(),
2943
            translation: Joi.string().required(),
2944
          }),
2945
        )
2946
        .optional()
2947
        .default([]),
2948
    }).unknown(true),
2949
  }),
2950
  _handleUpdateEndPage,
2951
] as ControllerHandler[]
2952

2953
/**
2954
 * Handler for GET /admin/forms/:formId/fields/:fieldId
2955
 * @security session
2956
 *
2957
 * @returns 200 with form field when retrieval is successful
2958
 * @returns 403 when current user does not have permissions to retrieve form field
2959
 * @returns 404 when form cannot be found
2960
 * @returns 404 when form field cannot be found
2961
 * @returns 410 when retrieving form field of an archived form
2962
 * @returns 422 when user in session cannot be retrieved from the database
2963
 * @returns 500 when database error occurs
2964
 */
2965
export const handleGetFormField: ControllerHandler<
8✔
2966
  {
2967
    formId: string
2968
    fieldId: string
2969
  },
2970
  ErrorDto | FormFieldDto
2971
> = (req, res) => {
8✔
2972
  const { formId, fieldId } = req.params
14✔
2973
  const sessionUserId = (req.session as AuthedSessionData).user._id
14✔
2974

2975
  return (
14✔
2976
    // Step 1: Retrieve currently logged in user.
2977
    UserService.getPopulatedUserById(sessionUserId)
2978
      .andThen((user) =>
2979
        // Step 2: Retrieve form with read permission check.
2980
        AuthService.getFormAfterPermissionChecks({
11✔
2981
          user,
2982
          formId,
2983
          level: PermissionLevel.Read,
2984
        }),
2985
      )
2986
      .andThen((form) => AdminFormService.getFormField(form, fieldId))
5✔
2987
      .map((formField) =>
2988
        res.status(StatusCodes.OK).json(formField as FormFieldDto),
2✔
2989
      )
2990
      .mapErr((error) => {
2991
        logger.error({
12✔
2992
          message: 'Error occurred when retrieving form field',
2993
          meta: {
2994
            action: 'handleGetFormField',
2995
            ...createReqMeta(req),
2996
            userId: sessionUserId,
2997
            formId,
2998
            fieldId,
2999
          },
3000
          error,
3001
        })
3002
        const { errorMessage, statusCode } = mapRouteError(error)
12✔
3003
        return res.status(statusCode).json({ message: errorMessage })
12✔
3004
      })
3005
  )
3006
}
3007

3008
/**
3009
 * NOTE: Exported for testing.
3010
 * Private handler for PUT /api/v3/admin/forms/:formId/collaborators
3011
 * @precondition Must be preceded by request validation
3012
 * @security session
3013
 *
3014
 * @returns 200 with updated collaborators and permissions
3015
 * @returns 403 when current user does not havße permissions to update the collaborators
3016
 * @returns 404 when form cannot be found
3017
 * @returns 410 when updating collaborators for an archived form
3018
 * @returns 422 when user in session cannot be retrieved from the database
3019
 * @returns 500 when database error occurs
3020
 */
3021
export const _handleUpdateCollaborators: ControllerHandler<
8✔
3022
  { formId: string },
3023
  PermissionsUpdateDto | ErrorDto,
3024
  PermissionsUpdateDto
3025
> = (req, res) => {
8✔
3026
  const { formId } = req.params
12✔
3027
  const sessionUserId = (req.session as AuthedSessionData).user._id
12✔
3028
  // Step 1: Get the form after permission checks
3029
  return (
12✔
3030
    UserService.getPopulatedUserById(sessionUserId)
3031
      .andThen((user) =>
3032
        // Step 2: Retrieve form with write permission check.
3033
        AuthService.getFormAfterPermissionChecks({
8✔
3034
          user,
3035
          formId,
3036
          level: PermissionLevel.Write,
3037
        }),
3038
      )
3039
      // Step 2: Update the form collaborators
3040
      .andThen((form) =>
3041
        AdminFormService.updateFormCollaborators(form, req.body),
2✔
3042
      )
3043
      .map((updatedCollaborators) =>
3044
        res.status(StatusCodes.OK).json(updatedCollaborators),
2✔
3045
      )
3046
      .mapErr((error) => {
3047
        logger.error({
10✔
3048
          message: 'Error occurred when updating collaborators',
3049
          meta: {
3050
            action: '_handleUpdateCollaborators',
3051
            ...createReqMeta(req),
3052
            userId: sessionUserId,
3053
            formId,
3054
            formCollaborators: req.body,
3055
          },
3056
          error,
3057
        })
3058
        const { errorMessage, statusCode } = mapRouteError(error)
10✔
3059
        return res.status(statusCode).json({ message: errorMessage })
10✔
3060
      })
3061
  )
3062
}
3063

3064
/**
3065
 * Handler for PUT /api/v3/admin/forms/:formId/collaborators
3066
 */
3067
export const handleUpdateCollaborators = [
8✔
3068
  celebrate({
3069
    [Segments.BODY]: Joi.array().items(
3070
      Joi.object({
3071
        email: Joi.string()
3072
          .required()
3073
          .email()
3074
          .message('Please enter a valid email')
3075
          .lowercase(),
3076
        write: Joi.bool().optional(),
3077
        _id: Joi.string().optional(),
3078
      }),
3079
    ),
3080
  }),
3081
  _handleUpdateCollaborators,
3082
] as ControllerHandler[]
3083

3084
/**
3085
 * Handler for DELETE /api/v3/admin/forms/:formId/collaborators/self
3086
 * @precondition Must be preceded by request validation
3087
 * @security session
3088
 *
3089
 * @returns 200 with updated collaborators and permissions
3090
 * @returns 403 when current user does not have permissions to remove themselves from the collaborators list
3091
 * @returns 404 when form cannot be found
3092
 * @returns 410 when updating collaborators for an archived form
3093
 * @returns 422 when user in session cannot be retrieved from the database
3094
 * @returns 500 when database error occurs
3095
 */
3096
export const handleRemoveSelfFromCollaborators: ControllerHandler<
8✔
3097
  { formId: string },
3098
  PermissionsUpdateDto | ErrorDto
3099
> = (req, res) => {
8✔
3100
  const { formId } = req.params
7✔
3101
  const sessionUserId = (req.session as AuthedSessionData).user._id
7✔
3102
  let currentUserEmail = ''
7✔
3103
  // Step 1: Get the form after permission checks
3104
  return (
7✔
3105
    UserService.getPopulatedUserById(sessionUserId)
3106
      .andThen((user) => {
3107
        // Step 2: Retrieve form with read permission check, since we are only removing the user themselves
3108
        currentUserEmail = user.email
5✔
3109
        return AuthService.getFormAfterPermissionChecks({
5✔
3110
          user,
3111
          formId,
3112
          level: PermissionLevel.Read,
3113
        })
3114
      })
3115
      // Step 3: Update the form collaborators
3116
      .andThen((form) => {
3117
        const updatedPermissionList = form.permissionList.filter(
2✔
3118
          (user) => user.email.toLowerCase() !== currentUserEmail.toLowerCase(),
4✔
3119
        )
3120
        return AdminFormService.updateFormCollaborators(
2✔
3121
          form,
3122
          updatedPermissionList,
3123
        )
3124
      })
3125
      .map((updatedCollaborators) =>
3126
        res.status(StatusCodes.OK).json(updatedCollaborators),
2✔
3127
      )
3128
      .mapErr((error) => {
3129
        logger.error({
5✔
3130
          message: 'Error occurred when updating collaborators',
3131
          meta: {
3132
            action: 'handleRemoveSelfFromCollaborators',
3133
            ...createReqMeta(req),
3134
            userId: sessionUserId,
3135
            formId,
3136
            formCollaborators: req.body,
3137
          },
3138
          error,
3139
        })
3140
        const { errorMessage, statusCode } = mapRouteError(error)
5✔
3141
        return res.status(statusCode).json({ message: errorMessage })
5✔
3142
      })
3143
  )
3144
}
3145

3146
/**
3147
 * NOTE: Exported for testing.
3148
 * Private handler for PUT /forms/:formId/start-page
3149
 * @precondition Must be preceded by request validation
3150
 * @security session
3151
 *
3152
 * @returns 200 with updated start page
3153
 * @returns 403 when current user does not have permissions to update the start page
3154
 * @returns 404 when form cannot be found
3155
 * @returns 410 when updating the start page for an archived form
3156
 * @returns 422 when user in session cannot be retrieved from the database
3157
 * @returns 500 when database error occurs
3158
 */
3159
export const _handleUpdateStartPage: ControllerHandler<
8✔
3160
  { formId: string },
3161
  IFormDocument['startPage'] | ErrorDto,
3162
  StartPageUpdateDto
3163
> = (req, res) => {
8✔
3164
  const { formId } = req.params
16✔
3165
  const sessionUserId = (req.session as AuthedSessionData).user._id
16✔
3166

3167
  // Step 1: Retrieve currently logged in user.
3168
  return (
16✔
3169
    UserService.getPopulatedUserById(sessionUserId)
3170
      .andThen((user) =>
3171
        // Step 2: Retrieve form with write permission check.
3172
        AuthService.getFormAfterPermissionChecks({
13✔
3173
          user,
3174
          formId,
3175
          level: PermissionLevel.Write,
3176
        }),
3177
      )
3178
      // Step 3: User has permissions, proceed to allow updating of start page
3179
      .andThen(() => AdminFormService.updateStartPage(formId, req.body))
6✔
3180
      .map((updatedStartPage) =>
3181
        res.status(StatusCodes.OK).json(updatedStartPage),
2✔
3182
      )
3183
      .mapErr((error) => {
3184
        logger.error({
14✔
3185
          message: 'Error occurred when updating start page',
3186
          meta: {
3187
            action: '_handleUpdateStartPage',
3188
            ...createReqMeta(req),
3189
            userId: sessionUserId,
3190
            formId,
3191
            body: req.body,
3192
          },
3193
          error,
3194
        })
3195
        const { errorMessage, statusCode } = mapRouteError(error)
14✔
3196
        return res.status(statusCode).json({ message: errorMessage })
14✔
3197
      })
3198
  )
3199
}
3200

3201
/**
3202
 * Handler for PUT /forms/:formId/start-page
3203
 */
3204
export const handleUpdateStartPage = [
8✔
3205
  celebrate({
3206
    [Segments.BODY]: {
3207
      paragraph: Joi.string().allow('').optional(),
3208
      estTimeTaken: Joi.number().min(1).max(1000).required(),
3209
      colorTheme: Joi.string()
3210
        .valid(...Object.values(FormColorTheme))
3211
        .required(),
3212
      logo: Joi.object({
3213
        state: Joi.string().valid(...Object.values(FormLogoState)),
3214
        fileId: Joi.when('state', {
3215
          is: FormLogoState.Custom,
3216
          then: Joi.string().required(),
3217
          otherwise: Joi.any().forbidden(),
3218
        }),
3219
        fileName: Joi.when('state', {
3220
          is: FormLogoState.Custom,
3221
          then: Joi.string()
3222
            // Captures only the extensions below regardless of their case
3223
            // Refer to https://regex101.com/ with the below regex for a full explanation
3224
            .pattern(/\.(gif|png|jpeg|jpg|jfif)$/im)
3225
            .required(),
3226
          otherwise: Joi.any().forbidden(),
3227
        }),
3228
        fileSizeInBytes: Joi.when('state', {
3229
          is: FormLogoState.Custom,
3230
          then: Joi.number().max(MAX_UPLOAD_FILE_SIZE).required(),
3231
          otherwise: Joi.any().forbidden(),
3232
        }),
3233
      }).required(),
3234
      paragraphTranslations: Joi.array()
3235
        .items(
3236
          Joi.object({
3237
            language: Joi.string()
3238
              .valid(...Object.values(Language))
3239
              .required(),
3240
            translation: Joi.string().required(),
3241
          }),
3242
        )
3243
        .optional()
3244
        .default([]),
3245
    },
3246
  }),
3247
  _handleUpdateStartPage,
3248
] as ControllerHandler[]
3249

3250
export const handleGetGoLinkSuffix: ControllerHandler<{ formId: string }> = (
8✔
3251
  req,
3252
  res,
3253
) => {
3254
  const { formId } = req.params
×
3255
  const sessionUserId = (req.session as AuthedSessionData).user._id
×
3256

3257
  // Step 1: Get the form after permission checks
3258
  return (
×
3259
    UserService.getPopulatedUserById(sessionUserId)
3260
      .andThen((user) => {
3261
        return AuthService.getFormAfterPermissionChecks({
×
3262
          user,
3263
          formId,
3264
          level: PermissionLevel.Read,
3265
        })
3266
      })
3267
      // Step 2: After permission checks, get the GoGov link suffix
3268
      .andThen(() => {
3269
        return AdminFormService.getGoLinkSuffix(formId)
×
3270
      })
3271
      .map((goLinkSuffix) => res.status(StatusCodes.OK).json(goLinkSuffix))
×
3272
      .mapErr((error) => {
3273
        const { errorMessage, statusCode } = mapRouteError(error)
×
3274
        // Don't log 404 errors as they are expected for most forms
3275
        if (statusCode !== StatusCodes.NOT_FOUND) {
×
3276
          logger.error({
×
3277
            message: 'Error occurred when getting GoGov link suffix',
3278
            meta: {
3279
              action: 'handleGetGoLinkSuffix',
3280
              ...createReqMeta(req),
3281
              userId: sessionUserId,
3282
              formId,
3283
            },
3284
            error,
3285
          })
3286
        }
3287
        return res.status(statusCode).json({ message: errorMessage })
×
3288
      })
3289
  )
3290
}
3291

3292
export const handleSetGoLinkSuffix: ControllerHandler<
8✔
3293
  { formId: string },
3294
  unknown,
3295
  { linkSuffix: string; adminEmail: string }
3296
> = (req, res) => {
8✔
3297
  const goGovBaseUrl = goGovConfig.goGovBaseUrl
×
3298
  const { formId } = req.params
×
3299
  const { linkSuffix, adminEmail } = req.body
×
3300
  const sessionUserId = (req.session as AuthedSessionData).user._id
×
3301

3302
  // Step 1: Get the form after permission checks
3303
  return (
×
3304
    UserService.getPopulatedUserById(sessionUserId)
3305
      .andThen((user) => {
3306
        return AuthService.getFormAfterPermissionChecks({
×
3307
          user,
3308
          formId,
3309
          level: PermissionLevel.Write,
3310
        })
3311
      })
3312
      // Step 2: After permission checks, try to get GoGov link
3313
      .andThen(() => {
3314
        return ResultAsync.fromPromise(
×
3315
          axios.post(
3316
            `${goGovBaseUrl}/api/v1/admin/urls`,
3317
            {
3318
              longUrl: `${process.env.APP_URL}/${formId}`,
3319
              shortUrl: linkSuffix,
3320
              email: adminEmail,
3321
            },
3322
            {
3323
              headers: {
3324
                Authorization: `Bearer ${goGovConfig.goGovAPIKey}`,
3325
                // Required due to bug introduced in axios 1.2.1: https://github.com/axios/axios/issues/5346
3326
                // TODO: remove when axios is upgraded to 1.2.2
3327
                'Accept-Encoding': 'gzip,deflate,compress',
3328
              },
3329
            },
3330
          ),
3331
          (error) => {
3332
            if (axios.isAxiosError(error)) {
×
3333
              return mapGoGovErrors(error)
×
3334
            }
3335

3336
            return new GoGovServerError()
×
3337
          },
3338
        )
3339
      })
3340
      // Step 3: After obtaining GoGov link, save it to the form
3341
      .andThen(() => AdminFormService.setGoLinkSuffix(formId, linkSuffix))
×
3342
      .map(() => res.sendStatus(StatusCodes.OK))
×
3343
      .mapErr((error) => {
3344
        logger.error({
×
3345
          message: 'Error occurred when setting GoGov link suffix',
3346
          meta: {
3347
            action: 'handleSetGoLinkSuffix',
3348
            ...createReqMeta(req),
3349
            userId: sessionUserId,
3350
            formId,
3351
          },
3352
          error,
3353
        })
3354
        const { errorMessage, statusCode } = mapRouteError(error)
×
3355
        return res.status(statusCode).json({ message: errorMessage })
×
3356
      })
3357
  )
3358
}
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