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

opengovsg / FormSG / 18089118599

29 Sep 2025 07:21AM UTC coverage: 71.723% (-0.04%) from 71.76%
18089118599

push

github

web-flow
Merge pull request #8766 from opengovsg/release-al2

build: merge release v6.249.0 to develop

3073 of 5208 branches covered (59.01%)

Branch coverage included in aggregate %.

10966 of 14366 relevant lines covered (76.33%)

48.03 hits per line

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

82.37
/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
  SmsCountsDto,
48
  StartPageUpdateDto,
49
  SubmissionCountQueryDto,
50
  WebhookSettingsUpdateDto,
51
} from '../../../../../shared/types'
52
import {
8✔
53
  EncryptedStringsMessageContent,
54
  encryptStringsMessage,
55
} from '../../../../../shared/utils/crypto'
56
import { IFormDocument, IPopulatedForm } from '../../../../types'
57
import {
58
  EncryptSubmissionDto,
59
  FormUpdateParams,
60
  ParsedEmailModeSubmissionBody,
61
} from '../../../../types/api'
62
import { goGovConfig } from '../../../config/features/gogov.config'
8✔
63
import { smsConfig } from '../../../config/features/sms.config'
8✔
64
import { createLoggerWithLabel } from '../../../config/logger'
8✔
65
import MailService from '../../../services/mail/mail.service'
8✔
66
import * as SmsService from '../../../services/sms/sms.service'
8✔
67
import { createReqMeta } from '../../../utils/request'
8✔
68
import * as AuthService from '../../auth/auth.service'
8✔
69
import {
70
  DatabaseConflictError,
71
  DatabaseError,
72
  DatabasePayloadSizeError,
73
  DatabaseValidationError,
74
} from '../../core/core.errors'
75
import { ControllerHandler } from '../../core/core.types'
76
import * as FeedbackService from '../../feedback/feedback.service'
8✔
77
import * as EmailSubmissionMiddleware from '../../submission/email-submission/email-submission.middleware'
8✔
78
import * as EmailSubmissionService from '../../submission/email-submission/email-submission.service'
8✔
79
import {
8✔
80
  mapRouteError as mapEmailSubmissionError,
81
  SubmissionEmailObj,
82
} from '../../submission/email-submission/email-submission.util'
83
import * as EncryptSubmissionService from '../../submission/encrypt-submission/encrypt-submission.service'
8✔
84
import ParsedResponsesObject from '../../submission/ParsedResponsesObject.class'
8✔
85
import * as ReceiverMiddleware from '../../submission/receiver/receiver.middleware'
8✔
86
import * as SubmissionService from '../../submission/submission.service'
8✔
87
import {
8✔
88
  extractEmailConfirmationData,
89
  mapAttachmentsFromResponses,
90
  mapRouteError as mapSubmissionError,
91
} from '../../submission/submission.utils'
92
import * as UserService from '../../user/user.service'
8✔
93
import { removeFormsFromAllWorkspaces } from '../../workspace/workspace.service'
8✔
94
import { PrivateFormError } from '../form.errors'
8✔
95
import * as FormService from '../form.service'
8✔
96
import { getSubmissionType } from '../form.utils'
8✔
97

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

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

122
const logger = createLoggerWithLabel(module)
8✔
123

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

612
  // Step 3: Has permissions, continue to retrieve submission counts.
613
  return formResult
13✔
614
    .map(({ responseMode }) => getSubmissionType(responseMode))
13✔
615
    .asyncAndThen((submissionType) =>
616
      SubmissionService.getFormSubmissionsCount({
13✔
617
        formId,
618
        dateRange,
619
        submissionType, // RATIONALE: For storage mode forms converted from email mode, only count encrypt mode submissions
620
      }),
621
    )
622
    .map((count) => res.json(count))
11✔
623
    .mapErr((error) => {
624
      logger.error({
2✔
625
        message: 'Error retrieving form submission count',
626
        meta: {
627
          action: 'handleCountFormSubmissions',
628
          ...createReqMeta(req),
629
          userId: sessionUserId,
630
          formId,
631
        },
632
        error,
633
      })
634
      const { errorMessage, statusCode } = mapRouteError(error)
2✔
635
      return res.status(statusCode).json({ message: errorMessage })
2✔
636
    })
637
}
638

639
// Handler for GET /admin/forms/:formId/submissions/count
640
export const handleCountFormSubmissions = [
8✔
641
  validateDateRange,
642
  countFormSubmissions,
643
] as ControllerHandler[]
644

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

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

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

712
  // Step 1: Retrieve currently logged in user.
713
  const hasReadPermissionResult = await UserService.getPopulatedUserById(
12✔
714
    sessionUserId,
715
  ).andThen((user) =>
716
    // Step 2: Check whether user has read permissions to form
717
    AuthService.getFormAfterPermissionChecks({
10✔
718
      user,
719
      formId,
720
      level: PermissionLevel.Read,
721
    }),
722
  )
723

724
  const logMeta = {
12✔
725
    action: 'handleStreamFormFeedback',
726
    ...createReqMeta(req),
727
    userId: sessionUserId,
728
    formId,
729
  }
730

