Skip to content

Commit

Permalink
feat(frontend): write FieldVerificationService in TypeScript (#1259)
Browse files Browse the repository at this point in the history
* feat(FieldVfnSvc): rewrite frontend service in TypeScript

* feat: replace usage of Verification module in verifiable-field

with new FieldVerificationService

* feat: replace usage of Verification module in submit-form directive

* fix: make getErrorMessage backwards compatible

axios returns errors as AxiosErrors, will need to extract out the error response text to make it behave the same way as the previous implementation

* feat: remove verification.client.factory file

* ref: convert ITransaction from interface to type and extend empty obj

* feat(VfnCtl): return empty obj if no transaction needs to be created

* fix: frontend to check if transaction data is an empty obj or not

* test: add unit tests for FieldVerificationService

* feat: correct JSDoc for FetchNewTransactionResponse type

* test: fix vfn ctl tests for no transactions needed

* ref(verifiableField): import entire service instead of destructuring

* test: update test description for resetVerifiedField success case

Co-authored-by: Antariksh Mahajan <antarikshmahajan@gmail.com>

* feat: wrap promises with angularjs $q

* ref(FieldVfnSvc): add spacing between jest tests

Co-authored-by: Antariksh Mahajan <antarikshmahajan@gmail.com>
  • Loading branch information
karrui and mantariksh committed Mar 10, 2021
1 parent 9e026f8 commit 72ad2d2
Show file tree
Hide file tree
Showing 11 changed files with 287 additions and 91 deletions.
6 changes: 3 additions & 3 deletions src/app/modules/verification/verification.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { createLoggerWithLabel } from '../../../config/logger'
import { VfnErrors } from '../../../shared/util/verification'

import * as VerificationService from './verification.service'
import { ITransaction } from './verification.types'
import { Transaction } from './verification.types'

const logger = createLoggerWithLabel(module)
/**
Expand All @@ -18,15 +18,15 @@ const logger = createLoggerWithLabel(module)
*/
export const createTransaction: RequestHandler<
Record<string, string>,
ITransaction,
Transaction,
{ formId: string }
> = async (req, res) => {
try {
const { formId } = req.body
const transaction = await VerificationService.createTransaction(formId)
return transaction
? res.status(StatusCodes.CREATED).json(transaction)
: res.sendStatus(StatusCodes.OK)
: res.status(StatusCodes.OK).json({})
} catch (error) {
logger.error({
message: 'Error creating transaction',
Expand Down
4 changes: 2 additions & 2 deletions src/app/modules/verification/verification.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import MailService from '../../services/mail/mail.service'
import { SmsFactory } from '../../services/sms/sms.factory'
import { generateOtp } from '../../utils/otp'

import { ITransaction } from './verification.types'
import { Transaction } from './verification.types'

const Form = getFormModel(mongoose)
const Verification = getVerificationModel(mongoose)
Expand All @@ -38,7 +38,7 @@ const {
*/
export const createTransaction = async (
formId: string,
): Promise<ITransaction | null> => {
): Promise<Transaction | null> => {
const form = await Form.findById(formId)

if (!form) {
Expand Down
10 changes: 6 additions & 4 deletions src/app/modules/verification/verification.types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { IVerificationSchema } from '../../../types'

export interface ITransaction {
transactionId: IVerificationSchema['_id']
expireAt: IVerificationSchema['expireAt']
}
export type Transaction =
| {
transactionId: IVerificationSchema['_id']
expireAt: IVerificationSchema['expireAt']
}
| Record<string, never>
1 change: 0 additions & 1 deletion src/public/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,6 @@ require('./modules/forms/services/attachment.client.service.js')
require('./modules/forms/services/color-themes.client.service.js')
require('./modules/forms/services/rating.client.service.js')
require('./modules/forms/services/betas.client.factory.js')
require('./modules/forms/services/verification.client.factory.js')
require('./modules/forms/services/captcha.client.service.js')
require('./modules/forms/services/mailto.client.factory.js')

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use strict'
const { isEmpty, merge, keys } = require('lodash')
const { isEmpty, merge, keys, get } = require('lodash')
const FieldVerificationService = require('../../../../services/FieldVerificationService')
angular.module('forms').component('verifiableFieldComponent', {
transclude: true,
templateUrl: 'modules/forms/base/componentViews/verifiable-field.html',
Expand All @@ -8,16 +9,11 @@ angular.module('forms').component('verifiableFieldComponent', {
field: '<', // The model that the input field is based on
input: '<',
},
controller: [
'Verification',
'$timeout',
'$interval',
verifiableFieldController,
],
controller: ['$q', '$timeout', '$interval', verifiableFieldController],
controllerAs: 'vm',
})

function verifiableFieldController(Verification, $timeout, $interval) {
function verifiableFieldController($q, $timeout, $interval) {
const vm = this
vm.$onInit = () => {
vm.otp = {
Expand Down Expand Up @@ -50,10 +46,11 @@ function verifiableFieldController(Verification, $timeout, $interval) {
throw new Error('No transaction id')
}

await Verification.getNewOtp(
{ transactionId: vm.transactionId },
{ fieldId: vm.field._id, answer: lastRequested.value },
)
await FieldVerificationService.triggerSendOtp({
transactionId: vm.transactionId,
fieldId: vm.field._id,
answer: lastRequested.value,
})
disableResendButton(DISABLED_SECONDS)
updateView(STATES.VFN_WAITING_FOR_INPUT)
} catch (err) {
Expand Down Expand Up @@ -81,9 +78,12 @@ function verifiableFieldController(Verification, $timeout, $interval) {
return onVerificationFailure()
}

await Verification.verifyOtp(
{ transactionId: vm.transactionId },
{ fieldId: vm.field._id, otp: otp },
$q.resolve(
FieldVerificationService.verifyOtp({
transactionId: vm.transactionId,
fieldId: vm.field._id,
otp,
}),
)
.then(onVerificationSuccess)
.catch(onVerificationFailure)
Expand All @@ -110,10 +110,10 @@ function verifiableFieldController(Verification, $timeout, $interval) {
if (getView() !== STATES.VFN_DEFAULT) {
// We don't await on reset because we don't care if it fails
// The signature will be wrong anyway if it fails, and submission will be prevented
Verification.resetFieldInTransaction(
{ transactionId: vm.transactionId },
{ fieldId: vm.field._id },
)
FieldVerificationService.resetVerifiedField({
transactionId: vm.transactionId,
fieldId: vm.field._id,
})
}
resetDefault()
}
Expand Down Expand Up @@ -244,8 +244,11 @@ function verifiableFieldController(Verification, $timeout, $interval) {
}

const getErrorMessage = (err) => {
// So that switch case works for both axios error objects and string objects.
const error = get(err, 'response.data', err)

let errMessage = ''
switch (err) {
switch (error) {
case 'SEND_OTP_FAILED':
case 'RESEND_OTP':
errMessage = 'Error - try resending the OTP.'
Expand Down
20 changes: 11 additions & 9 deletions src/public/modules/forms/base/directives/submit-form.directive.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
'use strict'
const { cloneDeep } = require('lodash')

const FieldVerificationService = require('../../../../services/FieldVerificationService')
const MyInfoService = require('../../../../services/MyInfoService')
const {
getVisibleFieldIds,
Expand All @@ -23,8 +25,8 @@ const FORM_STATES = {
angular
.module('forms')
.directive('submitFormDirective', [
'$window',
'$q',
'$window',
'GTag',
'SpcpRedirect',
'SpcpSession',
Expand All @@ -34,13 +36,12 @@ angular
'Submissions',
'$uibModal',
'$timeout',
'Verification',
submitFormDirective,
])

function submitFormDirective(
$window,
$q,
$window,
GTag,
SpcpRedirect,
SpcpSession,
Expand All @@ -50,7 +51,6 @@ function submitFormDirective(
Submissions,
$uibModal,
$timeout,
Verification,
) {
return {
restrict: 'E',
Expand Down Expand Up @@ -421,11 +421,13 @@ function submitFormDirective(

// Create a transaction if there are fields to be verified and the form is intended for submission
if (!scope.disableSubmitButton) {
Verification.createTransaction({ formId: scope.form._id }).then(
({ transactionId }) => {
if (transactionId) scope.transactionId = transactionId
},
)
$q.resolve(
FieldVerificationService.createTransactionForForm(scope.form._id),
).then((res) => {
if (res.transactionId) {
scope.transactionId = res.transactionId
}
})
}
},
}
Expand Down
50 changes: 0 additions & 50 deletions src/public/modules/forms/services/verification.client.factory.js

This file was deleted.

100 changes: 100 additions & 0 deletions src/public/services/FieldVerificationService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import axios from 'axios'
import { Opaque } from 'type-fest'

export type JsonDate = Opaque<string, 'JsonDate'>

/**
* Response when retrieving new transaction. Can be an empty object if the
* current form does not have any verifiable fields.
*/
export type FetchNewTransactionResponse =
| { expireAt: JsonDate; transactionId: string }
| Record<string, never>

type VerifiedFieldSignature = Opaque<string, 'VerifiedFieldSignature'>

/** Exported for testing */
export const TRANSACTION_ENDPOINT = '/transaction'

/**
* Create a transaction for given form.
* @param formId The id of the form to create a transaction for
* @returns transaction metadata on success. Can be empty if no transactions are found in the form.
*/
export const createTransactionForForm = async (
formId: string,
): Promise<FetchNewTransactionResponse> => {
return axios
.post<FetchNewTransactionResponse>(TRANSACTION_ENDPOINT, {
formId,
})
.then(({ data }) => data)
}

/**
* Sends an OTP to given answer.
* @param transactionId The generated transaction id for the form
* @param fieldId The id of the verification field
* @param answer The value of the verification field to verify. Usually an email or phone number
* @returns 201 Created status if successfully sent
*/
export const triggerSendOtp = async ({
transactionId,
fieldId,
answer,
}: {
transactionId: string
fieldId: string
answer: string
}): Promise<void> => {
return axios.post(`${TRANSACTION_ENDPOINT}/${transactionId}/otp`, {
fieldId,
answer,
})
}

/**
* Verifies given OTP for given fieldId
* @param transactionId The generated transaction id for the form
* @param fieldId The id of the verification field
* @param otp The user-entered OTP value to verify against
* @returns The verified signature on success
*/
export const verifyOtp = async ({
transactionId,
fieldId,
otp,
}: {
transactionId: string
fieldId: string
otp: string
}): Promise<VerifiedFieldSignature> => {
return axios
.post<VerifiedFieldSignature>(
`${TRANSACTION_ENDPOINT}/${transactionId}/otp/verify`,
{
fieldId,
otp,
},
)
.then(({ data }) => data)
}

/**
* Reset the field in the transaction, removing the previously saved signature.
*
* @param transactionId The generated transaction id for the form
* @param fieldId The id of the verification field to reset
* @returns 200 OK status if successfully reset
*/
export const resetVerifiedField = async ({
transactionId,
fieldId,
}: {
transactionId: string
fieldId: string
}): Promise<void> => {
return axios.post(`${TRANSACTION_ENDPOINT}/${transactionId}/reset`, {
fieldId,
})
}
Loading

0 comments on commit 72ad2d2

Please sign in to comment.