From c06cbe2acc97f58be840381d8a383aa984b54672 Mon Sep 17 00:00:00 2001 From: Jason Mahirwe Date: Tue, 8 Dec 2020 23:17:16 +0200 Subject: [PATCH 01/53] Create service to handle phone verification --- .../src/core/invitation/invitation-service.ts | 2 +- .../link-generator/link-generator-service.ts | 9 +++ server/src/core/message/message.ts | 4 ++ server/src/core/phone-verification/index.ts | 0 .../phone-verification-service.ts | 69 +++++++++++++++++++ server/src/core/phone-verification/types.ts | 7 ++ 6 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 server/src/core/phone-verification/index.ts create mode 100644 server/src/core/phone-verification/phone-verification-service.ts create mode 100644 server/src/core/phone-verification/types.ts diff --git a/server/src/core/invitation/invitation-service.ts b/server/src/core/invitation/invitation-service.ts index 3f05f842..8e3f0877 100644 --- a/server/src/core/invitation/invitation-service.ts +++ b/server/src/core/invitation/invitation-service.ts @@ -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'; diff --git a/server/src/core/link-generator/link-generator-service.ts b/server/src/core/link-generator/link-generator-service.ts index 1bdb632d..0028a263 100644 --- a/server/src/core/link-generator/link-generator-service.ts +++ b/server/src/core/link-generator/link-generator-service.ts @@ -57,4 +57,13 @@ export class Links implements LinkGeneratorService { return link; } + + async getPhoneVerificationLink(code: string, shorten: boolean = true): Promise { + const link: string = `${this.args.baseUrl}/confirm/phone/${code}`; + if (shorten) { + return this.args.shortener.shortenLink(link); + } + + return link; + } } \ No newline at end of file diff --git a/server/src/core/message/message.ts b/server/src/core/message/message.ts index 575752f5..6bdbb554 100644 --- a/server/src/core/message/message.ts +++ b/server/src/core/message/message.ts @@ -51,6 +51,10 @@ export function createMonthlyDistributionReportEmailMessageForOccasionalDonor(do

`; } +export function createPhoneVerificationSms(user: User, verificationLink: string): string { + return `Hello ${extractFirstName(user.name)}, before you can start making donations, you need to confirm your phone number by clicking ${verificationLink}` +} + function beneficiariesAndAmountReceived(beneficiaries: User[], receivedAmount: number[], type: MessageType): string { if (type === 'sms') { return beneficiariesAndAmountReceivedForSms(beneficiaries, receivedAmount); diff --git a/server/src/core/phone-verification/index.ts b/server/src/core/phone-verification/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/server/src/core/phone-verification/phone-verification-service.ts b/server/src/core/phone-verification/phone-verification-service.ts new file mode 100644 index 00000000..00d1f2ff --- /dev/null +++ b/server/src/core/phone-verification/phone-verification-service.ts @@ -0,0 +1,69 @@ +import { Db, Collection } from 'mongodb'; +import { generateId } from '../util'; +import { VerificationService } from './types'; +import { UserService, User } from '../user'; +import { SmsProvider } from '../sms'; +import { Links } from '../link-generator'; +import { createPhoneVerificationSms } from '../message'; +import { createDbOpFailedError, rethrowIfAppError } from '../error'; + +const COLLECTION = 'phone-verifications'; + +export interface PhoneVerificationRecord { + _id: string, + phone: string, + createdAt: Date, + updatedAt: Date, +} + +export interface PhoneVerificationArgs { + smsProvider: SmsProvider; + users: UserService; + links: Links +} + +export class PhoneVerification implements VerificationService { + private db: Db; + private collection: Collection; + 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 createIndexes(): Promise { + 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 sendSms(user: User): Promise { + try { + const code = generateId(); + const link = await this.args.links.getPhoneVerificationLink(code); + const smsMessage = createPhoneVerificationSms(user, link); + await this.args.smsProvider.sendSms(user.phone, smsMessage); + } + catch(e) { + console.error("Error occured: ", e.message); + rethrowIfAppError(e); + throw createDbOpFailedError(e.message); + } + } + + async confirmSms(): Promise { + // TODO + } +} \ No newline at end of file diff --git a/server/src/core/phone-verification/types.ts b/server/src/core/phone-verification/types.ts new file mode 100644 index 00000000..fd2ec03c --- /dev/null +++ b/server/src/core/phone-verification/types.ts @@ -0,0 +1,7 @@ +import { User } from '../user'; + +export interface VerificationService { + createIndexes(): Promise; + sendSms(user: User): Promise; + confirmSms(): Promise; +} \ No newline at end of file From 076c5d8bd8f0dc04d149de978b961e278894801a Mon Sep 17 00:00:00 2001 From: Jason Mahirwe Date: Mon, 14 Dec 2020 11:06:07 +0200 Subject: [PATCH 02/53] Rename methods sendSms and confirmSms in module VerificationService to sendVerificationSms and confirmVerificationSms, respectiviely --- .../src/core/phone-verification/phone-verification-service.ts | 4 ++-- server/src/core/phone-verification/types.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/server/src/core/phone-verification/phone-verification-service.ts b/server/src/core/phone-verification/phone-verification-service.ts index 00d1f2ff..62943373 100644 --- a/server/src/core/phone-verification/phone-verification-service.ts +++ b/server/src/core/phone-verification/phone-verification-service.ts @@ -49,7 +49,7 @@ export class PhoneVerification implements VerificationService { } } - async sendSms(user: User): Promise { + async sendVerificationSms(user: User): Promise { try { const code = generateId(); const link = await this.args.links.getPhoneVerificationLink(code); @@ -63,7 +63,7 @@ export class PhoneVerification implements VerificationService { } } - async confirmSms(): Promise { + async confirmVerificationSms(): Promise { // TODO } } \ No newline at end of file diff --git a/server/src/core/phone-verification/types.ts b/server/src/core/phone-verification/types.ts index fd2ec03c..4e7effdf 100644 --- a/server/src/core/phone-verification/types.ts +++ b/server/src/core/phone-verification/types.ts @@ -2,6 +2,6 @@ import { User } from '../user'; export interface VerificationService { createIndexes(): Promise; - sendSms(user: User): Promise; - confirmSms(): Promise; + sendVerificationSms(user: User): Promise; + confirmVerificationSms(): Promise; } \ No newline at end of file From 4b126d204b1b282e8ac6f2fbd9c9bcea851d303c Mon Sep 17 00:00:00 2001 From: Jason Mahirwe Date: Mon, 14 Dec 2020 11:58:02 +0200 Subject: [PATCH 03/53] Rename method confirmVerficationSms to confirmVerificationCode --- server/src/core/error.ts | 5 +++++ .../phone-verification-service.ts | 22 ++++++++++++++++--- server/src/core/phone-verification/types.ts | 2 +- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/server/src/core/error.ts b/server/src/core/error.ts index ba386075..b7938517 100644 --- a/server/src/core/error.ts +++ b/server/src/core/error.ts @@ -49,6 +49,7 @@ export type ErrorCode = | 'loginFailed' | 'invalidToken' | 'resourceNotFound' + | 'phoneVerificationRecordNotFound' | 'uniquenessFailed' | 'paymentRequestFailed' | 'b2cRequestFailed' @@ -178,4 +179,8 @@ export function createTransactionRejectedError(message: string = messages.ERROR_ export function createInsufficientFundsError(message: string) { return createAppError(message, 'insufficientFunds'); +} + +export function createPhoneVerificationRecordNotFound(message: string) { + return createAppError(message, 'phoneVerificationRecordNotFound'); } \ No newline at end of file diff --git a/server/src/core/phone-verification/phone-verification-service.ts b/server/src/core/phone-verification/phone-verification-service.ts index 62943373..01cee0c6 100644 --- a/server/src/core/phone-verification/phone-verification-service.ts +++ b/server/src/core/phone-verification/phone-verification-service.ts @@ -5,7 +5,8 @@ import { UserService, User } from '../user'; import { SmsProvider } from '../sms'; import { Links } from '../link-generator'; import { createPhoneVerificationSms } from '../message'; -import { createDbOpFailedError, rethrowIfAppError } from '../error'; +import { createDbOpFailedError, rethrowIfAppError, createPhoneVerificationRecordNotFound } from '../error'; +import * as messages from '../messages'; const COLLECTION = 'phone-verifications'; @@ -63,7 +64,22 @@ export class PhoneVerification implements VerificationService { } } - async confirmVerificationSms(): Promise { - // TODO + async confirmVerificationCode(code: string): Promise { + try { + const record = await this.collection.findOne({ + _id: code + }); + if (!record) { + throw createPhoneVerificationRecordNotFound(messages.ERROR_INVITATION_NOT_FOUND); + } + else { + + } + } + catch(e) { + console.error("Error occured: ", e.message); + rethrowIfAppError(e); + throw createDbOpFailedError(e.message); + } } } \ No newline at end of file diff --git a/server/src/core/phone-verification/types.ts b/server/src/core/phone-verification/types.ts index 4e7effdf..36e24397 100644 --- a/server/src/core/phone-verification/types.ts +++ b/server/src/core/phone-verification/types.ts @@ -3,5 +3,5 @@ import { User } from '../user'; export interface VerificationService { createIndexes(): Promise; sendVerificationSms(user: User): Promise; - confirmVerificationSms(): Promise; + confirmVerificationCode(): Promise; } \ No newline at end of file From 62a9341c7376c8c63292d5b068bec8494833b11b Mon Sep 17 00:00:00 2001 From: Jason Mahirwe Date: Mon, 14 Dec 2020 12:02:00 +0200 Subject: [PATCH 04/53] Create error message for when there's no record corresponding to a specific verification code --- server/src/core/messages.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/core/messages.ts b/server/src/core/messages.ts index 457d757a..e6fcce02 100644 --- a/server/src/core/messages.ts +++ b/server/src/core/messages.ts @@ -28,4 +28,5 @@ 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'; From e31f4d3b8fe51f86d35d1707fd757eae20955b4a Mon Sep 17 00:00:00 2001 From: Jason Mahirwe Date: Mon, 14 Dec 2020 15:57:16 +0200 Subject: [PATCH 05/53] Implement user service method for verifying donor by setting isPhoneVerified field to true --- server/src/core/error.ts | 7 ++++++- server/src/core/messages.ts | 1 + .../phone-verification-service.ts | 14 ++++++++------ server/src/core/phone-verification/types.ts | 2 +- server/src/core/user/types.ts | 5 +++++ server/src/core/user/user-service.ts | 19 +++++++++++++++++++ 6 files changed, 40 insertions(+), 8 deletions(-) diff --git a/server/src/core/error.ts b/server/src/core/error.ts index b7938517..1b3ab0b1 100644 --- a/server/src/core/error.ts +++ b/server/src/core/error.ts @@ -69,6 +69,7 @@ export type ErrorCode = | 'messageDeliveryFailed' | 'emailDeliveryFailed' | 'linkShorteningFailed' + | 'phoneAlreadyVerified' /** * This error should only be thrown when a transaction fails * because the user's transactions are blocked (based on the transactionsBlockedReason field) @@ -181,6 +182,10 @@ export function createInsufficientFundsError(message: string) { return createAppError(message, 'insufficientFunds'); } -export function createPhoneVerificationRecordNotFound(message: string) { +export function createPhoneVerificationRecordNotFoundError(message: string) { return createAppError(message, 'phoneVerificationRecordNotFound'); +} + +export function createPhoneAlreadyVerifiedError(message: string) { + return createAppError(message, 'phoneAlreadyVerified'); } \ No newline at end of file diff --git a/server/src/core/messages.ts b/server/src/core/messages.ts index e6fcce02..3a5c2766 100644 --- a/server/src/core/messages.ts +++ b/server/src/core/messages.ts @@ -29,4 +29,5 @@ export const ERROR_USER_BLOCKED_FROM_TRANSACTIONS = 'User is blocked from making 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' diff --git a/server/src/core/phone-verification/phone-verification-service.ts b/server/src/core/phone-verification/phone-verification-service.ts index 01cee0c6..e4a184cf 100644 --- a/server/src/core/phone-verification/phone-verification-service.ts +++ b/server/src/core/phone-verification/phone-verification-service.ts @@ -5,7 +5,7 @@ import { UserService, User } from '../user'; import { SmsProvider } from '../sms'; import { Links } from '../link-generator'; import { createPhoneVerificationSms } from '../message'; -import { createDbOpFailedError, rethrowIfAppError, createPhoneVerificationRecordNotFound } from '../error'; +import { createDbOpFailedError, rethrowIfAppError, createPhoneVerificationRecordNotFoundError, createPhoneAlreadyVerifiedError } from '../error'; import * as messages from '../messages'; const COLLECTION = 'phone-verifications'; @@ -13,6 +13,7 @@ const COLLECTION = 'phone-verifications'; export interface PhoneVerificationRecord { _id: string, phone: string, + isVerified: boolean, createdAt: Date, updatedAt: Date, } @@ -66,14 +67,15 @@ export class PhoneVerification implements VerificationService { async confirmVerificationCode(code: string): Promise { try { - const record = await this.collection.findOne({ - _id: code - }); + const record = await this.collection.findOne({ _id: code }); if (!record) { - throw createPhoneVerificationRecordNotFound(messages.ERROR_INVITATION_NOT_FOUND); + throw createPhoneVerificationRecordNotFoundError(messages.ERROR_PHONE_VERIFICATION_RECORD_NOT_FOUND); + } + else if (record.isVerified) { + throw createPhoneAlreadyVerifiedError(messages.ERROR_PHONE_ALREADY_VERIFIED); } else { - + const user = await this.args.users.getByPhone(record.phone); } } catch(e) { diff --git a/server/src/core/phone-verification/types.ts b/server/src/core/phone-verification/types.ts index 36e24397..35b4fcec 100644 --- a/server/src/core/phone-verification/types.ts +++ b/server/src/core/phone-verification/types.ts @@ -3,5 +3,5 @@ import { User } from '../user'; export interface VerificationService { createIndexes(): Promise; sendVerificationSms(user: User): Promise; - confirmVerificationCode(): Promise; + confirmVerificationCode(code: string): Promise; } \ No newline at end of file diff --git a/server/src/core/user/types.ts b/server/src/core/user/types.ts index 5150face..129106af 100644 --- a/server/src/core/user/types.ts +++ b/server/src/core/user/types.ts @@ -15,6 +15,11 @@ export interface User { email?: string, name: string, isAnonymous?: boolean, + /** + * indicates whether or not the phone + * of the corresponding donor has been verified. + */ + isPhoneVerified?: boolean, /** * indicates whether or not the added beneficiary user * has been approved to receive funds from any donor. diff --git a/server/src/core/user/user-service.ts b/server/src/core/user/user-service.ts index f597409a..4bb93770 100644 --- a/server/src/core/user/user-service.ts +++ b/server/src/core/user/user-service.ts @@ -29,6 +29,7 @@ const SAFE_USER_PROJECTION = { phone: 1, email: 1, name: 1, + isPhoneVerified: 1, isVetted: 1, beneficiaryStatus: 1, addedBy: 1, @@ -43,6 +44,7 @@ const SAFE_USER_PROJECTION = { const NOMINATED_USER_PROJECTION = { _id: 1, phone: 1, name: 1, createdAt: 1 }; const ALL_DONORS_PROJECTION = { _id: 1, phone: 1, name: 1, email: 1, createdAt: 1 }; const RELATED_BENEFICIARY_PROJECTION = { _id: 1, name: 1, addedBy: 1, createdAt: 1}; +const VERIFIED_DONOR_PROTECTION = { _id: 1, phone: 1, name: 1, isPhoneVerified: 1, createdAt: 1, updatedAt: 1 }; /** * removes fields that should @@ -868,4 +870,21 @@ export class Users implements UserService { throw createDbOpFailedError(e.message); } } + + async verifyDonor(donor: User): Promise { + try { + const verifiedDonor = await this.collection.findOneAndUpdate( + { _id: donor._id }, + { + $set: { isPhoneVerified: true }, + }, + { upsert: true, returnOriginal: false, projection: VERIFIED_DONOR_PROTECTION } + ); + + return verifiedDonor.value; + } + catch(e) { + + } + } } From 9d03e4cbb09fc400c6d6b72b20fcbb9c7d4ea024 Mon Sep 17 00:00:00 2001 From: Jason Mahirwe Date: Mon, 14 Dec 2020 15:58:14 +0200 Subject: [PATCH 06/53] Implement user service method for verifying donor by setting isPhoneVerified field to true --- server/src/core/user/user-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/core/user/user-service.ts b/server/src/core/user/user-service.ts index 4bb93770..2f29443c 100644 --- a/server/src/core/user/user-service.ts +++ b/server/src/core/user/user-service.ts @@ -884,7 +884,7 @@ export class Users implements UserService { return verifiedDonor.value; } catch(e) { - + throw createDbOpFailedError(e.message); } } } From 83c3ae6e464b6fdc6afe522be34dadc02d48de76 Mon Sep 17 00:00:00 2001 From: Jason Mahirwe Date: Mon, 14 Dec 2020 16:16:46 +0200 Subject: [PATCH 07/53] Update sms format for phone verification --- server/src/core/message/message.ts | 2 +- .../core/phone-verification/phone-verification-service.ts | 1 + server/src/core/user/types.ts | 7 ++++++- server/src/core/user/user-service.ts | 8 ++++---- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/server/src/core/message/message.ts b/server/src/core/message/message.ts index 6bdbb554..4489bde6 100644 --- a/server/src/core/message/message.ts +++ b/server/src/core/message/message.ts @@ -52,7 +52,7 @@ export function createMonthlyDistributionReportEmailMessageForOccasionalDonor(do } export function createPhoneVerificationSms(user: User, verificationLink: string): string { - return `Hello ${extractFirstName(user.name)}, before you can start making donations, you need to confirm your phone number by clicking ${verificationLink}` + return `Hello ${extractFirstName(user.name)}, you need to confirm your phone number by clicking ${verificationLink}` } function beneficiariesAndAmountReceived(beneficiaries: User[], receivedAmount: number[], type: MessageType): string { diff --git a/server/src/core/phone-verification/phone-verification-service.ts b/server/src/core/phone-verification/phone-verification-service.ts index e4a184cf..73a9c59d 100644 --- a/server/src/core/phone-verification/phone-verification-service.ts +++ b/server/src/core/phone-verification/phone-verification-service.ts @@ -76,6 +76,7 @@ export class PhoneVerification implements VerificationService { } else { const user = await this.args.users.getByPhone(record.phone); + await this.args.users.verifyUser(user); } } catch(e) { diff --git a/server/src/core/user/types.ts b/server/src/core/user/types.ts index 129106af..52c0e4af 100644 --- a/server/src/core/user/types.ts +++ b/server/src/core/user/types.ts @@ -302,7 +302,12 @@ export interface UserService { /** * Returns all users with the role donor */ - getAllDonors(): Promise + getAllDonors(): Promise; + /** + * Sets the isPhoneVerified field in user + * to true + */ + verifyUser(user: User): Promise }; export interface UserCreateAnonymousArgs { diff --git a/server/src/core/user/user-service.ts b/server/src/core/user/user-service.ts index 2f29443c..3078873f 100644 --- a/server/src/core/user/user-service.ts +++ b/server/src/core/user/user-service.ts @@ -871,17 +871,17 @@ export class Users implements UserService { } } - async verifyDonor(donor: User): Promise { + public async verifyUser(user: User): Promise { try { - const verifiedDonor = await this.collection.findOneAndUpdate( - { _id: donor._id }, + const verifiedUser = await this.collection.findOneAndUpdate( + { _id: user._id }, { $set: { isPhoneVerified: true }, }, { upsert: true, returnOriginal: false, projection: VERIFIED_DONOR_PROTECTION } ); - return verifiedDonor.value; + return verifiedUser.value; } catch(e) { throw createDbOpFailedError(e.message); From 94e579b734695c72c6e295bda43d275c3cf45919 Mon Sep 17 00:00:00 2001 From: Jason Mahirwe Date: Tue, 15 Dec 2020 10:52:11 +0200 Subject: [PATCH 08/53] Commit master branch changes --- server/src/core/user/types.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/server/src/core/user/types.ts b/server/src/core/user/types.ts index 440a9746..52c0e4af 100644 --- a/server/src/core/user/types.ts +++ b/server/src/core/user/types.ts @@ -302,20 +302,12 @@ export interface UserService { /** * Returns all users with the role donor */ -<<<<<<< HEAD getAllDonors(): Promise; /** * Sets the isPhoneVerified field in user * to true */ verifyUser(user: User): Promise -======= - getAllDonors(): Promise - /** - * Returns all beneficiaries - */ - getAllBeneficiaries(): Promise ->>>>>>> master }; export interface UserCreateAnonymousArgs { From 2605035acf6b3e436fb0fd80329e208ab708b79a Mon Sep 17 00:00:00 2001 From: Jason Mahirwe Date: Tue, 15 Dec 2020 15:22:28 +0200 Subject: [PATCH 09/53] Create event emitters and listeners for when a user is created or activated --- server/src/core/app.ts | 2 ++ server/src/core/bootstrap.ts | 11 +++++++ server/src/core/event/event-bus.ts | 18 +++++++++- server/src/core/event/event-name.ts | 5 +-- server/src/core/phone-verification/index.ts | 2 ++ .../phone-verification-service.ts | 33 ++++++++++++++++++- server/src/core/user/index.ts | 2 +- server/src/core/user/types.ts | 8 ++++- server/src/core/user/user-service.ts | 6 ++++ server/src/rest/index.ts | 3 +- server/src/rest/middleware.ts | 1 + server/src/rest/routes/verifications.ts | 7 ++++ 12 files changed, 91 insertions(+), 7 deletions(-) create mode 100644 server/src/rest/routes/verifications.ts diff --git a/server/src/core/app.ts b/server/src/core/app.ts index a610429b..6e5b1fe2 100644 --- a/server/src/core/app.ts +++ b/server/src/core/app.ts @@ -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; @@ -13,6 +14,7 @@ export interface App { donationDistributions: DonationDistributionService; stats: StatsService; distributionReports: DistributionReportService; + phoneVerification: VerificationService; bulkMessages: BulkMessageService; }; diff --git a/server/src/core/bootstrap.ts b/server/src/core/bootstrap.ts index 51cb6fcf..82fbd9e6 100644 --- a/server/src/core/bootstrap.ts +++ b/server/src/core/bootstrap.ts @@ -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 { const client = await getDbConnection(config.dbUri); @@ -66,6 +67,7 @@ export async function bootstrap(config: AppConfig): Promise { apiKey: config.atApiKey, sender: config.atSmsSender }); + const emailProvider = new SendGridEmailProvider({ apiKey: config.sendgridApiKey, emailSender: config.emailSender @@ -94,6 +96,13 @@ export async function bootstrap(config: AppConfig): Promise { links }); + const phoneVerification = new PhoneVerification(db, { + smsProvider, + users, + links, + eventBus + }); + const messageContextFactory = new DefaultMessageContextFactory({ baseUrl: config.webappBaseUrl, linkGenerator: links @@ -108,12 +117,14 @@ export async function bootstrap(config: AppConfig): Promise { await users.createIndexes(); await transactions.createIndexes(); await invitations.createIndexes(); + await phoneVerification.createIndexes(); return { users, transactions, invitations, donationDistributions, + phoneVerification, stats, distributionReports, bulkMessages diff --git a/server/src/core/event/event-bus.ts b/server/src/core/event/event-bus.ts index 2e51d840..14f54673 100644 --- a/server/src/core/event/event-bus.ts +++ b/server/src/core/event/event-bus.ts @@ -1,5 +1,5 @@ import { EventEmitter } from 'events'; -import { UserInvitationEventData } from '../user'; +import { UserCreatedEventData, UserActivatedEventData, UserInvitationEventData } from '../user'; import { TransactionCompletedEventData } from '../payment'; import * as EventName from './event-name'; @@ -32,10 +32,26 @@ export class EventBus extends EventEmitter { this.on(EventName.USER_INVITATION_CREATED, listener); } + onUserCreated(listener: Listener): void { + this.on(EventName.USER_CREATED, listener); + } + + onUserActivated(listener: Listener): void { + this.on(EventName.USER_ACTIVATED, listener); + } + emitTransactionCompleted(eventData: TransactionCompletedEventData): void { this.innerEmit(EventName.TRANSACTION_COMPLETED, eventData); } + emitUserCreated(eventData: UserCreatedEventData): void { + this.innerEmit(EventName.USER_CREATED, eventData); + } + + emitUserActivated(eventData: UserActivatedEventData): void { + this.innerEmit(EventName.USER_ACTIVATED, eventData); + } + onTransactionCompleted(listener: Listener): void { this.on(EventName.TRANSACTION_COMPLETED, listener); } diff --git a/server/src/core/event/event-name.ts b/server/src/core/event/event-name.ts index 20a2ba5a..92e99708 100644 --- a/server/src/core/event/event-name.ts +++ b/server/src/core/event/event-name.ts @@ -1,5 +1,6 @@ const USER_INVITATION_CREATED = 'userInvitationCreated'; const TRANSACTION_COMPLETED = 'transactionCompleted'; +const USER_CREATED = 'userCreated'; +const USER_ACTIVATED = 'userActivated'; -export { USER_INVITATION_CREATED }; -export { TRANSACTION_COMPLETED }; +export { USER_INVITATION_CREATED, TRANSACTION_COMPLETED, USER_CREATED, USER_ACTIVATED }; diff --git a/server/src/core/phone-verification/index.ts b/server/src/core/phone-verification/index.ts index e69de29b..a6d96bb9 100644 --- a/server/src/core/phone-verification/index.ts +++ b/server/src/core/phone-verification/index.ts @@ -0,0 +1,2 @@ +export { VerificationService } from './types'; +export * from './phone-verification-service'; \ No newline at end of file diff --git a/server/src/core/phone-verification/phone-verification-service.ts b/server/src/core/phone-verification/phone-verification-service.ts index 73a9c59d..ce7a549a 100644 --- a/server/src/core/phone-verification/phone-verification-service.ts +++ b/server/src/core/phone-verification/phone-verification-service.ts @@ -7,6 +7,8 @@ import { Links } from '../link-generator'; import { createPhoneVerificationSms } from '../message'; import { createDbOpFailedError, rethrowIfAppError, createPhoneVerificationRecordNotFoundError, createPhoneAlreadyVerifiedError } from '../error'; import * as messages from '../messages'; +import { EventBus, Event } from '../event'; +import { UserCreatedEventData, UserActivatedEventData } from '../user'; const COLLECTION = 'phone-verifications'; @@ -21,7 +23,8 @@ export interface PhoneVerificationRecord { export interface PhoneVerificationArgs { smsProvider: SmsProvider; users: UserService; - links: Links + links: Links; + eventBus: EventBus; } export class PhoneVerification implements VerificationService { @@ -35,6 +38,34 @@ export class PhoneVerification implements VerificationService { this.collection = this.db.collection(COLLECTION); this.args = args; this.indexesCreated = false; + + this.registerEventHandlers(); + } + + private registerEventHandlers() { + this.args.eventBus.onUserCreated(event => this.handleUserCreated(event)); + this.args.eventBus.onUserActivated(event => this.handleUserActivated(event)); + } + + async handleUserCreated(event: Event) { + console.log('Calling handleUserCreated...'); + return await this.handleUserCreatedOrActivated(event); + } + + async handleUserActivated(event: Event) { + console.log('Calling handleUserActivated...'); + return await this.handleUserCreatedOrActivated(event); + } + + async handleUserCreatedOrActivated(event: Event) { + const { data: { user } } = event; + + try { + await this.sendVerificationSms(user); + } + catch(e) { + console.error('Error occurred when handling event', event, e); + } } async createIndexes(): Promise { diff --git a/server/src/core/user/index.ts b/server/src/core/user/index.ts index 583e90e1..48e0ea19 100644 --- a/server/src/core/user/index.ts +++ b/server/src/core/user/index.ts @@ -1,2 +1,2 @@ -export { User, UserCreateArgs, UserService, UserRole, UserInvitationEventData, UserAddVettedBeneficiaryArgs } from './types'; +export { User, UserCreateArgs, UserService, UserRole, UserInvitationEventData, UserAddVettedBeneficiaryArgs, UserCreatedEventData, UserActivatedEventData } from './types'; export { Users, COLLECTION } from './user-service'; \ No newline at end of file diff --git a/server/src/core/user/types.ts b/server/src/core/user/types.ts index 52c0e4af..19bc9c4b 100644 --- a/server/src/core/user/types.ts +++ b/server/src/core/user/types.ts @@ -319,4 +319,10 @@ export interface UserCreateAnonymousArgs { export interface UserDonateAnonymouslyArgs extends UserCreateAnonymousArgs { amount: number -} \ No newline at end of file +} + +export interface UserCreatedEventData { + user: User; +} + +export interface UserActivatedEventData extends UserCreatedEventData {} \ No newline at end of file diff --git a/server/src/core/user/user-service.ts b/server/src/core/user/user-service.ts index 3078873f..17fed0ee 100644 --- a/server/src/core/user/user-service.ts +++ b/server/src/core/user/user-service.ts @@ -139,6 +139,7 @@ export class Users implements UserService { _id: generateId(), phone: args.phone, name: args.name, + isPhoneVerified: false, addedBy: '', donors: [], roles: ['donor'], @@ -162,6 +163,7 @@ export class Users implements UserService { } const res = await this.collection.insertOne(user); + this.eventBus.emitUserCreated({ user: getSafeUser(res.ops[0]) }); return getSafeUser(res.ops[0]); } catch (e) { @@ -274,6 +276,7 @@ export class Users implements UserService { _id: generateId(), password: '', phone, + isPhoneVerified: false, name, addedBy: nominatorId, createdAt: new Date(), @@ -288,6 +291,8 @@ export class Users implements UserService { }, { upsert: true, returnOriginal: false, projection: NOMINATED_USER_PROJECTION } ); + + this.eventBus.emitUserActivated({ user: getSafeUser(result.value) }); return getSafeUser(result.value); } catch (e) { @@ -331,6 +336,7 @@ export class Users implements UserService { }, { upsert: true, returnOriginal: false, projection: NOMINATED_USER_PROJECTION } ); + this.eventBus.emitUserActivated({ user: getSafeUser(result.value) }); return getSafeUser(result.value); } catch (e) { diff --git a/server/src/rest/index.ts b/server/src/rest/index.ts index d7d30ec9..fc88061c 100644 --- a/server/src/rest/index.ts +++ b/server/src/rest/index.ts @@ -1,7 +1,7 @@ import { Express, Router } from 'express'; import { messages } from '../core'; import { errorHandler, error404Handler } from './middleware'; -import { root, users, donations, transactions, invitations, refunds, stats } from './routes'; +import { root, users, donations, transactions, invitations, verifications, refunds, stats } from './routes'; export function mountRestApi(server: Express, apiRoot: string) { const router = Router(); @@ -12,6 +12,7 @@ export function mountRestApi(server: Express, apiRoot: string) { router.use('/invitations', invitations); router.use('/refunds', refunds); router.use('/stats', stats); + router.use('/verifications', verifications); router.use('/', root); router.use(errorHandler()); diff --git a/server/src/rest/middleware.ts b/server/src/rest/middleware.ts index a377618c..6e49c714 100644 --- a/server/src/rest/middleware.ts +++ b/server/src/rest/middleware.ts @@ -28,6 +28,7 @@ export const errorHandler = (): ErrorRequestHandler => case 'loginFailed': return sendErrorResponse(res, statusCodes.STATUS_UNAUTHORIZED, error); case 'resourceNotFound': + case 'phoneVerificationRecordNotFound': return sendErrorResponse(res, statusCodes.STATUS_NOT_FOUND, error); case 'uniquenessFailed': return sendErrorResponse(res, statusCodes.STATUS_CONFLICT, error); diff --git a/server/src/rest/routes/verifications.ts b/server/src/rest/routes/verifications.ts new file mode 100644 index 00000000..1dbf7ac1 --- /dev/null +++ b/server/src/rest/routes/verifications.ts @@ -0,0 +1,7 @@ +import { Router } from 'express'; +import { wrapResponse } from '../middleware'; + +export const verifications = Router(); + +verifications.put('/phone/:id', wrapResponse( + req => req.core.phoneVerification.confirmVerificationCode(req.params.id))); \ No newline at end of file From 319f28689fd589bdf939adc45f755da300f22a86 Mon Sep 17 00:00:00 2001 From: Jason Mahirwe Date: Tue, 15 Dec 2020 15:35:34 +0200 Subject: [PATCH 10/53] implement error handling cases for error codes phoneAlreadyVerfied and phoneVerificationRecordNotFound --- server/src/rest/middleware.ts | 1 + server/src/rest/routes/index.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/server/src/rest/middleware.ts b/server/src/rest/middleware.ts index 6e49c714..bdbc0c87 100644 --- a/server/src/rest/middleware.ts +++ b/server/src/rest/middleware.ts @@ -31,6 +31,7 @@ export const errorHandler = (): ErrorRequestHandler => case 'phoneVerificationRecordNotFound': return sendErrorResponse(res, statusCodes.STATUS_NOT_FOUND, error); case 'uniquenessFailed': + case 'phoneAlreadyVerified': return sendErrorResponse(res, statusCodes.STATUS_CONFLICT, error); case 'paymentRequestFailed': case 'activationFailed': diff --git a/server/src/rest/routes/index.ts b/server/src/rest/routes/index.ts index 8f6208da..04fe7b93 100644 --- a/server/src/rest/routes/index.ts +++ b/server/src/rest/routes/index.ts @@ -5,3 +5,4 @@ export { transactions } from './transactions'; export { invitations } from './invitations'; export { refunds } from './refunds'; export { stats } from './stats'; +export { verifications } from './verifications'; From 4cb51bf80e54900da703938a3245e3ff567f1834 Mon Sep 17 00:00:00 2001 From: Jason Mahirwe Date: Tue, 15 Dec 2020 16:03:05 +0200 Subject: [PATCH 11/53] Resolve error 'Property getAllBeneficiaries does not exist' --- server/src/core/user/types.ts | 6 +++++- server/src/core/user/user-service.ts | 10 ++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/server/src/core/user/types.ts b/server/src/core/user/types.ts index 19bc9c4b..471a1a03 100644 --- a/server/src/core/user/types.ts +++ b/server/src/core/user/types.ts @@ -307,7 +307,11 @@ export interface UserService { * Sets the isPhoneVerified field in user * to true */ - verifyUser(user: User): Promise + verifyUser(user: User): Promise; + /** + * Returns all beneficiaries + */ + getAllBeneficiaries(): Promise }; export interface UserCreateAnonymousArgs { diff --git a/server/src/core/user/user-service.ts b/server/src/core/user/user-service.ts index 17fed0ee..02adfc29 100644 --- a/server/src/core/user/user-service.ts +++ b/server/src/core/user/user-service.ts @@ -893,4 +893,14 @@ export class Users implements UserService { throw createDbOpFailedError(e.message); } } + + async getAllBeneficiaries(): Promise { + try { + const donors = await this.collection.find({ roles: { $in: ['beneficiary'] } }, { projection: SAFE_USER_PROJECTION }).toArray(); + return donors; + } + catch (e) { + throw createDbOpFailedError(e.message); + } + } } From 6fc807502f08ff97bd56b06a2617291087f44f63 Mon Sep 17 00:00:00 2001 From: Jason Mahirwe Date: Wed, 16 Dec 2020 17:41:25 +0200 Subject: [PATCH 12/53] Return corresponding verification record after verifying phone number --- server/src/core/event/event-bus.ts | 2 ++ .../link-generator/link-generator-service.ts | 4 ++-- .../phone-verification-service.ts | 22 ++++++++++++++----- server/src/core/phone-verification/types.ts | 9 +++++++- 4 files changed, 29 insertions(+), 8 deletions(-) diff --git a/server/src/core/event/event-bus.ts b/server/src/core/event/event-bus.ts index 14f54673..813b2af0 100644 --- a/server/src/core/event/event-bus.ts +++ b/server/src/core/event/event-bus.ts @@ -33,6 +33,7 @@ export class EventBus extends EventEmitter { } onUserCreated(listener: Listener): void { + console.log('In onUserCreated...'); this.on(EventName.USER_CREATED, listener); } @@ -45,6 +46,7 @@ export class EventBus extends EventEmitter { } emitUserCreated(eventData: UserCreatedEventData): void { + console.log('In emitUserCreated...'); this.innerEmit(EventName.USER_CREATED, eventData); } diff --git a/server/src/core/link-generator/link-generator-service.ts b/server/src/core/link-generator/link-generator-service.ts index 0028a263..7d954bf6 100644 --- a/server/src/core/link-generator/link-generator-service.ts +++ b/server/src/core/link-generator/link-generator-service.ts @@ -59,11 +59,11 @@ export class Links implements LinkGeneratorService { } async getPhoneVerificationLink(code: string, shorten: boolean = true): Promise { - const link: string = `${this.args.baseUrl}/confirm/phone/${code}`; + const link: string = `${this.args.baseUrl}/verifications/phone/${code}`; if (shorten) { return this.args.shortener.shortenLink(link); } - + return link; } } \ No newline at end of file diff --git a/server/src/core/phone-verification/phone-verification-service.ts b/server/src/core/phone-verification/phone-verification-service.ts index ce7a549a..860f720c 100644 --- a/server/src/core/phone-verification/phone-verification-service.ts +++ b/server/src/core/phone-verification/phone-verification-service.ts @@ -1,6 +1,6 @@ -import { Db, Collection } from 'mongodb'; +import { Db, Collection, FindAndModifyWriteOpResultObject } from 'mongodb'; import { generateId } from '../util'; -import { VerificationService } from './types'; +import { VerificationRecord, VerificationService } from './types'; import { UserService, User } from '../user'; import { SmsProvider } from '../sms'; import { Links } from '../link-generator'; @@ -12,7 +12,7 @@ import { UserCreatedEventData, UserActivatedEventData } from '../user'; const COLLECTION = 'phone-verifications'; -export interface PhoneVerificationRecord { +export interface PhoneVerificationRecord extends VerificationRecord { _id: string, phone: string, isVerified: boolean, @@ -86,6 +86,7 @@ export class PhoneVerification implements VerificationService { try { const code = generateId(); const link = await this.args.links.getPhoneVerificationLink(code); + console.log('generated link: ', link); const smsMessage = createPhoneVerificationSms(user, link); await this.args.smsProvider.sendSms(user.phone, smsMessage); } @@ -96,7 +97,7 @@ export class PhoneVerification implements VerificationService { } } - async confirmVerificationCode(code: string): Promise { + async confirmVerificationCode(code: string): Promise { try { const record = await this.collection.findOne({ _id: code }); if (!record) { @@ -106,8 +107,19 @@ export class PhoneVerification implements VerificationService { throw createPhoneAlreadyVerifiedError(messages.ERROR_PHONE_ALREADY_VERIFIED); } else { - const user = await this.args.users.getByPhone(record.phone); + const result = await this.collection.findOneAndUpdate( + { _id: code }, + { + $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) { diff --git a/server/src/core/phone-verification/types.ts b/server/src/core/phone-verification/types.ts index 35b4fcec..f55824af 100644 --- a/server/src/core/phone-verification/types.ts +++ b/server/src/core/phone-verification/types.ts @@ -1,7 +1,14 @@ import { User } from '../user'; +export interface VerificationRecord { + _id: string, + isVerified: boolean, + createdAt: Date, + updatedAt: Date, +} + export interface VerificationService { createIndexes(): Promise; sendVerificationSms(user: User): Promise; - confirmVerificationCode(code: string): Promise; + confirmVerificationCode(code: string): Promise; } \ No newline at end of file From e8d91130c804f5081d99d3ec7cf3214924a898ea Mon Sep 17 00:00:00 2001 From: Jason Mahirwe Date: Wed, 16 Dec 2020 17:45:21 +0200 Subject: [PATCH 13/53] Design and implement phone verification page --- webapp/src/services/index.ts | 1 + webapp/src/services/verifications.ts | 9 +++++ webapp/src/store/actions.ts | 7 +++- webapp/src/store/index.ts | 1 + webapp/src/store/mutations.ts | 6 ++++ webapp/src/types.ts | 9 +++++ webapp/src/views/verify-phone.vue | 50 ++++++++++++++++++++++++++++ 7 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 webapp/src/services/verifications.ts create mode 100644 webapp/src/views/verify-phone.vue diff --git a/webapp/src/services/index.ts b/webapp/src/services/index.ts index a8651070..e23d3ff9 100644 --- a/webapp/src/services/index.ts +++ b/webapp/src/services/index.ts @@ -4,5 +4,6 @@ export { Refunds } from './refunds'; export { Users } from './users'; export { Transactions } from './transactions'; export { Invitations } from './invitations'; +export { Verifications } from './verifications'; export { Statistics } from './stats'; export { AnonymousUser } from './anonymousUser'; \ No newline at end of file diff --git a/webapp/src/services/verifications.ts b/webapp/src/services/verifications.ts new file mode 100644 index 00000000..33dfa7c4 --- /dev/null +++ b/webapp/src/services/verifications.ts @@ -0,0 +1,9 @@ +import { PhoneVerificationRecord } from '../types'; +import axios from 'axios'; + +export const Verifications = { + async verifyPhone(recordId: string) { + const res = await axios.post(`/verifications/phone/${recordId}`); + return res.data; + }, +} \ No newline at end of file diff --git a/webapp/src/store/actions.ts b/webapp/src/store/actions.ts index 1ff8d1a5..c0625bff 100644 --- a/webapp/src/store/actions.ts +++ b/webapp/src/store/actions.ts @@ -1,5 +1,5 @@ import { wrapActions, googleSignOut } from './util'; -import { Users, Transactions, Donations, Refunds, Invitations, Statistics } from '../services'; +import { Users, Transactions, Donations, Refunds, Invitations, Verifications, Statistics } from '../services'; import router from '../router'; import { DEFAULT_SIGNED_IN_PAGE, DEFAULT_SIGNED_OUT_PAGE } from '../router/defaults'; import { NominationRole } from '@/types'; @@ -81,6 +81,10 @@ const actions = wrapActions({ commit('updateInvitation', invitation); commit('setCurrentInvitation', invitation); }, + async verifyPhone({ commit }, recordId: string) { + const record = await Verifications.verifyPhone(recordId); + commit('setPhoneVerificationRecord', record); + }, async donate({ commit, state }, { amount }: { amount: number }) { if (state.user) { const trx = await Donations.initiateDonation({ amount }); @@ -177,6 +181,7 @@ const actions = wrapActions({ 'unsetTransactions', 'unsetInvitations', 'unsetCurrentInvitation', + 'unsetPhoneVerificationRecord', 'unsetLastPaymentRequest', 'unsetMessage', 'unsetStats', diff --git a/webapp/src/store/index.ts b/webapp/src/store/index.ts index f2e14849..80da8fdd 100644 --- a/webapp/src/store/index.ts +++ b/webapp/src/store/index.ts @@ -13,6 +13,7 @@ const state: AppState = { anonymousUser: undefined, newUser: undefined, anonymousDonationDetails: undefined, + phoneVerificationRecord: undefined, beneficiaries: [], middlemen: [], transactions: [], diff --git a/webapp/src/store/mutations.ts b/webapp/src/store/mutations.ts index 3e435292..9ff60e21 100644 --- a/webapp/src/store/mutations.ts +++ b/webapp/src/store/mutations.ts @@ -79,6 +79,12 @@ const mutations: MutationTree = { unsetCurrentInvitation(state) { state.currentInvitation = undefined }, + setPhoneVerificationRecord(state, record) { + state.phoneVerificationRecord = record; + }, + unsetPhoneVerificationRecord(state) { + state.phoneVerificationRecord = undefined + }, setBeneficiaries(state, beneficiaries) { state.beneficiaries = beneficiaries }, diff --git a/webapp/src/types.ts b/webapp/src/types.ts index 3418d3e1..b03cc981 100644 --- a/webapp/src/types.ts +++ b/webapp/src/types.ts @@ -163,10 +163,19 @@ export interface FAQ { answer: string; }; +export interface PhoneVerificationRecord { + _id: string, + phone: string, + isVerified: boolean, + createdAt: Date, + updatedAt: Date, +} + export interface AppState { user?: User; anonymousUser?: User; anonymousDonationDetails?: AnonymousDonateArgs; + phoneVerificationRecord?: PhoneVerificationRecord; newUser?: User; beneficiaries: User[]; middlemen: User[]; diff --git a/webapp/src/views/verify-phone.vue b/webapp/src/views/verify-phone.vue new file mode 100644 index 00000000..4468e081 --- /dev/null +++ b/webapp/src/views/verify-phone.vue @@ -0,0 +1,50 @@ + + \ No newline at end of file From 9733a651f99a3515ce73a69bc9544f9ac8bd86bc Mon Sep 17 00:00:00 2001 From: Jason Mahirwe Date: Thu, 17 Dec 2020 13:44:34 +0200 Subject: [PATCH 14/53] Create a route entry for the verify-phone page --- webapp/src/app.vue | 1 + webapp/src/router/index.ts | 5 +++++ webapp/src/views/verify-phone.vue | 12 +----------- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/webapp/src/app.vue b/webapp/src/app.vue index a5ff7049..28617568 100644 --- a/webapp/src/app.vue +++ b/webapp/src/app.vue @@ -42,6 +42,7 @@ export default { showLoggedInNavigation () { if (this.$route.name === DEFAULT_SIGNED_OUT_PAGE || this.$route.name === 'accept-invitation' || + this.$route.name === 'verify-phone' || this.$route.name === 'signup-new-user' || (this.$route.name === 'post-payment-flutterwave' && AnonymousUser.isSet())) { return false diff --git a/webapp/src/router/index.ts b/webapp/src/router/index.ts index 04b6547c..48433b66 100644 --- a/webapp/src/router/index.ts +++ b/webapp/src/router/index.ts @@ -48,6 +48,11 @@ const routes = [ name: 'accept-invitation', component: () => import(/* webpackChunkName: "invitations" */ '../views/accept-invitation.vue') }, + { + path: '/verifications/phone/:id', + name: 'verify-phone', + component: () => import(/* webpackChunkName: "verifications" */ '../views/verify-phone.vue') + }, { path: '/account', name: 'account', diff --git a/webapp/src/views/verify-phone.vue b/webapp/src/views/verify-phone.vue index 4468e081..7aae4e1e 100644 --- a/webapp/src/views/verify-phone.vue +++ b/webapp/src/views/verify-phone.vue @@ -5,20 +5,10 @@ Your phone number {{ phoneVerificationRecord.phone }} has been verified - - Your transaction is being verified. - Your account will be updated and you will receive a confirmation email when the process is complete. - - - - The transaction was cancelled. - - - Transaction not found
Return Home -
+ \ No newline at end of file From 80af8065910f9b170c7a2f7c928ab497626974b7 Mon Sep 17 00:00:00 2001 From: Jason Mahirwe Date: Mon, 11 Jan 2021 11:56:00 +0200 Subject: [PATCH 32/53] Implement new verifications service method for creating phone verification records --- webapp/src/services/verifications.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/webapp/src/services/verifications.ts b/webapp/src/services/verifications.ts index 5f2eb8d5..2c8e5abd 100644 --- a/webapp/src/services/verifications.ts +++ b/webapp/src/services/verifications.ts @@ -2,6 +2,9 @@ import { PhoneVerificationRecord, PhoneVerificationArgs } from '../types'; import axios from 'axios'; export const Verifications = { + async createPhoneVerificationRecord(phone: string) { + const res = await axios.post(`/verifications/phone`, {phone}); + }, async verifyPhone(args: PhoneVerificationArgs) { const { id, code } = args; const res = await axios.put(`/verifications/phone/${id}`, {code}); From 62e1be6fbdcae39e1fe9ecd4f4b0ca3350205c77 Mon Sep 17 00:00:00 2001 From: Jason Mahirwe Date: Mon, 11 Jan 2021 12:23:56 +0200 Subject: [PATCH 33/53] Create another a db-specific type that includes field code in phone verification service docs --- .../phone-verification-service.ts | 51 +++++++++++++++++-- server/src/core/phone-verification/types.ts | 1 + server/src/core/user/user-service.ts | 1 - server/src/rest/routes/verifications.ts | 5 +- 4 files changed, 51 insertions(+), 7 deletions(-) diff --git a/server/src/core/phone-verification/phone-verification-service.ts b/server/src/core/phone-verification/phone-verification-service.ts index 4c0d7626..38e6ee1f 100644 --- a/server/src/core/phone-verification/phone-verification-service.ts +++ b/server/src/core/phone-verification/phone-verification-service.ts @@ -22,12 +22,27 @@ const SAFE_PHONE_VERIFICATION_RECORD_PROJECTION = { updatedAt: 1 }; +function getSafePhoneVerificationRecord(record: DbPhoneVerificationRecord): PhoneVerificationRecord { + const recordDict: any = record; + return Object.keys(SAFE_PHONE_VERIFICATION_RECORD_PROJECTION) + .reduce((safeRecord, field) => { + if (field in record) { + safeRecord[field] = recordDict[field]; + } + + return safeRecord; + }, {}); +} + export interface PhoneVerificationRecord extends VerificationRecord { + phone: string, +} + +export interface DbPhoneVerificationRecord extends PhoneVerificationRecord { /** * A 6-digit unique code */ code: number, - phone: string, } export interface PhoneVerificationArgs { @@ -85,13 +100,12 @@ export class PhoneVerification implements VerificationService { } } - async create(args: PhoneVerificationRecordCreateArgs): Promise { - const { id, code, phone } = args; + async create(phone: string): Promise { try { const now = new Date(); const record = { - _id: id, - code, + _id: generateId(), + code: generatePhoneVerificationCode(), phone, isVerified: false, createdAt: now, @@ -112,6 +126,33 @@ export class PhoneVerification implements VerificationService { } } + // async create(args: PhoneVerificationRecordCreateArgs): Promise { + // const { id, code, phone } = args; + // try { + // const now = new Date(); + // const record = { + // _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, args.phone)) { + // throw createUniquenessFailedError(messages.ERROR_PHONE_ALREADY_IN_USE); + // } + + // throw createDbOpFailedError(e.message); + // } + // } + public async getById(id: string): Promise { try { const record = await this.collection.findOne( diff --git a/server/src/core/phone-verification/types.ts b/server/src/core/phone-verification/types.ts index dc17b4f0..2455fc8e 100644 --- a/server/src/core/phone-verification/types.ts +++ b/server/src/core/phone-verification/types.ts @@ -12,4 +12,5 @@ export interface VerificationService { sendVerificationSms(user: User, id: string, code: number): Promise; confirmVerificationCode(id: string, code: number): Promise; getById(id: string): Promise; + create(phone: string): Promise; } \ No newline at end of file diff --git a/server/src/core/user/user-service.ts b/server/src/core/user/user-service.ts index 143c4e55..2d12860c 100644 --- a/server/src/core/user/user-service.ts +++ b/server/src/core/user/user-service.ts @@ -61,7 +61,6 @@ function getSafeUser(user: DbUser): User { return safeUser; }, {}); - } function hasRole(user: User, role: UserRole): boolean { diff --git a/server/src/rest/routes/verifications.ts b/server/src/rest/routes/verifications.ts index 8f884faf..d919148a 100644 --- a/server/src/rest/routes/verifications.ts +++ b/server/src/rest/routes/verifications.ts @@ -7,4 +7,7 @@ verifications.put('/phone/:id', wrapResponse( req => req.core.phoneVerification.confirmVerificationCode(req.params.id, req.body.code))); verifications.get('/phone/:id', wrapResponse( - req => req.core.phoneVerification.getById(req.params.id))); \ No newline at end of file + req => req.core.phoneVerification.getById(req.params.id))); + +verifications.post('/phone', wrapResponse( + req => req.core.phoneVerification.create(req.body.phone))); \ No newline at end of file From 6e7b917b3349a7da4afb14cc169ae0d3ac5ef674 Mon Sep 17 00:00:00 2001 From: Jason Mahirwe Date: Mon, 11 Jan 2021 13:19:38 +0200 Subject: [PATCH 34/53] Implement validators and validation schemas for service phone-verification --- .../phone-verification-service.ts | 45 ++----------------- .../phone-verification/validation-schemas.ts | 6 +++ .../src/core/phone-verification/validator.ts | 8 ++++ 3 files changed, 18 insertions(+), 41 deletions(-) create mode 100644 server/src/core/phone-verification/validation-schemas.ts create mode 100644 server/src/core/phone-verification/validator.ts diff --git a/server/src/core/phone-verification/phone-verification-service.ts b/server/src/core/phone-verification/phone-verification-service.ts index 38e6ee1f..a3557eea 100644 --- a/server/src/core/phone-verification/phone-verification-service.ts +++ b/server/src/core/phone-verification/phone-verification-service.ts @@ -12,6 +12,7 @@ import { createDbOpFailedError, rethrowIfAppError, import * as messages from '../messages'; import { EventBus, Event } from '../event'; import { UserCreatedEventData, UserActivatedEventData } from '../user'; +import * as validators from './validator'; const COLLECTION = 'phone-verifications'; const SAFE_PHONE_VERIFICATION_RECORD_PROJECTION = { @@ -22,18 +23,6 @@ const SAFE_PHONE_VERIFICATION_RECORD_PROJECTION = { updatedAt: 1 }; -function getSafePhoneVerificationRecord(record: DbPhoneVerificationRecord): PhoneVerificationRecord { - const recordDict: any = record; - return Object.keys(SAFE_PHONE_VERIFICATION_RECORD_PROJECTION) - .reduce((safeRecord, field) => { - if (field in record) { - safeRecord[field] = recordDict[field]; - } - - return safeRecord; - }, {}); -} - export interface PhoneVerificationRecord extends VerificationRecord { phone: string, } @@ -101,9 +90,10 @@ export class PhoneVerification implements VerificationService { } async create(phone: string): Promise { + validators.validatesCreate(phone); try { const now = new Date(); - const record = { + const record: DbPhoneVerificationRecord = { _id: generateId(), code: generatePhoneVerificationCode(), phone, @@ -118,7 +108,7 @@ export class PhoneVerification implements VerificationService { catch (e) { rethrowIfAppError(e); - if (isMongoDuplicateKeyError(e, args.phone)) { + if (isMongoDuplicateKeyError(e, phone)) { throw createUniquenessFailedError(messages.ERROR_PHONE_ALREADY_IN_USE); } @@ -126,33 +116,6 @@ export class PhoneVerification implements VerificationService { } } - // async create(args: PhoneVerificationRecordCreateArgs): Promise { - // const { id, code, phone } = args; - // try { - // const now = new Date(); - // const record = { - // _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, args.phone)) { - // throw createUniquenessFailedError(messages.ERROR_PHONE_ALREADY_IN_USE); - // } - - // throw createDbOpFailedError(e.message); - // } - // } - public async getById(id: string): Promise { try { const record = await this.collection.findOne( diff --git a/server/src/core/phone-verification/validation-schemas.ts b/server/src/core/phone-verification/validation-schemas.ts new file mode 100644 index 00000000..1a8a3de7 --- /dev/null +++ b/server/src/core/phone-verification/validation-schemas.ts @@ -0,0 +1,6 @@ +import * as joi from '@hapi/joi'; +import { phoneValidationSchema } from '../util/validation-util'; + +export const createInputSchema = joi.object().keys({ + phone: phoneValidationSchema, +}); \ No newline at end of file diff --git a/server/src/core/phone-verification/validator.ts b/server/src/core/phone-verification/validator.ts new file mode 100644 index 00000000..3f9fc983 --- /dev/null +++ b/server/src/core/phone-verification/validator.ts @@ -0,0 +1,8 @@ +import { createValidationError } from '../error'; +import * as schemas from './validation-schemas'; + +export const validatesCreate = (phone: string) => { + const { error } = schemas.createInputSchema.validate({phone}); + if (error) throw createValidationError(error.details[0].message); + if (phone[0] === '0') createValidationError('Phone number cannot start with 0'); +} \ No newline at end of file From ba3e5436c4eadd01b2e89a0fd8340229f5ef12e9 Mon Sep 17 00:00:00 2001 From: Jason Mahirwe Date: Mon, 11 Jan 2021 13:44:25 +0200 Subject: [PATCH 35/53] Pass user object in body when creating phone verification record --- webapp/src/services/verifications.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/webapp/src/services/verifications.ts b/webapp/src/services/verifications.ts index 2c8e5abd..96e70839 100644 --- a/webapp/src/services/verifications.ts +++ b/webapp/src/services/verifications.ts @@ -1,9 +1,9 @@ -import { PhoneVerificationRecord, PhoneVerificationArgs } from '../types'; +import { PhoneVerificationRecord, PhoneVerificationArgs, User } from '../types'; import axios from 'axios'; export const Verifications = { - async createPhoneVerificationRecord(phone: string) { - const res = await axios.post(`/verifications/phone`, {phone}); + async createPhoneVerificationRecord(user: User) { + const res = await axios.post(`/verifications/phone`, { user }); }, async verifyPhone(args: PhoneVerificationArgs) { const { id, code } = args; From 5ca422cacda56e0fdd5782b66880e695b58d4920 Mon Sep 17 00:00:00 2001 From: Jason Mahirwe Date: Mon, 11 Jan 2021 14:01:25 +0200 Subject: [PATCH 36/53] Pass user phone in body when creating phone verification records --- webapp/src/services/verifications.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webapp/src/services/verifications.ts b/webapp/src/services/verifications.ts index 96e70839..ae217b31 100644 --- a/webapp/src/services/verifications.ts +++ b/webapp/src/services/verifications.ts @@ -2,8 +2,8 @@ import { PhoneVerificationRecord, PhoneVerificationArgs, User } from '../types'; import axios from 'axios'; export const Verifications = { - async createPhoneVerificationRecord(user: User) { - const res = await axios.post(`/verifications/phone`, { user }); + async createPhoneVerificationRecord(phone: string) { + const res = await axios.post(`/verifications/phone`, { phone }); }, async verifyPhone(args: PhoneVerificationArgs) { const { id, code } = args; From 6bac9577c1a0b38e3ef8ab36e9f0e678feb18140 Mon Sep 17 00:00:00 2001 From: Jason Mahirwe Date: Mon, 11 Jan 2021 14:32:02 +0200 Subject: [PATCH 37/53] Remove event emitter emitUserCreated in User service method create --- server/src/core/event/event-bus.ts | 5 --- .../phone-verification-service.ts | 34 +++---------------- .../src/core/phone-verification/validator.ts | 2 +- server/src/core/user/user-service.ts | 1 - server/src/rest/routes/verifications.ts | 2 +- 5 files changed, 7 insertions(+), 37 deletions(-) diff --git a/server/src/core/event/event-bus.ts b/server/src/core/event/event-bus.ts index 813b2af0..ac41577a 100644 --- a/server/src/core/event/event-bus.ts +++ b/server/src/core/event/event-bus.ts @@ -32,11 +32,6 @@ export class EventBus extends EventEmitter { this.on(EventName.USER_INVITATION_CREATED, listener); } - onUserCreated(listener: Listener): void { - console.log('In onUserCreated...'); - this.on(EventName.USER_CREATED, listener); - } - onUserActivated(listener: Listener): void { this.on(EventName.USER_ACTIVATED, listener); } diff --git a/server/src/core/phone-verification/phone-verification-service.ts b/server/src/core/phone-verification/phone-verification-service.ts index a3557eea..14344fa8 100644 --- a/server/src/core/phone-verification/phone-verification-service.ts +++ b/server/src/core/phone-verification/phone-verification-service.ts @@ -58,44 +58,20 @@ export class PhoneVerification implements VerificationService { this.collection = this.db.collection(COLLECTION); this.args = args; this.indexesCreated = false; - - this.registerEventHandlers(); - } - - private registerEventHandlers() { - this.args.eventBus.onUserCreated(event => this.handleUserCreated(event)); - this.args.eventBus.onUserActivated(event => this.handleUserActivated(event)); - } - - async handleUserCreated(event: Event) { - return await this.handleUserCreatedOrActivated(event); } - async handleUserActivated(event: Event) { - return await this.handleUserCreatedOrActivated(event); - } - - async handleUserCreatedOrActivated(event: Event) { - const { data: { user } } = event; - + async create(phone: string): Promise { + validators.validatesCreate(phone); try { + const user = await this.args.users.getByPhone(phone); const id = generateId(); // id to be given to the phone verification record const code = generatePhoneVerificationCode(); await this.sendVerificationSms(user, id, code); - const record = await this.create({ id, code, phone: user.phone }); - } - catch(e) { - console.error('Error occurred when handling event', event, e); - } - } - async create(phone: string): Promise { - validators.validatesCreate(phone); - try { const now = new Date(); const record: DbPhoneVerificationRecord = { - _id: generateId(), - code: generatePhoneVerificationCode(), + _id: id, + code, phone, isVerified: false, createdAt: now, diff --git a/server/src/core/phone-verification/validator.ts b/server/src/core/phone-verification/validator.ts index 3f9fc983..3f5b7455 100644 --- a/server/src/core/phone-verification/validator.ts +++ b/server/src/core/phone-verification/validator.ts @@ -2,7 +2,7 @@ import { createValidationError } from '../error'; import * as schemas from './validation-schemas'; export const validatesCreate = (phone: string) => { - const { error } = schemas.createInputSchema.validate({phone}); + const { error } = schemas.createInputSchema.validate({ phone }); if (error) throw createValidationError(error.details[0].message); if (phone[0] === '0') createValidationError('Phone number cannot start with 0'); } \ No newline at end of file diff --git a/server/src/core/user/user-service.ts b/server/src/core/user/user-service.ts index 2d12860c..adb1cbf0 100644 --- a/server/src/core/user/user-service.ts +++ b/server/src/core/user/user-service.ts @@ -162,7 +162,6 @@ export class Users implements UserService { } const res = await this.collection.insertOne(user); - this.eventBus.emitUserCreated({ user: getSafeUser(res.ops[0]) }); return getSafeUser(res.ops[0]); } catch (e) { diff --git a/server/src/rest/routes/verifications.ts b/server/src/rest/routes/verifications.ts index d919148a..20d0a5c5 100644 --- a/server/src/rest/routes/verifications.ts +++ b/server/src/rest/routes/verifications.ts @@ -10,4 +10,4 @@ verifications.get('/phone/:id', wrapResponse( req => req.core.phoneVerification.getById(req.params.id))); verifications.post('/phone', wrapResponse( - req => req.core.phoneVerification.create(req.body.phone))); \ No newline at end of file + req => req.core.phoneVerification.create(req.body.user))); \ No newline at end of file From 24de1fc0f9a0be7bc53bdfa6985199efa1a1e9ef Mon Sep 17 00:00:00 2001 From: Jason Mahirwe Date: Mon, 11 Jan 2021 14:35:09 +0200 Subject: [PATCH 38/53] Remove event emitter emitUserCreated in User service method create --- server/src/core/event/event-bus.ts | 15 +-------------- server/src/core/event/event-name.ts | 4 +--- server/src/core/user/user-service.ts | 1 - 3 files changed, 2 insertions(+), 18 deletions(-) diff --git a/server/src/core/event/event-bus.ts b/server/src/core/event/event-bus.ts index ac41577a..2e51d840 100644 --- a/server/src/core/event/event-bus.ts +++ b/server/src/core/event/event-bus.ts @@ -1,5 +1,5 @@ import { EventEmitter } from 'events'; -import { UserCreatedEventData, UserActivatedEventData, UserInvitationEventData } from '../user'; +import { UserInvitationEventData } from '../user'; import { TransactionCompletedEventData } from '../payment'; import * as EventName from './event-name'; @@ -32,23 +32,10 @@ export class EventBus extends EventEmitter { this.on(EventName.USER_INVITATION_CREATED, listener); } - onUserActivated(listener: Listener): void { - this.on(EventName.USER_ACTIVATED, listener); - } - emitTransactionCompleted(eventData: TransactionCompletedEventData): void { this.innerEmit(EventName.TRANSACTION_COMPLETED, eventData); } - emitUserCreated(eventData: UserCreatedEventData): void { - console.log('In emitUserCreated...'); - this.innerEmit(EventName.USER_CREATED, eventData); - } - - emitUserActivated(eventData: UserActivatedEventData): void { - this.innerEmit(EventName.USER_ACTIVATED, eventData); - } - onTransactionCompleted(listener: Listener): void { this.on(EventName.TRANSACTION_COMPLETED, listener); } diff --git a/server/src/core/event/event-name.ts b/server/src/core/event/event-name.ts index 92e99708..11065706 100644 --- a/server/src/core/event/event-name.ts +++ b/server/src/core/event/event-name.ts @@ -1,6 +1,4 @@ const USER_INVITATION_CREATED = 'userInvitationCreated'; const TRANSACTION_COMPLETED = 'transactionCompleted'; -const USER_CREATED = 'userCreated'; -const USER_ACTIVATED = 'userActivated'; -export { USER_INVITATION_CREATED, TRANSACTION_COMPLETED, USER_CREATED, USER_ACTIVATED }; +export { USER_INVITATION_CREATED, TRANSACTION_COMPLETED }; diff --git a/server/src/core/user/user-service.ts b/server/src/core/user/user-service.ts index adb1cbf0..0116069a 100644 --- a/server/src/core/user/user-service.ts +++ b/server/src/core/user/user-service.ts @@ -290,7 +290,6 @@ export class Users implements UserService { { upsert: true, returnOriginal: false, projection: NOMINATED_USER_PROJECTION } ); - this.eventBus.emitUserActivated({ user: getSafeUser(result.value) }); return getSafeUser(result.value); } catch (e) { From 45eda1707328bd17963ffb5aca4663dd0049c33c Mon Sep 17 00:00:00 2001 From: Jason Mahirwe Date: Mon, 11 Jan 2021 14:57:14 +0200 Subject: [PATCH 39/53] Redirect user to verify their phone number after signing up --- webapp/src/services/verifications.ts | 1 + webapp/src/store/actions.ts | 8 +++----- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/webapp/src/services/verifications.ts b/webapp/src/services/verifications.ts index ae217b31..60df44e0 100644 --- a/webapp/src/services/verifications.ts +++ b/webapp/src/services/verifications.ts @@ -4,6 +4,7 @@ import axios from 'axios'; export const Verifications = { async createPhoneVerificationRecord(phone: string) { const res = await axios.post(`/verifications/phone`, { phone }); + return res.data; }, async verifyPhone(args: PhoneVerificationArgs) { const { id, code } = args; diff --git a/webapp/src/store/actions.ts b/webapp/src/store/actions.ts index 4af1cc6b..02adb36f 100644 --- a/webapp/src/store/actions.ts +++ b/webapp/src/store/actions.ts @@ -131,11 +131,9 @@ const actions = wrapActions({ */ async createUser({ commit }, { name, phone, password, email, googleIdToken }: { name: string; phone: string; password: string; email: string; googleIdToken: string }) { const user = await Users.createUser({ name, phone, password, email, googleIdToken }); - await Users.login({ phone, password, googleIdToken }); - commit('setUser', user); - - if (user) { - router.push({ name: DEFAULT_SIGNED_IN_PAGE }); + const record = await Verifications.createPhoneVerificationRecord(user.phone); + if (record) { + router.push({ path: `/verifications/phone/${record._id}` }); } }, /** From 6efa0e21a2bbf6a2d8da30fff06a5e897a83d821 Mon Sep 17 00:00:00 2001 From: Jason Mahirwe Date: Mon, 11 Jan 2021 15:06:44 +0200 Subject: [PATCH 40/53] Remove event emitter emitUserActivated in User service method activateMiddleman --- server/src/core/user/user-service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/server/src/core/user/user-service.ts b/server/src/core/user/user-service.ts index 0116069a..54023eac 100644 --- a/server/src/core/user/user-service.ts +++ b/server/src/core/user/user-service.ts @@ -333,7 +333,6 @@ export class Users implements UserService { }, { upsert: true, returnOriginal: false, projection: NOMINATED_USER_PROJECTION } ); - this.eventBus.emitUserActivated({ user: getSafeUser(result.value) }); return getSafeUser(result.value); } catch (e) { From 6f6ac310e0d2ae853154ff42dfce29d44013fe50 Mon Sep 17 00:00:00 2001 From: Jason Mahirwe Date: Mon, 11 Jan 2021 15:14:28 +0200 Subject: [PATCH 41/53] Pass phone to User-service method create upon hitting endpoint for creating phone verification records --- server/src/rest/routes/verifications.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/rest/routes/verifications.ts b/server/src/rest/routes/verifications.ts index 20d0a5c5..d919148a 100644 --- a/server/src/rest/routes/verifications.ts +++ b/server/src/rest/routes/verifications.ts @@ -10,4 +10,4 @@ verifications.get('/phone/:id', wrapResponse( req => req.core.phoneVerification.getById(req.params.id))); verifications.post('/phone', wrapResponse( - req => req.core.phoneVerification.create(req.body.user))); \ No newline at end of file + req => req.core.phoneVerification.create(req.body.phone))); \ No newline at end of file From 1840802f398783ad60bffe6db2eea3eb9e454fed Mon Sep 17 00:00:00 2001 From: Jason Mahirwe Date: Mon, 11 Jan 2021 15:40:56 +0200 Subject: [PATCH 42/53] Hide sign-up dialog upon redirection to verify-phone page --- webapp/src/components/sign-up-modal.vue | 5 +++++ webapp/src/store/actions.ts | 1 + webapp/src/views/verify-phone.vue | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/webapp/src/components/sign-up-modal.vue b/webapp/src/components/sign-up-modal.vue index b909e4b7..a3fc26b3 100644 --- a/webapp/src/components/sign-up-modal.vue +++ b/webapp/src/components/sign-up-modal.vue @@ -266,6 +266,11 @@ export default { role: roles[0] } } + }, + phoneVerificationRecord(record) { + if (record) { + this.hideDialog(); + } } } } diff --git a/webapp/src/store/actions.ts b/webapp/src/store/actions.ts index 02adb36f..c09c6781 100644 --- a/webapp/src/store/actions.ts +++ b/webapp/src/store/actions.ts @@ -133,6 +133,7 @@ const actions = wrapActions({ const user = await Users.createUser({ name, phone, password, email, googleIdToken }); const record = await Verifications.createPhoneVerificationRecord(user.phone); if (record) { + commit('setPhoneVerificationRecord', record); router.push({ path: `/verifications/phone/${record._id}` }); } }, diff --git a/webapp/src/views/verify-phone.vue b/webapp/src/views/verify-phone.vue index d7944523..71590bd6 100644 --- a/webapp/src/views/verify-phone.vue +++ b/webapp/src/views/verify-phone.vue @@ -8,7 +8,7 @@
- Please enter the 6-digit phone verification code sent to 254... + Please enter the 6-digit phone verification code sent to {{phoneVerificationRecord.phone}} From cf7213c9474ea8f6e0c68dd4bf7cd51c2faa73cc Mon Sep 17 00:00:00 2001 From: Jason Mahirwe Date: Mon, 11 Jan 2021 15:43:29 +0200 Subject: [PATCH 43/53] Fetch phone verification record only if not available in state --- webapp/src/views/verify-phone.vue | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/webapp/src/views/verify-phone.vue b/webapp/src/views/verify-phone.vue index 71590bd6..4e4729d9 100644 --- a/webapp/src/views/verify-phone.vue +++ b/webapp/src/views/verify-phone.vue @@ -99,7 +99,9 @@ export default { } }, async mounted() { - await this.getPhoneVerificationRecord(this.$route.params.id); + if (!this.phoneVerificationRecord) { + await this.getPhoneVerificationRecord(this.$route.params.id); + } }, watch: { async phoneVerificationRecord(record) { From c74011eefac32cc81eb79a72f96f727be7230257 Mon Sep 17 00:00:00 2001 From: Jason Mahirwe Date: Mon, 11 Jan 2021 15:51:50 +0200 Subject: [PATCH 44/53] Reformat phone verification sms to be shorter --- .../src/core/link-generator/link-generator-service.ts | 3 ++- server/src/core/message/message.ts | 4 ++-- .../phone-verification/phone-verification-service.ts | 10 ++++------ server/src/core/phone-verification/types.ts | 2 +- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/server/src/core/link-generator/link-generator-service.ts b/server/src/core/link-generator/link-generator-service.ts index ba3d1335..5d3f0065 100644 --- a/server/src/core/link-generator/link-generator-service.ts +++ b/server/src/core/link-generator/link-generator-service.ts @@ -59,7 +59,8 @@ export class Links implements LinkGeneratorService { } async getPhoneVerificationLink(id: string, shorten: boolean = true): Promise { - const link: string = `${this.args.baseUrl}/verifications/phone/${id}`; + // const link: string = `${this.args.baseUrl}/verifications/phone/${id}`; + const link: string = `https://socialrelief.co/verifications/phone/${id}`; if (shorten) { return await this.args.shortener.shortenLink(link); } diff --git a/server/src/core/message/message.ts b/server/src/core/message/message.ts index e21f2bf7..0fc3d53d 100644 --- a/server/src/core/message/message.ts +++ b/server/src/core/message/message.ts @@ -51,8 +51,8 @@ export function createMonthlyDistributionReportEmailMessageForOccasionalDonor(do

`; } -export function createPhoneVerificationSms(user: User, code: number, verificationLink: string): string { - return `Hello ${extractFirstName(user.name)}, your phone number verification code is ${code}. Enter this code at ${verificationLink}` +export function createPhoneVerificationSms(code: number, verificationLink: string): string { + return `Social Relief verification code: ${code}.` } function beneficiariesAndAmountReceived(beneficiaries: User[], receivedAmount: number[], type: MessageType): string { diff --git a/server/src/core/phone-verification/phone-verification-service.ts b/server/src/core/phone-verification/phone-verification-service.ts index 14344fa8..855bb049 100644 --- a/server/src/core/phone-verification/phone-verification-service.ts +++ b/server/src/core/phone-verification/phone-verification-service.ts @@ -11,7 +11,6 @@ import { createDbOpFailedError, rethrowIfAppError, createUniquenessFailedError } from '../error'; import * as messages from '../messages'; import { EventBus, Event } from '../event'; -import { UserCreatedEventData, UserActivatedEventData } from '../user'; import * as validators from './validator'; const COLLECTION = 'phone-verifications'; @@ -63,10 +62,9 @@ export class PhoneVerification implements VerificationService { async create(phone: string): Promise { validators.validatesCreate(phone); try { - const user = await this.args.users.getByPhone(phone); const id = generateId(); // id to be given to the phone verification record const code = generatePhoneVerificationCode(); - await this.sendVerificationSms(user, id, code); + await this.sendVerificationSms(phone, id, code); const now = new Date(); const record: DbPhoneVerificationRecord = { @@ -129,11 +127,11 @@ export class PhoneVerification implements VerificationService { } } - async sendVerificationSms(user: User, id: string, code: number): Promise { + async sendVerificationSms(phone: string, id: string, code: number): Promise { try { const link = await this.args.links.getPhoneVerificationLink(id); - const smsMessage = createPhoneVerificationSms(user, code, link); - await this.args.smsProvider.sendSms(user.phone, smsMessage); + const smsMessage = createPhoneVerificationSms(code, link); + await this.args.smsProvider.sendSms(phone, smsMessage); this.createIndexes(); } catch(e) { diff --git a/server/src/core/phone-verification/types.ts b/server/src/core/phone-verification/types.ts index 2455fc8e..f8975856 100644 --- a/server/src/core/phone-verification/types.ts +++ b/server/src/core/phone-verification/types.ts @@ -9,7 +9,7 @@ export interface VerificationRecord { export interface VerificationService { createIndexes(): Promise; - sendVerificationSms(user: User, id: string, code: number): Promise; + sendVerificationSms(phone: string, id: string, code: number): Promise; confirmVerificationCode(id: string, code: number): Promise; getById(id: string): Promise; create(phone: string): Promise; From 380eb14af6bf85f29b168013c55ae391a6987648 Mon Sep 17 00:00:00 2001 From: Jason Mahirwe Date: Mon, 11 Jan 2021 16:19:14 +0200 Subject: [PATCH 45/53] Fetch phone verification record only if not available in state --- webapp/src/components/sign-up-modal.vue | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/webapp/src/components/sign-up-modal.vue b/webapp/src/components/sign-up-modal.vue index a3fc26b3..f0abc5d2 100644 --- a/webapp/src/components/sign-up-modal.vue +++ b/webapp/src/components/sign-up-modal.vue @@ -170,7 +170,7 @@ export default { GoogleButton }, computed: { - ...mapState(['user', 'newUser']), + ...mapState(['user', 'newUser', 'phoneVerificationRecord']), imageUrl () { return require(`@/assets/Social Relief Logo_1.svg`); }, @@ -267,10 +267,8 @@ export default { } } }, - phoneVerificationRecord(record) { - if (record) { - this.hideDialog(); - } + phoneVerificationRecord() { + this.$bvModal.hide('sign-up'); } } } From 3ce4efa6e149611e0fac967e6ca20a648bb98f88 Mon Sep 17 00:00:00 2001 From: Jason Mahirwe Date: Tue, 12 Jan 2021 15:50:54 +0200 Subject: [PATCH 46/53] Implement Phone Verification service method to resend new verification code --- .../link-generator/link-generator-service.ts | 3 +- .../phone-verification-service.ts | 40 +++++++++++++++++++ server/src/core/phone-verification/types.ts | 1 + .../phone-verification/validation-schemas.ts | 30 ++++++++++++++ .../src/core/phone-verification/validator.ts | 10 +++++ 5 files changed, 82 insertions(+), 2 deletions(-) diff --git a/server/src/core/link-generator/link-generator-service.ts b/server/src/core/link-generator/link-generator-service.ts index 5d3f0065..ba3d1335 100644 --- a/server/src/core/link-generator/link-generator-service.ts +++ b/server/src/core/link-generator/link-generator-service.ts @@ -59,8 +59,7 @@ export class Links implements LinkGeneratorService { } async getPhoneVerificationLink(id: string, shorten: boolean = true): Promise { - // const link: string = `${this.args.baseUrl}/verifications/phone/${id}`; - const link: string = `https://socialrelief.co/verifications/phone/${id}`; + const link: string = `${this.args.baseUrl}/verifications/phone/${id}`; if (shorten) { return await this.args.shortener.shortenLink(link); } diff --git a/server/src/core/phone-verification/phone-verification-service.ts b/server/src/core/phone-verification/phone-verification-service.ts index 855bb049..bce15210 100644 --- a/server/src/core/phone-verification/phone-verification-service.ts +++ b/server/src/core/phone-verification/phone-verification-service.ts @@ -142,6 +142,7 @@ export class PhoneVerification implements VerificationService { } public async confirmVerificationCode(id: string, code: number): Promise { + validators.validatesConfirmVerificationCode({ recordId: id, code}); try { const record = await this.collection.findOne( { _id: id, code }, @@ -179,4 +180,43 @@ export class PhoneVerification implements VerificationService { throw createDbOpFailedError(e.message); } } + + async resendVerificationCode(id: string): Promise { + 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); + } + } } \ No newline at end of file diff --git a/server/src/core/phone-verification/types.ts b/server/src/core/phone-verification/types.ts index f8975856..bf5ca79f 100644 --- a/server/src/core/phone-verification/types.ts +++ b/server/src/core/phone-verification/types.ts @@ -13,4 +13,5 @@ export interface VerificationService { confirmVerificationCode(id: string, code: number): Promise; getById(id: string): Promise; create(phone: string): Promise; + resendVerificationCode(id: string): Promise } \ No newline at end of file diff --git a/server/src/core/phone-verification/validation-schemas.ts b/server/src/core/phone-verification/validation-schemas.ts index 1a8a3de7..d39242af 100644 --- a/server/src/core/phone-verification/validation-schemas.ts +++ b/server/src/core/phone-verification/validation-schemas.ts @@ -1,6 +1,36 @@ import * as joi from '@hapi/joi'; import { phoneValidationSchema } from '../util/validation-util'; +const recordIdSchema = joi.string() + .required() + .pattern(/^[a-fA-F0-9]{32}$/) + .messages({ + 'any.required': `recordId is required`, + 'string.base': 'Invalid type, recordId must be a string', + 'string.empty': `Please enter recordId`, + 'string.pattern.base': `Invalid recordId. Must contain hexadecimals only and be 32 characters long` +}); + +const codeSchema = joi.number() + .required() + .min(100000) + .max(999999) + .messages({ + 'any.required': `Code is required`, + 'number.base': 'Invalid type, code must be a number', + 'number.min': `Code must be between 100000 and 999999, inclusive`, + }) + + export const createInputSchema = joi.object().keys({ phone: phoneValidationSchema, +}); + +export const confirmVerificationCodeInputSchema = joi.object().keys({ + recordId: recordIdSchema, + +}); + +export const resendVerificationCodeInputSchema = joi.object().keys({ + recordId: recordIdSchema }); \ No newline at end of file diff --git a/server/src/core/phone-verification/validator.ts b/server/src/core/phone-verification/validator.ts index 3f5b7455..d8dc4364 100644 --- a/server/src/core/phone-verification/validator.ts +++ b/server/src/core/phone-verification/validator.ts @@ -5,4 +5,14 @@ export const validatesCreate = (phone: string) => { const { error } = schemas.createInputSchema.validate({ phone }); if (error) throw createValidationError(error.details[0].message); if (phone[0] === '0') createValidationError('Phone number cannot start with 0'); +} + +export const validatesConfirmVerificationCode = ({ recordId, code}: { recordId: string, code: number}) => { + const { error } = schemas.confirmVerificationCodeInputSchema.validate({ recordId, code }); + if (error) throw createValidationError(error.details[0].message); +} + +export const validatesResendVerificationCode = (recordId: string) => { + const { error } = schemas.resendVerificationCodeInputSchema.validate({ recordId }); + if (error) throw createValidationError(error.details[0].message); } \ No newline at end of file From ef351aea4501ec06985ad1cb0d85131aa367b6fd Mon Sep 17 00:00:00 2001 From: Jason Mahirwe Date: Tue, 12 Jan 2021 16:06:59 +0200 Subject: [PATCH 47/53] Create endpoint for requesting resend of phone verification code --- server/src/rest/routes/verifications.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/src/rest/routes/verifications.ts b/server/src/rest/routes/verifications.ts index d919148a..7b3d2151 100644 --- a/server/src/rest/routes/verifications.ts +++ b/server/src/rest/routes/verifications.ts @@ -6,6 +6,9 @@ export const verifications = Router(); verifications.put('/phone/:id', wrapResponse( req => req.core.phoneVerification.confirmVerificationCode(req.params.id, req.body.code))); +verifications.put('/phone/resend/code/:id', wrapResponse( + req => req.core.phoneVerification.resendVerificationCode(req.params.id))); + verifications.get('/phone/:id', wrapResponse( req => req.core.phoneVerification.getById(req.params.id))); From a6bce31b5943c4cc770bad984ea1b0a5421f9c43 Mon Sep 17 00:00:00 2001 From: Jason Mahirwe Date: Tue, 12 Jan 2021 16:10:42 +0200 Subject: [PATCH 48/53] Implement action for resending phone verification code --- webapp/src/services/verifications.ts | 6 +++++- webapp/src/store/actions.ts | 4 ++++ webapp/src/views/verify-phone.vue | 10 ++++++++-- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/webapp/src/services/verifications.ts b/webapp/src/services/verifications.ts index 60df44e0..d8d93228 100644 --- a/webapp/src/services/verifications.ts +++ b/webapp/src/services/verifications.ts @@ -1,4 +1,4 @@ -import { PhoneVerificationRecord, PhoneVerificationArgs, User } from '../types'; +import { PhoneVerificationRecord, PhoneVerificationArgs } from '../types'; import axios from 'axios'; export const Verifications = { @@ -15,4 +15,8 @@ export const Verifications = { const res = await axios.get(`/verifications/phone/${recordId}`); return res.data; }, + async resendPhoneVerificationCode(recordId: string) { + const res = await axios.put(`/verifications/phone/resend/code/${recordId}`); + return res.data; + } } \ No newline at end of file diff --git a/webapp/src/store/actions.ts b/webapp/src/store/actions.ts index c09c6781..f075b075 100644 --- a/webapp/src/store/actions.ts +++ b/webapp/src/store/actions.ts @@ -85,6 +85,10 @@ const actions = wrapActions({ const record = await Verifications.verifyPhone({id, code}); commit('setPhoneVerificationRecord', record); }, + async resendPhoneVerificationCode({ commit }, id: string) { + const record = await Verifications.resendPhoneVerificationCode(id); + commit('setPhoneVerificationRecord', record); + }, async getPhoneVerificationRecord({commit}, id: string) { const record = await Verifications.getPhoneVerificationRecord(id); commit('setPhoneVerificationRecord', record); diff --git a/webapp/src/views/verify-phone.vue b/webapp/src/views/verify-phone.vue index 4e4729d9..a9038470 100644 --- a/webapp/src/views/verify-phone.vue +++ b/webapp/src/views/verify-phone.vue @@ -1,6 +1,6 @@