From d758f4939e8d2e7c2ea1e006d6cd19218c52c13c Mon Sep 17 00:00:00 2001 From: Vijay Budhram Date: Wed, 13 Nov 2024 11:56:41 -0500 Subject: [PATCH] feat(phone): Add `unconfirmed` to RecoveryPhoneManager --- .../src/lib/recovery-phone.manager.in.spec.ts | 55 ++++++++++++++++ .../src/lib/recovery-phone.manager.ts | 64 ++++++++++++++++++- 2 files changed, 118 insertions(+), 1 deletion(-) diff --git a/libs/accounts/recovery-phone/src/lib/recovery-phone.manager.in.spec.ts b/libs/accounts/recovery-phone/src/lib/recovery-phone.manager.in.spec.ts index b44fd3712ed..cfbda2fd690 100644 --- a/libs/accounts/recovery-phone/src/lib/recovery-phone.manager.in.spec.ts +++ b/libs/accounts/recovery-phone/src/lib/recovery-phone.manager.in.spec.ts @@ -11,6 +11,12 @@ describe('RecoveryPhoneManager', () => { let recoveryPhoneManager: RecoveryPhoneManager; let db: AccountDatabase; + const mockRedis = { + set: jest.fn(), + get: jest.fn(), + del: jest.fn(), + }; + beforeAll(async () => { db = await testAccountDatabaseSetup(['accounts', 'recoveryPhones']); const moduleRef = await Test.createTestingModule({ @@ -20,6 +26,10 @@ describe('RecoveryPhoneManager', () => { provide: AccountDbProvider, useValue: db, }, + { + provide: 'Redis', + useValue: mockRedis, + }, ], }).compile(); @@ -76,4 +86,49 @@ describe('RecoveryPhoneManager', () => { recoveryPhoneManager.registerPhoneNumber(uid.toString('hex'), phoneNumber) ).rejects.toThrow('Database error'); }); + + it('should store unconfirmed phone number data in Redis', async () => { + const mockPhone = RecoveryPhoneFactory(); + const { uid, phoneNumber } = mockPhone; + const code = '123456'; + const isSetup = true; + const lookupData = { foo: 'bar' }; + + await recoveryPhoneManager.storeUnconfirmed( + uid.toString('hex'), + code, + phoneNumber, + isSetup, + lookupData + ); + + const expectedData = JSON.stringify({ + phoneNumber, + isSetup, + lookupData: JSON.stringify(lookupData), + }); + const redisKey = `sms-attempt:${uid.toString('hex')}:${code}`; + + expect(mockRedis.set).toHaveBeenCalledWith( + redisKey, + expectedData, + 'EX', + 600 + ); + }); + + it('should return null if no unconfirmed phone number data is found in Redis', async () => { + const mockPhone = RecoveryPhoneFactory(); + const { uid } = mockPhone; + const code = '123456'; + + mockRedis.get.mockResolvedValue(null); + + const result = await recoveryPhoneManager.getUnconfirmed( + uid.toString('hex'), + code + ); + + expect(result).toBeNull(); + }); }); diff --git a/libs/accounts/recovery-phone/src/lib/recovery-phone.manager.ts b/libs/accounts/recovery-phone/src/lib/recovery-phone.manager.ts index 56cec056711..d76806e9870 100644 --- a/libs/accounts/recovery-phone/src/lib/recovery-phone.manager.ts +++ b/libs/accounts/recovery-phone/src/lib/recovery-phone.manager.ts @@ -12,11 +12,17 @@ import { RecoveryNumberAlreadyExistsError, RecoveryNumberInvalidFormatError, } from './recovery-phone.errors'; +import { Redis } from 'ioredis'; + +const RECORD_EXPIRATION_SECONDS = 10 * 60; @Injectable() export class RecoveryPhoneManager { + private readonly redisPrefix = 'sms-attempt'; + constructor( - @Inject(AccountDbProvider) private readonly db: AccountDatabase + @Inject(AccountDbProvider) private readonly db: AccountDatabase, + @Inject('Redis') private readonly redisClient: Redis ) {} private isE164Format(phoneNumber: string) { @@ -57,4 +63,60 @@ export class RecoveryPhoneManager { throw err; } } + + /** + * Store phone number data and SMS code for a user. + * + * @param uid The user's unique identifier + * @param code The SMS code to associate with this UID + * @param phoneNumber The phone number to store + * @param isSetup Flag indicating if this SMS is to set up a number or verify an existing one + * @param lookupData Optional lookup data for the phone number + */ + async storeUnconfirmed( + uid: string, + code: string, + phoneNumber: string, + isSetup: boolean, + lookupData?: Record + ): Promise { + const redisKey = `${this.redisPrefix}:${uid}:${code}`; + const data = { + phoneNumber, + isSetup, + lookupData: lookupData ? JSON.stringify(lookupData) : null, + }; + + await this.redisClient.set( + redisKey, + JSON.stringify(data), + 'EX', + RECORD_EXPIRATION_SECONDS + ); + } + + /** + * Retrieve phone number data for a user using uid and sms code. + * + * @param uid The user's unique identifier + * @param code The SMS code associated with this user + * @returns The stored phone number data if found, or null if not found + */ + async getUnconfirmed( + uid: string, + code: string + ): Promise<{ + phoneNumber: string; + isSetup: boolean; + lookupData: Record | null; + } | null> { + const redisKey = `${this.redisPrefix}:${uid}:${code}`; + const data = await this.redisClient.get(redisKey); + + if (!data) { + return null; + } + + return JSON.parse(data); + } }