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

opengovsg / FormSG / 14186560056

01 Apr 2025 03:51AM UTC coverage: 72.179% (-0.3%) from 72.481%
14186560056

push

github

Ken
chore: bump version to v6.191.0

2866 of 4836 branches covered (59.26%)

Branch coverage included in aggregate %.

10306 of 13413 relevant lines covered (76.84%)

44.94 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
  { optionsToRecipientsMap: Record<string, string[]> }
2277
> = (req, res) => {
8✔
2278
  const { formId, fieldId } = req.params
×
2279
  const { optionsToRecipientsMap } = req.body
×
2280
  const sessionUserId = (req.session as AuthedSessionData).user._id
×
2281

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

3243
export const handleGetGoLinkSuffix: ControllerHandler<{ formId: string }> = (
8✔
3244
  req,
3245
  res,
3246
) => {
3247
  const { formId } = req.params
×
3248
  const sessionUserId = (req.session as AuthedSessionData).user._id
×
3249

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

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

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

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

© 2026 Coveralls, Inc