Skip to content

Commit

Permalink
Merge pull request #18012 from mozilla/fxa-10346
Browse files Browse the repository at this point in the history
feat(phone): Add `unconfirmed` to RecoveryPhoneManager
  • Loading branch information
vbudhram authored Nov 14, 2024
2 parents 13f7bc8 + d758f49 commit a2a8c19
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -20,6 +26,10 @@ describe('RecoveryPhoneManager', () => {
provide: AccountDbProvider,
useValue: db,
},
{
provide: 'Redis',
useValue: mockRedis,
},
],
}).compile();

Expand Down Expand Up @@ -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();
});
});
64 changes: 63 additions & 1 deletion libs/accounts/recovery-phone/src/lib/recovery-phone.manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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<string, any>
): Promise<void> {
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<string, any> | null;
} | null> {
const redisKey = `${this.redisPrefix}:${uid}:${code}`;
const data = await this.redisClient.get(redisKey);

if (!data) {
return null;
}

return JSON.parse(data);
}
}

0 comments on commit a2a8c19

Please sign in to comment.