731
  if (hasReadPermissionResult.isErr()) {
12✔
732
    logger.error({
9✔
733
      message: 'Error occurred whilst verifying user permissions',
734
      meta: logMeta,
735
      error: hasReadPermissionResult.error,
736
    })
737
    const { errorMessage, statusCode } = mapRouteError(
9✔
738
      hasReadPermissionResult.error,
739
    )
740
    return res.status(statusCode).json({ message: errorMessage })
9✔
741
  }
742

743
  // No errors, start stream.
744
  const cursor = FeedbackService.getFormFeedbackStream(formId)
3✔
745

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

785
      return res.end()
2✔
786
    })
787
}
788

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

807
  return UserService.getPopulatedUserById(sessionUserId)
16✔
808
    .andThen((user) =>
809
      AuthService.getFormAfterPermissionChecks({
12✔
810
        user,
811
        formId,
812
        level: PermissionLevel.Read,
813
      }),
814
    )
815
    .andThen(() => FeedbackService.getFormFeedbacks(formId))
5✔
816
    .map((fbResponse) => res.json(fbResponse))
3✔
817
    .mapErr((error) => {
818
      logger.error({
13✔
819
        message: 'Error retrieving form feedbacks',
820
        meta: {
821
          action: 'handleGetFormFeedback',
822
          ...createReqMeta(req),
823
          userId: sessionUserId,
824
          formId,
825
        },
826
        error,
827
      })
828
      const { errorMessage, statusCode } = mapRouteError(error)
13✔
829
      return res.status(statusCode).json({ message: errorMessage })
13✔
830
    })
831
}
832

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

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

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

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

964
export const handleDuplicateAdminForm = [
8✔
965
  duplicateFormValidator,
966
  duplicateAdminForm,
967
] as ControllerHandler[]
968

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

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

1010
        // Specialized error response for PrivateFormError.
1011
        if (error instanceof PrivateFormError) {
4✔
1012
          return res.status(statusCode).json({
1✔
1013
            message: error.message,
1014
            // Flag to prevent default 404 subtext ("please check link") from
1015
            // showing.
1016
            isPageFound: true,
1017
            formTitle: error.formTitle,
1018
          })
1019
        }
1020
        return res.status(statusCode).json({ message: errorMessage })
3✔
1021
      })
1022
  )
1023
}
1024

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

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

1078
        // Specialized error response for PrivateFormError.
1079
        if (error instanceof PrivateFormError) {
7✔
1080
          return res.status(statusCode).json({
1✔
1081
            message: 'Form must be public to be copied',
1082
          })
1083
        }
1084
        return res.status(statusCode).json({ message: errorMessage })
6✔
1085
      })
1086
  )
1087
}
1088

1089
/**
1090
 * Handler for POST /admin/forms/all-transfer-owner.
1091
 * @security session
1092
 *
1093
 * @returns 200 with true if transfer was successful
1094
 * @returns 400 when new owner is not in the database yet
1095
 * @returns 400 when new owner is already current owner
1096
 * @returns 422 when user in session cannot be retrieved from the database
1097
 * @returns 500 when database error occurs
1098
 */
1099
export const transferAllFormsOwnership: ControllerHandler<
8✔
1100
  unknown,
1101
  unknown,
1102
  { email: string }
1103
> = (req, res) => {
8✔
1104
  const { email: newOwnerEmail } = req.body
11✔
1105
  const sessionUserId = (req.session as AuthedSessionData).user._id
11✔
1106

1107
  return (
11✔
1108
    // Step 1: Retrieve currently logged in user.
1109
    UserService.getPopulatedUserById(sessionUserId)
1110
      .andThen((user) =>
1111
        // Step 2: Transfer all forms to new owner
1112
        AdminFormService.transferAllFormsOwnership(user, newOwnerEmail),
8✔
1113
      )
1114
      .map((data) => {
1115
        return res.status(StatusCodes.OK).json(data)
2✔
1116
      })
1117
      // Some error occurred earlier in the chain.
1118
      .mapErr((error) => {
1119
        logger.error({
9✔
1120
          message: 'Error occurred whilst transferring all forms ownership',
1121
          meta: {
1122
            action: 'transferAllFormsOwnership',
1123
            ...createReqMeta(req),
1124
            userId: sessionUserId,
1125
            newOwnerEmail,
1126
          },
1127
          error,
1128
        })
1129
        const { errorMessage, statusCode } = mapRouteError(error)
9✔
1130
        return res.status(statusCode).json({ message: errorMessage })
9✔
1131
      })
1132
  )
1133
}
1134

1135
export const handleTransferAllFormsOwnership = [
8✔
1136
  transferFormOwnershipValidator,
1137
  transferAllFormsOwnership,
1138
] as ControllerHandler[]
1139

1140
/**
1141
 * Handler for POST /{formId}/adminform/transfer-owner.
1142
 * @security session
1143
 *
1144
 * @returns 200 with updated form with transferred owners
1145
 * @returns 400 when new owner is not in the database yet
1146
 * @returns 400 when new owner is already current owner
1147
 * @returns 403 when user is not the current owner of the form
1148
 * @returns 404 when form cannot be found
1149
 * @returns 410 when form is archived
1150
 * @returns 422 when user in session cannot be retrieved from the database
1151
 * @returns 500 when database error occurs
1152
 */
1153
export const transferFormOwnership: ControllerHandler<
8✔
1154
  { formId: string },
1155
  unknown,
1156
  { email: string }
1157
> = (req, res) => {
8✔
1158
  const { formId } = req.params
16✔
1159
  const { email: newOwnerEmail } = req.body
16✔
1160
  const sessionUserId = (req.session as AuthedSessionData).user._id
16✔
1161

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

1198
export const handleTransferFormOwnership = [
8✔
1199
  transferFormOwnershipValidator,
1200
  transferFormOwnership,
1201
] as ControllerHandler[]
1202

1203
/**
1204
 * Handler for POST /adminform.
1205
 * @security session
1206
 *
1207
 * @returns 200 with newly created form
1208
 * @returns 409 when a database conflict error occurs
1209
 * @returns 413 when payload for created form exceeds size limit
1210
 * @returns 422 when user of given id cannnot be found in the database, or when form parameters are invalid
1211
 * @returns 500 when database error occurs
1212
 */
1213
export const createForm: ControllerHandler<
8✔
1214
  unknown,
1215
  DeserializeTransform<FormDto> | ErrorDto,
1216
  { form: CreateFormBodyDto }
1217
> = async (req, res) => {
15✔
1218
  const {
1219
    form: { workspaceId, ...formParams },
15✔
1220
  } = req.body
15✔
1221
  const sessionUserId = (req.session as AuthedSessionData).user._id
15✔
1222

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

1257
export const handleCreateForm = [
8✔
1258
  createFormValidator,
1259
  createForm,
1260
] as ControllerHandler[]
1261

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

1286
  // Step 1: Retrieve currently logged in user.
1287
  return UserService.getPopulatedUserById(sessionUserId)
11✔
1288
    .andThen((user) =>
1289
      // Step 2: Retrieve form with write permission check.
1290
      AuthService.getFormAfterPermissionChecks({
10✔
1291
        user,
1292
        formId,
1293
        level: PermissionLevel.Write,
1294
      }),
1295
    )
1296
    .andThen((retrievedForm) => {
1297
      // Step 3: Update form or form fields depending on form update parameters
1298
      // passed in.
1299
      const { editFormField } = formUpdateParams
7✔
1300

1301
      // Use different service function depending on type of form update.
1302
      const updateFormResult: ResultAsync<
1303
        IPopulatedForm,
1304
        | EditFieldError
1305
        | DatabaseError
1306
        | DatabaseValidationError
1307
        | DatabaseConflictError
1308
        | DatabasePayloadSizeError
1309
      > = editFormField
7✔
1310
        ? AdminFormService.editFormFields(retrievedForm, editFormField)
1311
        : AdminFormService.updateForm(retrievedForm, formUpdateParams)
1312

1313
      return updateFormResult
7✔
1314
    })
1315
    .map((updatedForm) => res.status(StatusCodes.OK).json(updatedForm))
2✔
1316
    .mapErr((error) => {
1317
      logger.error({
9✔
1318
        message: 'Error occurred when updating form',
1319
        meta: {
1320
          action: 'handleUpdateForm',
1321
          ...createReqMeta(req),
1322
          userId: sessionUserId,
1323
          formId,
1324
          formUpdateParams,
1325
        },
1326
        error,
1327
      })
1328
      const { errorMessage, statusCode } = mapRouteError(error)
9✔
1329
      return res.status(statusCode).json({ message: errorMessage })
9✔
1330
    })
1331
}
1332

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

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

1386
export const _handleUpdateSettings: ControllerHandler<
8✔
1387
  { formId: string },
1388
  FormSettings | ErrorDto,
1389
  SettingsUpdateDto
1390
> = (req, res) => {
8✔
1391
  const { formId } = req.params
17✔
1392
  const sessionUserId = (req.session as AuthedSessionData).user._id
17✔
1393
  const settingsToPatch = req.body
17✔
1394

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

1426
/**
1427
 * Handler for PATCH /forms/:formId/settings.
1428
 * @security session
1429
 *
1430
 * @returns 200 with updated form settings
1431
 * @returns 400 when body is malformed
1432
 * @returns 403 when current user does not have permissions to update form settings
1433
 * @returns 404 when form to update settings for cannot be found
1434
 * @returns 409 when saving form settings incurs a conflict in the database
1435
 * @returns 410 when updating settings for archived form
1436
 * @returns 413 when updating settings causes form to be too large to be saved in the database
1437
 * @returns 422 when an invalid settings update is attempted on the form
1438
 * @returns 422 when user in session cannot be retrieved from the database
1439
 * @returns 500 when database error occurs
1440
 */
1441
export const handleUpdateSettings = [
8✔
1442
  updateSettingsValidator,
1443
  _handleUpdateSettings,
1444
] as ControllerHandler[]
1445

1446
export const _handleUpdateWebhookSettings: ControllerHandler<
8✔
1447
  { formId: string },
1448
  FormWebhookSettings | ErrorDto,
1449
  WebhookSettingsUpdateDto
1450
> = (req, res) => {
8✔
1451
  const { formId } = req.params
×
1452
  const { userEmail, webhook: webhookSettings } = req.body
×
1453
  const authedUserId = (req.session as AuthedSessionData).user._id
×
1454

1455
  logger.info({
×
1456
    message: 'User attempting to update webhook settings',
1457
    meta: {
1458
      action: '_handleUpdateWebhookSettings',
1459
      ...createReqMeta(req),
1460
      reqBody: req.body,
1461
      formId,
1462
      userEmail,
1463
      webhookSettings,
1464
    },
1465
  })
1466

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

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

1523
export const _handleCreateWorkflowStep: ControllerHandler<
8✔
1524
  { formId: string },
1525
  FormWorkflowDto | ErrorDto,
1526
  FormWorkflowStepDto
1527
> = (req, res) => {
8✔
1528
  const { formId } = req.params
×
1529
  const workflowStepToCreate = req.body
×
1530
  const sessionUserId = (req.session as AuthedSessionData).user._id
×
1531

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

1568
export const handleCreateWorkflowStep = [
8✔
1569
  createWorkflowStepValidator,
1570
  _handleCreateWorkflowStep,
1571
]
1572

1573
const _handleUpdateWorkflowStep: ControllerHandler<
1574
  {
1575
    formId: string
1576
    stepNumber: number
1577
  },
1578
  FormWorkflowDto | ErrorDto,
1579
  FormWorkflowStepDto
1580
> = (req, res) => {
8✔
1581
  const { formId, stepNumber } = req.params
×
1582
  const sessionUserId = (req.session as AuthedSessionData).user._id
×
1583
  const updatedWorkflowStep = req.body
×
1584

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

1620
export const handleUpdateWorkflowStep = [
8✔
1621
  updateWorkflowStepValidator,
1622
  _handleUpdateWorkflowStep,
1623
] as ControllerHandler[]
1624

1625
export const handleDeleteWorkflowStep: ControllerHandler<
8✔
1626
  {
1627
    formId: string
1628
    stepNumber: number
1629
  },
1630
  FormWorkflowDto | ErrorDto
1631
> = (req, res) => {
8✔
1632
  const { formId, stepNumber } = req.params
×
1633
  const sessionUserId = (req.session as AuthedSessionData).user._id
×
1634

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

1671
const LIMIT_IN_KB = 250
8✔
1672
const STRING_MAX_LENGTH = LIMIT_IN_KB * KB
8✔
1673
const _handleUpdateWhitelistSettingValidator = celebrate({
8✔
1674
  [Segments.PARAMS]: Joi.object({
1675
    formId: Joi.string()
1676
      .required()
1677
      .pattern(/^[a-fA-F0-9]{24}$/)
1678
      .message('Your form ID is invalid.'),
1679
  }),
1680
  [Segments.BODY]: Joi.object({
1681
    whitelistCsvString: Joi.string()
1682
      .allow(null) // for removal of whitelist
1683
      .max(STRING_MAX_LENGTH)
1684
      .pattern(/^[a-zA-Z0-9,\r\n]+$/)
1685
      .messages({
1686
        'string.empty': 'Your csv is empty.',
1687
        'string.pattern.base': 'Your csv has one or more invalid characters.',
1688
        'string.max': `You have exceeded the file size limit, please upload a file below ${LIMIT_IN_KB} kB.`,
1689
      }),
1690
  }),
1691
})
1692

1693
const _parseWhitelistCsvString = (whitelistCsvString: string | null) => {
8✔
1694
  if (!whitelistCsvString) {
3✔
1695
    return null
1✔
1696
  }
1697
  return whitelistCsvString.split(',').map((entry: string) => entry.trim())
6✔
1698
}
1699

1700
const _handleUpdateWhitelistSetting: ControllerHandler<
1701
  { formId: string },
1702
  object,
1703
  { whitelistCsvString: string | null }
1704
> = async (req, res) => {
8✔
1705
  const { formId } = req.params
4✔
1706
  const sessionUserId = (req.session as AuthedSessionData).user._id
4✔
1707

1708
  const logMeta = {
4✔
1709
    action: '_handleUpdateWhitelistSetting',
1710
    ...createReqMeta(req),
1711
    userId: sessionUserId,
1712
    formId,
1713
  }
1714

1715
  // Step 1: Retrieve form only if currently logged in user has write permissions for form.
1716
  const formResult = await UserService.getPopulatedUserById(
4✔
1717
    sessionUserId,
1718
  ).andThen((user) =>
1719
    AuthService.getFormAfterPermissionChecks({
4✔
1720
      user,
1721
      formId,
1722
      level: PermissionLevel.Write,
1723
    }),
1724
  )
1725

1726
  if (formResult.isErr()) {
4✔
1727
    const { error } = formResult
1✔
1728
    logger.error({
1✔
1729
      message: 'Error occurred when updating form settings',
1730
      meta: logMeta,
1731
      error,
1732
    })
1733
    const { errorMessage, statusCode } = mapRouteError(error)
1✔
1734
    return res.status(statusCode).json({ message: errorMessage })
1✔
1735
  }
1736

1737
  const form = formResult.value
3✔
1738

1739
  const { whitelistCsvString } = req.body
3✔
1740
  const whitelistedSubmitterIds = _parseWhitelistCsvString(whitelistCsvString)
3✔
1741

1742
  const upperCaseWhitelistedSubmitterIds =
1743
    whitelistedSubmitterIds && whitelistedSubmitterIds.length > 0
3✔
1744
      ? whitelistedSubmitterIds.map((id) => id.toUpperCase())
6✔
1745
      : null
1746

1747
  // Step 2: perform validation on submitted whitelist setting
1748
  const isWhitelistSettingValid = AdminFormService.checkIsWhitelistSettingValid(
3✔
1749
    upperCaseWhitelistedSubmitterIds,
1750
  )
1751
  if (!isWhitelistSettingValid.isValid) {
3!
1752
    logger.error({
×
1753
      message: 'Invalid whitelist setting',
1754
      meta: logMeta,
1755
    })
1756
    return res.status(StatusCodes.UNPROCESSABLE_ENTITY).json({
×
1757
      message: isWhitelistSettingValid.invalidReason,
1758
    })
1759
  }
1760

1761
  // Step 3: Encrypt whitelist settings
1762
  if (!form.publicKey) {
3!
1763
    logger.error({
×
1764
      message: 'Form does not have a public key',
1765
      meta: logMeta,
1766
    })
1767
    return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
×
1768
      message: 'Form does not have a public key',
1769
    })
1770
  }
1771
  const formPublicKey = form.publicKey
3✔
1772
  const encryptedWhitelistSubmitterIdsContent = upperCaseWhitelistedSubmitterIds
3✔
1773
    ? encryptStringsMessage(upperCaseWhitelistedSubmitterIds, formPublicKey)
1774
    : null
1775

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

1799
export const _handleUpdateWhitelistSettingForTest =
8✔
1800
  _handleUpdateWhitelistSetting
1801

1802
export const handleUpdateWhitelistSetting = [
8✔
1803
  _handleUpdateWhitelistSettingValidator,
1804
  _handleUpdateWhitelistSetting,
1805
] as ControllerHandler[]
1806

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

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

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

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

1904
export const handleGetWhitelistSetting: ControllerHandler<
8✔
1905
  {
1906
    formId: string
1907
  },
1908
  | {
1909
      encryptedWhitelistedSubmitterIds: EncryptedStringsMessageContent | null
1910
    }
1911
  | ErrorDto
1912
> = (req, res) => {
8✔
1913
  const { formId } = req.params
2✔
1914
  const sessionUserId = (req.session as AuthedSessionData).user._id
2✔
1915

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

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

1968
  const logMeta = {
×
1969
    action: 'handleGetWebhookSettings',
1970
    ...createReqMeta(req),
1971
    userEmail,
1972
    formId,
1973
  }
1974

1975
  logger.info({
×
1976
    message: 'User attempting to get webhook settings',
1977
    meta: logMeta,
1978
  })
1979

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

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

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

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

2087
export const handleEncryptPreviewSubmission = [
8✔
2088
  submitEncryptPreview,
2089
] as ControllerHandler[]
2090

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

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

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

2159
  // Handle SingPass, CorpPass and MyInfo authentication and validation
2160
  const { authType } = form
10✔
2161
  if (authType === FormAuthType.SP || authType === FormAuthType.MyInfo) {
10!
2162
    parsedResponses.addNdiResponses({
×
2163
      authType,
2164
      uinFin: PREVIEW_SINGPASS_UINFIN,
2165
    })
2166
  } else if (authType === FormAuthType.CP) {
10!
2167
    parsedResponses.addNdiResponses({
×
2168
      authType,
2169
      uinFin: PREVIEW_CORPPASS_UINFIN,
2170
      userInfo: PREVIEW_CORPPASS_UID,
2171
    })
2172
  }
2173

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

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

2211
  // Don't await on email confirmations, so submission is successful even if
2212
  // this fails
2213
  void SubmissionService.sendEmailConfirmations({
8✔
2214
    form,
2215
    submission,
2216
    attachments,
2217
    responsesData: emailData.autoReplyData,
2218
    recipientData: extractEmailConfirmationData(
2219
      parsedResponses.getAllResponses(),
2220
      form.form_fields,
2221
    ),
2222
    isUseLambdaOutput: false, // TODO: [PDF-LAMBDA-GENERATION]: To remove once pdf lambda rollout is complete. Currently set to false since v2 api is not being used.
2223
  }).mapErr((error) => {
2224
    logger.error({
1✔
2225
      message: 'Error while sending email confirmations',
2226
      meta: logMeta,
2227
      error,
2228
    })
2229
  })
2230

2231
  return res.json({
8✔
2232
    message: 'Form submission successful.',
2233
    submissionId: submission.id,
2234
  })
2235
}
2236

2237
export const handleEmailPreviewSubmission = [
8✔
2238
  ReceiverMiddleware.receiveEmailSubmission,
2239
  EmailSubmissionMiddleware.validateResponseParams,
2240
  submitEmailPreview,
2241
] as ControllerHandler[]
2242

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2819
/**
2820
 * Handler for POST /forms/:formId/fields/delete
2821
 * @security session
2822
 *
2823
 * @returns 204 when deletion is successful
2824
 * @returns 403 when current user does not have permissions to delete form fields
2825
 * @returns 404 when form cannot be found
2826
 * @returns 410 when deleting fields of an archived form
2827
 * @returns 422 when user in session cannot be retrieved from the database
2828
 * @returns 500 when database error occurs during deletion
2829
 */
2830
export const handleDeleteFormFields: ControllerHandler<
8✔
2831
  { formId: string },
2832
  { message: string } | ErrorDto,
2833
  { fieldIds: string[] }
2834
> = (req, res) => {
8✔
2835
  const { formId } = req.params
×
2836
  const { fieldIds } = req.body
×
2837
  const sessionUserId = (req.session as AuthedSessionData).user._id
×
2838

2839
  return (
×
2840
    // Step 1: Retrieve currently logged in user.
2841
    UserService.getPopulatedUserById(sessionUserId)
2842
      .andThen((user) =>
2843
        // Step 2: Retrieve form with write permission check.
2844
        AuthService.getFormAfterPermissionChecks({
×
2845
          user,
2846
          formId,
2847
          level: PermissionLevel.Write,
2848
        }),
2849
      )
2850
      // Step 3: Delete form fields.
2851
      .andThen((form) => AdminFormService.deleteFormFields(form, fieldIds))
×
2852
      .map(() => res.sendStatus(StatusCodes.NO_CONTENT))
×
2853
      .mapErr((error) => {
2854
        logger.error({
×
2855
          message: 'Error occurred when deleting form fields',
2856
          meta: {
2857
            action: 'handleDeleteFormFields',
2858
            ...createReqMeta(req),
2859
            userId: sessionUserId,
2860
            formId,
2861
            fieldIds,
2862
          },
2863
          error,
2864
        })
2865
        const { errorMessage, statusCode } = mapRouteError(error)
×
2866
        return res.status(statusCode).json({ message: errorMessage })
×
2867
      })
2868
  )
2869
}
2870

2871
/**
2872
 * NOTE: Exported for testing.
2873
 * Private handler for PUT /forms/:formId/end-page
2874
 * @precondition Must be preceded by request validation
2875
 * @security session
2876
 *
2877
 * @returns 200 with updated end page
2878
 * @returns 400 when end page form field has invalid updates to be performed
2879
 * @returns 403 when current user does not have permissions to update the end page
2880
 * @returns 404 when form cannot be found
2881
 * @returns 410 when updating the end page for an archived form
2882
 * @returns 422 when user in session cannot be retrieved from the database
2883
 * @returns 500 when database error occurs
2884
 */
2885
export const _handleUpdateEndPage: ControllerHandler<
8✔
2886
  { formId: string },
2887
  IFormDocument['endPage'] | ErrorDto,
2888
  EndPageUpdateDto
2889
> = (req, res) => {
8✔
2890
  const { formId } = req.params
12✔
2891
  const sessionUserId = (req.session as AuthedSessionData).user._id
12✔
2892

2893
  // Step 1: Retrieve currently logged in user.
2894
  return (
12✔
2895
    UserService.getPopulatedUserById(sessionUserId)
2896
      .andThen((user) =>
2897
        // Step 2: Retrieve form with write permission check.
2898
        AuthService.getFormAfterPermissionChecks({
10✔
2899
          user,
2900
          formId,
2901
          level: PermissionLevel.Write,
2902
        }),
2903
      )
2904
      // Step 3: User has permissions, proceed to allow updating of end page
2905
      .andThen(() => AdminFormService.updateEndPage(formId, req.body))
6✔
2906
      .map((updatedEndPage) => res.status(StatusCodes.OK).json(updatedEndPage))
3✔
2907
      .mapErr((error) => {
2908
        logger.error({
9✔
2909
          message: 'Error occurred when updating end page',
2910
          meta: {
2911
            action: '_handleUpdateEndPage',
2912
            ...createReqMeta(req),
2913
            userId: sessionUserId,
2914
            formId,
2915
            body: req.body,
2916
          },
2917
          error,
2918
        })
2919
        const { errorMessage, statusCode } = mapRouteError(error)
9✔
2920
        return res.status(statusCode).json({ message: errorMessage })
9✔
2921
      })
2922
  )
2923
}
2924

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

2966
/**
2967
 * Handler for GET /admin/forms/:formId/fields/:fieldId
2968
 * @security session
2969
 *
2970
 * @returns 200 with form field when retrieval is successful
2971
 * @returns 403 when current user does not have permissions to retrieve form field
2972
 * @returns 404 when form cannot be found
2973
 * @returns 404 when form field cannot be found
2974
 * @returns 410 when retrieving form field of an archived form
2975
 * @returns 422 when user in session cannot be retrieved from the database
2976
 * @returns 500 when database error occurs
2977
 */
2978
export const handleGetFormField: ControllerHandler<
8✔
2979
  {
2980
    formId: string
2981
    fieldId: string
2982
  },
2983
  ErrorDto | FormFieldDto
2984
> = (req, res) => {
8✔
2985
  const { formId, fieldId } = req.params
14✔
2986
  const sessionUserId = (req.session as AuthedSessionData).user._id
14✔
2987

2988
  return (
14✔
2989
    // Step 1: Retrieve currently logged in user.
2990
    UserService.getPopulatedUserById(sessionUserId)
2991
      .andThen((user) =>
2992
        // Step 2: Retrieve form with read permission check.
2993
        AuthService.getFormAfterPermissionChecks({
11✔
2994
          user,
2995
          formId,
2996
          level: PermissionLevel.Read,
2997
        }),
2998
      )
2999
      .andThen((form) => AdminFormService.getFormField(form, fieldId))
5✔
3000
      .map((formField) =>
3001
        res.status(StatusCodes.OK).json(formField as FormFieldDto),
2✔
3002
      )
3003
      .mapErr((error) => {
3004
        logger.error({
12✔
3005
          message: 'Error occurred when retrieving form field',
3006
          meta: {
3007
            action: 'handleGetFormField',
3008
            ...createReqMeta(req),
3009
            userId: sessionUserId,
3010
            formId,
3011
            fieldId,
3012
          },
3013
          error,
3014
        })
3015
        const { errorMessage, statusCode } = mapRouteError(error)
12✔
3016
        return res.status(statusCode).json({ message: errorMessage })
12✔
3017
      })
3018
  )
3019
}
3020

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

3077
/**
3078
 * Handler for PUT /api/v3/admin/forms/:formId/collaborators
3079
 */
3080
export const handleUpdateCollaborators = [
8✔
3081
  celebrate({
3082
    [Segments.BODY]: Joi.array().items(
3083
      Joi.object({
3084
        email: Joi.string()
3085
          .required()
3086
          .email()
3087
          .message('Please enter a valid email')
3088
          .lowercase(),
3089
        write: Joi.bool().optional(),
3090
        _id: Joi.string().optional(),
3091
      }),
3092
    ),
3093
  }),
3094
  _handleUpdateCollaborators,
3095
] as ControllerHandler[]
3096

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

3159
/**
3160
 * NOTE: Exported for testing.
3161
 * Private handler for PUT /forms/:formId/start-page
3162
 * @precondition Must be preceded by request validation
3163
 * @security session
3164
 *
3165
 * @returns 200 with updated start page
3166
 * @returns 403 when current user does not have permissions to update the start page
3167
 * @returns 404 when form cannot be found
3168
 * @returns 410 when updating the start page for an archived form
3169
 * @returns 422 when user in session cannot be retrieved from the database
3170
 * @returns 500 when database error occurs
3171
 */
3172
export const _handleUpdateStartPage: ControllerHandler<
8✔
3173
  { formId: string },
3174
  IFormDocument['startPage'] | ErrorDto,
3175
  StartPageUpdateDto
3176
> = (req, res) => {
8✔
3177
  const { formId } = req.params
16✔
3178
  const sessionUserId = (req.session as AuthedSessionData).user._id
16✔
3179

3180
  // Step 1: Retrieve currently logged in user.
3181
  return (
16✔
3182
    UserService.getPopulatedUserById(sessionUserId)
3183
      .andThen((user) =>
3184
        // Step 2: Retrieve form with write permission check.
3185
        AuthService.getFormAfterPermissionChecks({
13✔
3186
          user,
3187
          formId,
3188
          level: PermissionLevel.Write,
3189
        }),
3190
      )
3191
      // Step 3: User has permissions, proceed to allow updating of start page
3192
      .andThen(() => AdminFormService.updateStartPage(formId, req.body))
6✔
3193
      .map((updatedStartPage) =>
3194
        res.status(StatusCodes.OK).json(updatedStartPage),
2✔
3195
      )
3196
      .mapErr((error) => {
3197
        logger.error({
14✔
3198
          message: 'Error occurred when updating start page',
3199
          meta: {
3200
            action: '_handleUpdateStartPage',
3201
            ...createReqMeta(req),
3202
            userId: sessionUserId,
3203
            formId,
3204
            body: req.body,
3205
          },
3206
          error,
3207
        })
3208
        const { errorMessage, statusCode } = mapRouteError(error)
14✔
3209
        return res.status(statusCode).json({ message: errorMessage })
14✔
3210
      })
3211
  )
3212
}
3213

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

3263
export const handleGetGoLinkSuffix: ControllerHandler<{ formId: string }> = (
8✔
3264
  req,
3265
  res,
3266
) => {
3267
  const { formId } = req.params
×
3268
  const sessionUserId = (req.session as AuthedSessionData).user._id
×
3269

3270
  // Step 1: Get the form after permission checks
3271
  return (
×
3272
    UserService.getPopulatedUserById(sessionUserId)
3273
      .andThen((user) => {
3274
        return AuthService.getFormAfterPermissionChecks({
×
3275
          user,
3276
          formId,
3277
          level: PermissionLevel.Read,
3278
        })
3279
      })
3280
      // Step 2: After permission checks, get the GoGov link suffix
3281
      .andThen(() => {
3282
        return AdminFormService.getGoLinkSuffix(formId)
×
3283
      })
3284
      .map((goLinkSuffix) => res.status(StatusCodes.OK).json(goLinkSuffix))
×
3285
      .mapErr((error) => {
3286
        const { errorMessage, statusCode } = mapRouteError(error)
×
3287
        // Don't log 404 errors as they are expected for most forms
3288
        if (statusCode !== StatusCodes.NOT_FOUND) {
×
3289
          logger.error({
×
3290
            message: 'Error occurred when getting GoGov link suffix',
3291
            meta: {
3292
              action: 'handleGetGoLinkSuffix',
3293
              ...createReqMeta(req),
3294
              userId: sessionUserId,
3295
              formId,
3296
            },
3297
            error,
3298
          })
3299
        }
3300
        return res.status(statusCode).json({ message: errorMessage })
×
3301
      })
3302
  )
3303
}
3304

3305
export const handleSetGoLinkSuffix: ControllerHandler<
8✔
3306
  { formId: string },
3307
  unknown,
3308
  { linkSuffix: string; adminEmail: string }
3309
> = (req, res) => {
8✔
3310
  const goGovBaseUrl = goGovConfig.goGovBaseUrl
×
3311
  const { formId } = req.params
×
3312
  const { linkSuffix, adminEmail } = req.body
×
3313
  const sessionUserId = (req.session as AuthedSessionData).user._id
×
3314

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

3349
            return new GoGovServerError()
×
3350
          },
3351
        )
3352
      })
3353
      // Step 3: After obtaining GoGov link, save it to the form
3354
      .andThen(() => AdminFormService.setGoLinkSuffix(formId, linkSuffix))
×
3355
      .map(() => res.sendStatus(StatusCodes.OK))
×
3356
      .mapErr((error) => {
3357
        logger.error({
×
3358
          message: 'Error occurred when setting GoGov link suffix',
3359
          meta: {
3360
            action: 'handleSetGoLinkSuffix',
3361
            ...createReqMeta(req),
3362
            userId: sessionUserId,
3363
            formId,
3364
          },
3365
          error,
3366
        })
3367
        const { errorMessage, statusCode } = mapRouteError(error)
×
3368
        return res.status(statusCode).json({ message: errorMessage })
×
3369
      })
3370
  )
3371
}
3372

