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

opengovsg / FormSG / 14745377544

30 Apr 2025 02:41AM UTC coverage: 72.128% (-0.006%) from 72.134%
14745377544

push

github

GitHub
feat: cond routing v2 (#8248)

2889 of 4876 branches covered (59.25%)

Branch coverage included in aggregate %.

10379 of 13519 relevant lines covered (76.77%)

48.39 hits per line

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

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

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

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

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

118
const logger = createLoggerWithLabel(module)
8✔
119

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

773
      return res.end()
2✔
774
    })
775
}
776

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1554
export const handleCreateWorkflowStep = [
8✔
1555
  createWorkflowStepValidator,
1556
  _handleCreateWorkflowStep,
1557
]
1558

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

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

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

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

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

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

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

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

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

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

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

1723
  const form = formResult.value
3✔
1724

1725
  const { whitelistCsvString } = req.body
3✔
1726
  const whitelistedSubmitterIds = _parseWhitelistCsvString(whitelistCsvString)
3✔
1727

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

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

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

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

1785
export const _handleUpdateWhitelistSettingForTest =
8✔
1786
  _handleUpdateWhitelistSetting
1787

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2073
export const handleEncryptPreviewSubmission = [
8✔
2074
  submitEncryptPreview,
2075
] as ControllerHandler[]
2076

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

3334
            return new GoGovServerError()
×
3335
          },
3336
        )
3337
      })
3338
      // Step 3: After obtaining GoGov link, save it to the form
3339
      .andThen(() => AdminFormService.setGoLinkSuffix(formId, linkSuffix))
×
3340
      .map(() => res.sendStatus(StatusCodes.OK))
×
3341
      .mapErr((error) => {
3342
        logger.error({
×
3343
          message: 'Error occurred when setting GoGov link suffix',
3344
          meta: {
3345
            action: 'handleSetGoLinkSuffix',
3346
            ...createReqMeta(req),
3347
            userId: sessionUserId,
3348
            formId,
3349
          },
3350
          error,
3351
        })
3352
        const { errorMessage, statusCode } = mapRouteError(error)
×
3353
        return res.status(statusCode).json({ message: errorMessage })
×
3354
      })
3355
  )
3356
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc