Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement phone verification service #188

Open
wants to merge 54 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
c06cbe2
Create service to handle phone verification
BambanzaJuniorThe2nd Dec 8, 2020
076c5d8
Rename methods sendSms and confirmSms in module VerificationService t…
BambanzaJuniorThe2nd Dec 14, 2020
4b126d2
Rename method confirmVerficationSms to confirmVerificationCode
BambanzaJuniorThe2nd Dec 14, 2020
62a9341
Create error message for when there's no record corresponding to a sp…
BambanzaJuniorThe2nd Dec 14, 2020
e31f4d3
Implement user service method for verifying donor by setting isPhoneV…
BambanzaJuniorThe2nd Dec 14, 2020
9d03e4c
Implement user service method for verifying donor by setting isPhoneV…
BambanzaJuniorThe2nd Dec 14, 2020
83c3ae6
Update sms format for phone verification
BambanzaJuniorThe2nd Dec 14, 2020
ec896a3
Commit master branch changes
BambanzaJuniorThe2nd Dec 15, 2020
94e579b
Commit master branch changes
BambanzaJuniorThe2nd Dec 15, 2020
2605035
Create event emitters and listeners for when a user is created or act…
BambanzaJuniorThe2nd Dec 15, 2020
319f286
implement error handling cases for error codes phoneAlreadyVerfied an…
BambanzaJuniorThe2nd Dec 15, 2020
4cb51bf
Resolve error 'Property getAllBeneficiaries does not exist'
BambanzaJuniorThe2nd Dec 15, 2020
6fc8075
Return corresponding verification record after verifying phone number
BambanzaJuniorThe2nd Dec 16, 2020
e8d9113
Design and implement phone verification page
BambanzaJuniorThe2nd Dec 16, 2020
9733a65
Create a route entry for the verify-phone page
BambanzaJuniorThe2nd Dec 17, 2020
c3383ff
Display phone verification-related error message on page in addition …
BambanzaJuniorThe2nd Dec 17, 2020
d3ebd1f
Redirect user to home page upon clicking Return home button
BambanzaJuniorThe2nd Dec 17, 2020
b532cfc
Use await when invoking bitly link shortener method shortenLink
BambanzaJuniorThe2nd Jan 4, 2021
27cb7cc
Update verified user projection label from VERIFIED_DONOR_PROJECTION …
BambanzaJuniorThe2nd Jan 4, 2021
8fb2b7c
Display appropriate error message on verify-phone page
BambanzaJuniorThe2nd Jan 5, 2021
679acd2
Exclude code when fetching phone verification records
BambanzaJuniorThe2nd Jan 6, 2021
b717950
Create endpoint for fetching specific phone verification record
BambanzaJuniorThe2nd Jan 6, 2021
e21c04c
Update utility method generateId to take in a size parameter
BambanzaJuniorThe2nd Jan 6, 2021
0624abe
Redefine method confirmVerificationCode to take in and id and code
BambanzaJuniorThe2nd Jan 6, 2021
32322ae
Redefine method confirmVerifcationCode to be more error-specific
BambanzaJuniorThe2nd Jan 7, 2021
0e5f2f2
Redefine endpoint for confirming verification code to be of HTTP PUT …
BambanzaJuniorThe2nd Jan 7, 2021
5a9087c
Create a frontend verifications service method to fetch speciclear
BambanzaJuniorThe2nd Jan 7, 2021
32d9d33
Implement logic for submitting phone verification code
BambanzaJuniorThe2nd Jan 7, 2021
920b662
Only pass the phone verification code in the request body when hittin…
BambanzaJuniorThe2nd Jan 8, 2021
a105384
Throw phone already verified error in method getById from service Pho…
BambanzaJuniorThe2nd Jan 8, 2021
9417726
Use base_url in link in getPhoneVerificationLink
BambanzaJuniorThe2nd Jan 8, 2021
c2f10b8
Remove access to state field message from verify-phone view
BambanzaJuniorThe2nd Jan 8, 2021
80af806
Implement new verifications service method for creating phone verific…
BambanzaJuniorThe2nd Jan 11, 2021
62e1be6
Create another a db-specific type that includes field code in phone v…
BambanzaJuniorThe2nd Jan 11, 2021
6e7b917
Implement validators and validation schemas for service phone-verific…
BambanzaJuniorThe2nd Jan 11, 2021
ba3e543
Pass user object in body when creating phone verification record
BambanzaJuniorThe2nd Jan 11, 2021
5ca422c
Pass user phone in body when creating phone verification records
BambanzaJuniorThe2nd Jan 11, 2021
6bac957
Remove event emitter emitUserCreated in User service method create
BambanzaJuniorThe2nd Jan 11, 2021
24de1fc
Remove event emitter emitUserCreated in User service method create
BambanzaJuniorThe2nd Jan 11, 2021
45eda17
Redirect user to verify their phone number after signing up
BambanzaJuniorThe2nd Jan 11, 2021
6efa0e2
Remove event emitter emitUserActivated in User service method activat…
BambanzaJuniorThe2nd Jan 11, 2021
6f6ac31
Pass phone to User-service method create upon hitting endpoint for cr…
BambanzaJuniorThe2nd Jan 11, 2021
1840802
Hide sign-up dialog upon redirection to verify-phone page
BambanzaJuniorThe2nd Jan 11, 2021
cf7213c
Fetch phone verification record only if not available in state
BambanzaJuniorThe2nd Jan 11, 2021
c74011e
Reformat phone verification sms to be shorter
BambanzaJuniorThe2nd Jan 11, 2021
380eb14
Fetch phone verification record only if not available in state
BambanzaJuniorThe2nd Jan 11, 2021
3ce4efa
Implement Phone Verification service method to resend new verificatio…
BambanzaJuniorThe2nd Jan 12, 2021
ef351ae
Create endpoint for requesting resend of phone verification code
BambanzaJuniorThe2nd Jan 12, 2021
a6bce31
Implement action for resending phone verification code
BambanzaJuniorThe2nd Jan 12, 2021
242d6b4
Update confirmVerificationCodeInputSchema to allow code as input
BambanzaJuniorThe2nd Jan 12, 2021
3472666
Throw Invalid phone verification code error if supplied code does not…
BambanzaJuniorThe2nd Jan 13, 2021
012c1cb
Create an invalidPhoneVerificationCode error code
BambanzaJuniorThe2nd Jan 13, 2021
9d6a2d8
Update link in getPhoneVerificationLink
BambanzaJuniorThe2nd Jan 13, 2021
728e902
Commit implementation of Verifications service method resendPhoneVeri…
BambanzaJuniorThe2nd Jan 15, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions server/src/core/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { DonationDistributionService } from './distribution';
import { StatsService } from './stat';
import { DistributionReportService } from './distribution-report';
import { BulkMessageService } from './bulk-messaging';
import { VerificationService } from './phone-verification';

export interface App {
users: UserService;
Expand All @@ -13,6 +14,7 @@ export interface App {
donationDistributions: DonationDistributionService;
stats: StatsService;
distributionReports: DistributionReportService;
phoneVerification: VerificationService;
bulkMessages: BulkMessageService;
};

Expand Down
11 changes: 11 additions & 0 deletions server/src/core/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Statistics } from './stat';
import { DistributionReports } from './distribution-report';
import { Links, BitlyLinkShortener } from './link-generator';
import { BulkMessages, DefaultMessageContextFactory } from './bulk-messaging';
import { PhoneVerification } from './phone-verification';

export async function bootstrap(config: AppConfig): Promise<App> {
const client = await getDbConnection(config.dbUri);
Expand Down Expand Up @@ -66,6 +67,7 @@ export async function bootstrap(config: AppConfig): Promise<App> {
apiKey: config.atApiKey,
sender: config.atSmsSender
});

const emailProvider = new SendGridEmailProvider({
apiKey: config.sendgridApiKey,
emailSender: config.emailSender
Expand Down Expand Up @@ -94,6 +96,13 @@ export async function bootstrap(config: AppConfig): Promise<App> {
links
});

const phoneVerification = new PhoneVerification(db, {
smsProvider,
users,
links,
eventBus
});

const messageContextFactory = new DefaultMessageContextFactory({
baseUrl: config.webappBaseUrl,
linkGenerator: links
Expand All @@ -108,12 +117,14 @@ export async function bootstrap(config: AppConfig): Promise<App> {
await users.createIndexes();
await transactions.createIndexes();
await invitations.createIndexes();
await phoneVerification.createIndexes();

return {
users,
transactions,
invitations,
donationDistributions,
phoneVerification,
stats,
distributionReports,
bulkMessages
Expand Down
15 changes: 15 additions & 0 deletions server/src/core/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export type ErrorCode =
| 'loginFailed'
| 'invalidToken'
| 'resourceNotFound'
| 'phoneVerificationRecordNotFound'
| 'uniquenessFailed'
| 'paymentRequestFailed'
| 'b2cRequestFailed'
Expand All @@ -70,6 +71,8 @@ export type ErrorCode =
| 'messageDeliveryFailed'
| 'emailDeliveryFailed'
| 'linkShorteningFailed'
| 'phoneAlreadyVerified'
| 'invalidPhoneVerificationCode'
/**
* This error should only be thrown when a transaction fails
* because the user's transactions are blocked (based on the transactionsBlockedReason field)
Expand Down Expand Up @@ -180,4 +183,16 @@ export function createTransactionRejectedError(message: string = messages.ERROR_

export function createInsufficientFundsError(message: string) {
return createAppError(message, 'insufficientFunds');
}

export function createPhoneVerificationRecordNotFoundError(message: string) {
return createAppError(message, 'phoneVerificationRecordNotFound');
}

export function createPhoneAlreadyVerifiedError(message: string) {
return createAppError(message, 'phoneAlreadyVerified');
}

export function createInvalidPhoneVerificationCodeError(message: string) {
return createAppError(message, 'invalidPhoneVerificationCode');
}
3 changes: 1 addition & 2 deletions server/src/core/event/event-name.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
const USER_INVITATION_CREATED = 'userInvitationCreated';
const TRANSACTION_COMPLETED = 'transactionCompleted';

export { USER_INVITATION_CREATED };
export { TRANSACTION_COMPLETED };
export { USER_INVITATION_CREATED, TRANSACTION_COMPLETED };
2 changes: 1 addition & 1 deletion server/src/core/invitation/invitation-service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Db, Collection } from 'mongodb';
import { generateId } from '../util';
import { Invitation, DbInvitation, InvitationCreateArgs, InvitationService, InvitationStatus } from './types';
import { createDbOpFailedError, rethrowIfAppError, createResourceNotFoundError, AppError } from '../error';
import { createDbOpFailedError, rethrowIfAppError, createResourceNotFoundError } from '../error';
import * as validators from './validator';
import * as messages from '../messages';

Expand Down
11 changes: 10 additions & 1 deletion server/src/core/link-generator/link-generator-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,18 @@ export class Links implements LinkGeneratorService {
const link = `${this.args.baseUrl}?donate=1&${encodedQuery}`;

if (shorten) {
return this.args.shortener.shortenLink(link);
return await this.args.shortener.shortenLink(link);
}

return link;
}

async getPhoneVerificationLink(id: string, shorten: boolean = true): Promise<string> {
const link: string = `${this.args.baseUrl}/verifications/phone/${id}`;
if (shorten) {
return await this.args.shortener.shortenLink(link);
}

return link;
}
}
4 changes: 4 additions & 0 deletions server/src/core/message/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ export function createMonthlyDistributionReportEmailMessageForOccasionalDonor(do
</p>`;
}

export function createPhoneVerificationSms(code: number, verificationLink: string): string {
return `Social Relief verification code: ${code}.`
}

function beneficiariesAndAmountReceived(beneficiaries: User[], receivedAmount: number[], type: MessageType): string {
if (type === 'sms') {
return beneficiariesAndAmountReceivedForSms(beneficiaries, receivedAmount);
Expand Down
3 changes: 3 additions & 0 deletions server/src/core/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,7 @@ export const ERROR_REFUND_REQUEST_REJECTED = 'Refund request rejected';
export const ERROR_USER_BLOCKED_FROM_TRANSACTIONS = 'User is blocked from making transactions at the moment.';
export const ERROR_NO_BALANCE_FOR_REFUNDS = 'No available balance to request for refund.';
export const ERROR_TRANSACTION_REJECTED = 'Transaction rejected';
export const ERROR_PHONE_VERIFICATION_RECORD_NOT_FOUND = 'Phone verification record not found';
export const ERROR_PHONE_ALREADY_VERIFIED = 'Phone already verified';
export const ERROR_INVALID_PHONE_VERIFICATION_CODE = 'Invalid phone verification code';

2 changes: 2 additions & 0 deletions server/src/core/phone-verification/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { VerificationService } from './types';
export * from './phone-verification-service';
229 changes: 229 additions & 0 deletions server/src/core/phone-verification/phone-verification-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import { Db, Collection } from 'mongodb';
import { generateId, generatePhoneVerificationCode } from '../util';
import { VerificationRecord, VerificationService } from './types';
import { UserService, User } from '../user';
import { SmsProvider } from '../sms';
import { Links } from '../link-generator';
import { createPhoneVerificationSms } from '../message';
import { createDbOpFailedError, rethrowIfAppError,
createPhoneVerificationRecordNotFoundError,
createPhoneAlreadyVerifiedError, isMongoDuplicateKeyError,
createUniquenessFailedError, createInvalidPhoneVerificationCodeError } from '../error';
import * as messages from '../messages';
import { EventBus, Event } from '../event';
import * as validators from './validator';

const COLLECTION = 'phone-verifications';
const SAFE_PHONE_VERIFICATION_RECORD_PROJECTION = {
_id: 1,
phone: 1,
isVerified: 1,
createdAt: 1,
updatedAt: 1
};

export interface PhoneVerificationRecord extends VerificationRecord {
phone: string,
}

export interface DbPhoneVerificationRecord extends PhoneVerificationRecord {
/**
* A 6-digit unique code
*/
code: number,
}

export interface PhoneVerificationArgs {
smsProvider: SmsProvider;
users: UserService;
links: Links;
eventBus: EventBus;
}

export interface PhoneVerificationRecordCreateArgs {
id: string,
code: number;
phone: string;
}

export class PhoneVerification implements VerificationService {
private db: Db;
private collection: Collection<PhoneVerificationRecord>;
private indexesCreated: boolean;
private args: PhoneVerificationArgs;

constructor(db: Db, args: PhoneVerificationArgs) {
this.db = db;
this.collection = this.db.collection(COLLECTION);
this.args = args;
this.indexesCreated = false;
}

async create(phone: string): Promise<PhoneVerificationRecord> {
validators.validatesCreate(phone);
try {
const id = generateId(); // id to be given to the phone verification record
const code = generatePhoneVerificationCode();
await this.sendVerificationSms(phone, id, code);

const now = new Date();
const record: DbPhoneVerificationRecord = {
_id: id,
code,
phone,
isVerified: false,
createdAt: now,
updatedAt: now,
}

const res = await this.collection.insertOne(record);
return res.ops[0];
}
catch (e) {
rethrowIfAppError(e);

if (isMongoDuplicateKeyError(e, phone)) {
throw createUniquenessFailedError(messages.ERROR_PHONE_ALREADY_IN_USE);
}

throw createDbOpFailedError(e.message);
}
}

public async getById(id: string): Promise<PhoneVerificationRecord> {
try {
const record = await this.collection.findOne(
{ _id: id },
{ projection: SAFE_PHONE_VERIFICATION_RECORD_PROJECTION }
);

if (!record) {
throw createPhoneVerificationRecordNotFoundError(messages.ERROR_PHONE_VERIFICATION_RECORD_NOT_FOUND);
}

else if (record.isVerified) {
throw createPhoneAlreadyVerifiedError(messages.ERROR_PHONE_ALREADY_VERIFIED);
}

return record;
}
catch (e) {
rethrowIfAppError(e);
throw createDbOpFailedError(e.message);
}
}

async createIndexes(): Promise<void> {
if (this.indexesCreated) return;

try {
// unique phone index
await this.collection.createIndex({ 'phone': 1 }, { unique: true, sparse: false });

this.indexesCreated = true;
}
catch (e) {
throw createDbOpFailedError(e.message);
}
}

async sendVerificationSms(phone: string, id: string, code: number): Promise<void> {
try {
const link = await this.args.links.getPhoneVerificationLink(id);
const smsMessage = createPhoneVerificationSms(code, link);
await this.args.smsProvider.sendSms(phone, smsMessage);
this.createIndexes();
}
catch(e) {
console.error("Error occured: ", e.message);
rethrowIfAppError(e);
throw createDbOpFailedError(e.message);
}
}

public async confirmVerificationCode(id: string, code: number): Promise<PhoneVerificationRecord> {
validators.validatesConfirmVerificationCode({ recordId: id, code});
try {
let record = await this.collection.findOne(
{ _id: id },
{ projection: SAFE_PHONE_VERIFICATION_RECORD_PROJECTION }
);

if (!record) {
throw createPhoneVerificationRecordNotFoundError(messages.ERROR_PHONE_VERIFICATION_RECORD_NOT_FOUND);
}

else if (record.isVerified) {
throw createPhoneAlreadyVerifiedError(messages.ERROR_PHONE_ALREADY_VERIFIED);
}

else {
record = await this.collection.findOne(
{ _id: id, code },
{ projection: SAFE_PHONE_VERIFICATION_RECORD_PROJECTION }
);

if (!record) {
throw createInvalidPhoneVerificationCodeError(messages.ERROR_INVALID_PHONE_VERIFICATION_CODE);
}

const result = await this.collection.findOneAndUpdate(
{ _id: id },
{
$set: { isVerified: true },
$currentDate: { updatedAt: true },
},
{ upsert: true, returnOriginal: false }
);

const user = await this.args.users.getByPhone(result.value.phone);
await this.args.users.verifyUser(user);
return result.value;
}
}
catch(e) {
console.error("Error occured: ", e.message);
rethrowIfAppError(e);
throw createDbOpFailedError(e.message);
}
}

async resendVerificationCode(id: string): Promise<PhoneVerificationRecord> {
validators.validatesResendVerificationCode(id);
try {
const record = await this.collection.findOne(
{ _id: id, isVerified: false },
{ projection: SAFE_PHONE_VERIFICATION_RECORD_PROJECTION }
);

if (!record) {
throw createPhoneVerificationRecordNotFoundError(messages.ERROR_PHONE_VERIFICATION_RECORD_NOT_FOUND);
}

else if (record.isVerified) {
throw createPhoneAlreadyVerifiedError(messages.ERROR_PHONE_ALREADY_VERIFIED);
}

else {
const newCode = generatePhoneVerificationCode();
await this.sendVerificationSms(record.phone, id, newCode);

const result = await this.collection.findOneAndUpdate(
{ _id: id },
{
$set: { code: newCode },
$currentDate: { updatedAt: true },
},
{ upsert: true, returnOriginal: false }
);

return result.value;
}
}
catch(e) {
console.error("Error occured: ", e.message);
rethrowIfAppError(e);
throw createDbOpFailedError(e.message);
}
}
}
Loading