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

opengovsg / FormSG / 12667305727

08 Jan 2025 09:22AM UTC coverage: 72.402% (+0.005%) from 72.397%
12667305727

push

github

GitHub
fix(deps): bump cookie, cookie-parser, express-session and maildev

2725 of 4594 branches covered (59.32%)

Branch coverage included in aggregate %.

9962 of 12929 relevant lines covered (77.05%)

43.75 hits per line

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

83.95
/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 multer from 'multer'
8✔
9
import { ResultAsync } from 'neverthrow'
8✔
10

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

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

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

118
const logger = createLoggerWithLabel(module)
8✔
119

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

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

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

228
const fileUploadValidator = celebrate({
8✔
229
  [Segments.BODY]: {
230
    fileId: Joi.string().required(),
231
    fileMd5Hash: Joi.string().base64().required(),
232
    fileType: Joi.string()
233
      .valid(...VALID_UPLOAD_FILE_TYPES)
234
      .required(),
235
    isNewClient: Joi.boolean().optional(), // TODO(#4228): isNewClient in param was allowed for backward compatibility after #4213 removed isNewClient flag from frontend. To remove 2 weeks after release.
236
  },
237
})
238

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1659
const TWENTY_MB_IN_BYTES = 20 * 1024 * 1024
8✔
1660
const handleWhitelistSettingMultipartBody = multer({
8✔
1661
  limits: {
1662
    fieldSize: TWENTY_MB_IN_BYTES,
1663
    fields: 1, // only allow csv string field
1664
    files: 0,
1665
  },
1666
})
1667

1668
const _handleUpdateWhitelistSettingValidator = celebrate({
8✔
1669
  [Segments.PARAMS]: {
1670
    formId: Joi.string()
1671
      .required()
1672
      .pattern(/^[a-fA-F0-9]{24}$/)
1673
      .message('Your form ID is invalid.'),
1674
  },
1675
  [Segments.BODY]: {
1676
    whitelistCsvString: Joi.string()
1677
      .pattern(/^[a-zA-Z0-9,\r\n]+$/)
1678
      .messages({
1679
        'string.empty': 'Your csv is empty.',
1680
        'string.pattern.base': 'Your csv has one or more invalid characters.',
1681
      }),
1682
  },
1683
})
1684

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

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

1700
  const logMeta = {
4✔
1701
    action: '_handleUpdateWhitelistSetting',
1702
    ...createReqMeta(req),
1703
    userId: sessionUserId,
1704
    formId,
1705
  }
1706

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

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

1729
  const form = formResult.value
3✔
1730

1731
  const { whitelistCsvString } = req.body
3✔
1732
  const whitelistedSubmitterIds = _parseWhitelistCsvString(whitelistCsvString)
3✔
1733

1734
  const upperCaseWhitelistedSubmitterIds =
1735
    whitelistedSubmitterIds && whitelistedSubmitterIds.length > 0
3✔
1736
      ? whitelistedSubmitterIds.map((id) => id.toUpperCase())
6✔
1737
      : null
1738

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

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

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

1791
export const _handleUpdateWhitelistSettingForTest =
8✔
1792
  _handleUpdateWhitelistSetting
1793

1794
export const handleUpdateWhitelistSetting = [
8✔
1795
  handleWhitelistSettingMultipartBody.none(), // expecting string field
1796
  _handleUpdateWhitelistSettingValidator,
1797
  _handleUpdateWhitelistSetting,
1798
] as ControllerHandler[]
1799

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

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

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

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

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

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

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

1961
  const logMeta = {
×
1962
    action: 'handleGetWebhookSettings',
1963
    ...createReqMeta(req),
1964
    userEmail,
1965
    formId,
1966
  }
1967

1968
  logger.info({
×
1969
    message: 'User attempting to get webhook settings',
1970
    meta: logMeta,
1971
  })
1972

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

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

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

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

2080
export const handleEncryptPreviewSubmission = [
8✔
2081
  submitEncryptPreview,
2082
] as ControllerHandler[]
2083

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

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

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

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

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

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

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

2223
  return res.json({
8✔
2224
    message: 'Form submission successful.',
2225
    submissionId: submission.id,
2226
  })
2227
}
2228

2229
export const handleEmailPreviewSubmission = [
8✔
2230
  ReceiverMiddleware.receiveEmailSubmission,
2231
  EmailSubmissionMiddleware.validateResponseParams,
2232
  submitEmailPreview,
2233
] as ControllerHandler[]
2234

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

3198
export const handleGetGoLinkSuffix: ControllerHandler<{ formId: string }> = (
8✔
3199
  req,
3200
  res,
3201
) => {
3202
  const { formId } = req.params
×
3203
  const sessionUserId = (req.session as AuthedSessionData).user._id
×
3204

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

3240
export const handleSetGoLinkSuffix: ControllerHandler<
8✔
3241
  { formId: string },
3242
  unknown,
3243
  { linkSuffix: string; adminEmail: string }
3244
> = (req, res) => {
8✔
3245
  const goGovBaseUrl = goGovConfig.goGovBaseUrl
×
3246
  const { formId } = req.params
×
3247
  const { linkSuffix, adminEmail } = req.body
×
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.Write,
3258
        })
3259
      })
3260
      // Step 2: After permission checks, try to get GoGov link
3261
      .andThen(() => {
3262
        return ResultAsync.fromPromise(
×
3263
          axios.post(
3264
            `${goGovBaseUrl}/api/v1/admin/urls`,
3265
            {
3266
              longUrl: `${process.env.APP_URL}/${formId}`,
3267
              shortUrl: linkSuffix,
3268
              email: adminEmail,
3269
            },
3270
            {
3271
              headers: {
3272
                Authorization: `Bearer ${goGovConfig.goGovAPIKey}`,
3273
                // Required due to bug introduced in axios 1.2.1: https://github.com/axios/axios/issues/5346
3274
                // TODO: remove when axios is upgraded to 1.2.2
3275
                'Accept-Encoding': 'gzip,deflate,compress',
3276
              },
3277
            },
3278
          ),
3279
          (error) => {
3280
            if (axios.isAxiosError(error)) {
×
3281
              return mapGoGovErrors(error)
×
3282
            }
3283

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