3373
export const handleConvertEmailToStorageMode: ControllerHandler<
8✔
3374
  { formId: string },
3375
  unknown,
3376
  { publicKey: string }
3377
> = (req, res) => {
8✔
3378
  const { formId } = req.params
2✔
3379
  const { publicKey } = req.body
2✔
3380
  const sessionUserId = (req.session as AuthedSessionData).user._id
2✔
3381

3382
  return UserService.getPopulatedUserById(sessionUserId)
2✔
3383
    .andThen((user) => {
3384
      return AuthService.getFormAfterPermissionChecks({
2✔
3385
        user,
3386
        formId,
3387
        level: PermissionLevel.Write,
3388
      })
3389
    })
3390
    .andThen((form) => EmailSubmissionService.checkFormIsEmailMode(form))
2✔
3391
    .map((form) => {
3392
      return AdminFormService.convertEmailToStorageMode({ form, publicKey })
1✔
3393
    })
3394
    .map(() => {
3395
      logger.info({
1✔
3396
        message: 'Form successfully converted to storage mode',
3397
        meta: {
3398
          action: 'handleConvertEmailToStorageMode',
3399
          ...createReqMeta(req),
3400
          userId: sessionUserId,
3401
          formId,
3402
        },
3403
      })
3404
      return res.sendStatus(StatusCodes.OK)
1✔
3405
    })
3406
    .mapErr((error) => {
3407
      const { errorMessage, statusCode } = mapRouteError(error)
1✔
3408
      return res.status(statusCode).json({ message: errorMessage })
1✔
3409
    })
3410
}
3411

