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

opengovsg / FormSG / 16111031269

07 Jul 2025 07:46AM UTC coverage: 71.937% (-0.1%) from 72.034%
16111031269

push

github

web-flow
feat: mobile btn v3 (#8493)

* Add OTPVerificationTextStack to EditMobile FE

* add OTPVerificationTextCheck im8 texts

* update OTPVerificationTextStack with im8 texts & include contact support link

* OTPVerificationTextStack styling and spacing

* create MobileOTPModalProps

* replace OTP verification messages with standardized components

* replace modal with original smsCountsMdal

* show modal on enabling OTP verification toggle, refactored some texts

* add useSmsQuota to queries

* getSmsQuota to AdminViewForService

* include routes and handler to obtain sms counts & quota on BE

* include config, service and model for sms, updated to be used in control handler

* add sms types

* update query to query for sms counts by formId not by userId

* update FE to query for counts in the db

* add log to sms table for each successful otp message sent

* resolved FE message copy issues

* resolve FE api call to find sms counts and display

* remove local dev mail testing and logging

* added checkFormSmsLimitAndDeactivateForm to formService

* add util function and implement check if form has verifiable form fields

* implement sms threshold check to deactivate form in public form controller

* include formdeactivated mail component

* add formdeactivatednotificationhtml data type

* create sendFormDeactivatedNotification to MailService

* implement formdeactivatednotification sending when sms threshold is reached and form is turned off

* resolve sending formdeactivatednotification mail err to async

* resolved BE lint and error handling for mail sending

* add collaborators to the deactivated email

* void mail service result to return ResultAsync properly

* make logSendSms to return databaseerror

* fix bug for count query to query per form & not entire collection

* add noSmsLimit flag to surpass threshold limit

* resolved checkFormSmsLimitAndDeactivateForm to always return resultasync or asyncerr

* updated form.server.model.spec.ts with noSmsL... (continued)

2926 of 4941 branches covered (59.22%)

Branch coverage included in aggregate %.

92 of 145 new or added lines in 13 files covered. (63.45%)

10560 of 13806 relevant lines covered (76.49%)

48.53 hits per line

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

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

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

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

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

122
const logger = createLoggerWithLabel(module)
8✔
123

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1566
export const handleCreateWorkflowStep = [
8✔
1567
  createWorkflowStepValidator,
1568
  _handleCreateWorkflowStep,
1569
]
1570

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

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

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

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

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

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

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

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

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

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

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

1735
  const form = formResult.value
3✔
1736

1737
  const { whitelistCsvString } = req.body
3✔
1738
  const whitelistedSubmitterIds = _parseWhitelistCsvString(whitelistCsvString)
3✔
1739

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

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

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

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

1797
export const _handleUpdateWhitelistSettingForTest =
8✔
1798
  _handleUpdateWhitelistSetting
1799

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2085
export const handleEncryptPreviewSubmission = [
8✔
2086
  submitEncryptPreview,
2087
] as ControllerHandler[]
2088

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

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

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

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

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

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

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

2228
  return res.json({
8✔
2229
    message: 'Form submission successful.',
2230
    submissionId: submission.id,
2231
  })
2232
}
2233

2234
export const handleEmailPreviewSubmission = [
8✔
2235
  ReceiverMiddleware.receiveEmailSubmission,
2236
  EmailSubmissionMiddleware.validateResponseParams,
2237
  submitEmailPreview,
2238
] as ControllerHandler[]
2239

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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