3412
/**
3413
 * Handler to retrieve the sms counts used by a form's administrator and the sms verifications quota
3414
 * This is the controller for GET /admin/forms/:formId/verified-sms/count
3415
 * @param formId The id of the form to retrieve the sms counts for
3416
 * @returns 200 with sms counts and quota when successful
3417
 * @returns 404 when the formId is not found in the database
3418
 * @returns 500 when a database error occurs during retrieval
3419
 */
3420
export const handleGetSmsCountForFormAdmin: ControllerHandler<
8✔
3421
  {
3422
    formId: string
3423
  },
3424
  ErrorDto | SmsCountsDto
3425
> = (req, res) => {
8✔
3426
  const { formId } = req.params
×
3427
  const logMeta = {
×
3428
    action: 'handleGetSmsCountForFormAdmin',
3429
    ...createReqMeta(req),
3430
    formId,
3431
  }
3432

3433
  // Step 1: Check that the form exists
3434
  return (
×
3435
    FormService.retrieveFormById(formId)
3436
      // Step 2: Retrieve the current sms count
3437
      .andThen(() => {
3438
        return SmsService.retrieveSmsCounts(formId)
×
3439
      })
3440
      // Step 3: Map/MapErr accordingly
3441
      .map((smsCountForForm) =>
3442
        res.status(StatusCodes.OK).json({
×
3443
          smsCounts: smsCountForForm,
3444
          quota: smsConfig.smsVerificationLimit,
3445
        }),
3446
      )
3447
      .mapErr((error) => {
3448
        logger.error({
×
3449
          message: 'Error while retrieving sms counts for user',
3450
          meta: logMeta,
3451
          error,
3452
        })
3453
        const { statusCode, errorMessage } = mapRouteError(error)
×
3454
        return res.status(statusCode).json({ message: errorMessage })
×
3455
      })
3456
  )
3457
}
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