зеркало из https://github.com/mozilla/fxa.git
Merge pull request #17979 from mozilla/fxa-10341
feat(phone): Add creat methods for recovery phone lib
This commit is contained in:
Коммит
4fef7bb7ef
|
@ -21,7 +21,16 @@
|
|||
"executor": "@nx/jest:jest",
|
||||
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||
"options": {
|
||||
"jestConfig": "libs/accounts/recovery-phone/jest.config.ts"
|
||||
"jestConfig": "libs/accounts/recovery-phone/jest.config.ts",
|
||||
"testPathPattern": ["^(?!.*\\.in\\.spec\\.ts$).*$"]
|
||||
}
|
||||
},
|
||||
"test-integration": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||
"options": {
|
||||
"jestConfig": "libs/accounts/recovery-phone/jest.config.ts",
|
||||
"testPathPattern": ["\\.in\\.spec\\.ts$"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export * from './lib/recovery-phone';
|
||||
export * from './lib/recovery-phone.manager';
|
||||
export * from './lib/sms.manager';
|
||||
export * from './lib/twilio.config';
|
||||
export * from './lib/twilio.provider';
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { BaseError } from '@fxa/shared/error';
|
||||
|
||||
export class RecoveryPhoneError extends BaseError {
|
||||
constructor(message: string, info: Record<string, any>, cause?: Error) {
|
||||
super(message, {
|
||||
name: 'RecoveryPhoneError',
|
||||
cause,
|
||||
info,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class RecoveryNumberInvalidFormatError extends RecoveryPhoneError {
|
||||
constructor(uid: string, phoneNumber: string, cause?: Error) {
|
||||
super('Invalid phone number format', { uid, phoneNumber }, cause);
|
||||
}
|
||||
}
|
||||
|
||||
export class RecoveryNumberAlreadyExistsError extends RecoveryPhoneError {
|
||||
constructor(uid: string, phoneNumber: string, cause?: Error) {
|
||||
super('Recovery number already exists', { uid, phoneNumber }, cause);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { RecoveryPhone } from './recovery-phone.types';
|
||||
|
||||
export const RecoveryPhoneFactory = (override?: Partial<RecoveryPhone>) => ({
|
||||
uid: Buffer.from(
|
||||
faker.string.hexadecimal({
|
||||
length: 32,
|
||||
prefix: '',
|
||||
casing: 'lower',
|
||||
}),
|
||||
'hex'
|
||||
),
|
||||
phoneNumber: faker.phone.number({ style: 'international' }),
|
||||
createdAt: Date.now(),
|
||||
lastConfirmed: Date.now(),
|
||||
lookupData: JSON.stringify({
|
||||
a: 'test',
|
||||
b: 'test2',
|
||||
}),
|
||||
...override,
|
||||
});
|
|
@ -0,0 +1,79 @@
|
|||
import { RecoveryPhoneManager } from './recovery-phone.manager';
|
||||
import {
|
||||
AccountDatabase,
|
||||
AccountDbProvider,
|
||||
testAccountDatabaseSetup,
|
||||
} from '@fxa/shared/db/mysql/account';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { RecoveryPhoneFactory } from './recovery-phone.factories';
|
||||
|
||||
describe('RecoveryPhoneManager', () => {
|
||||
let recoveryPhoneManager: RecoveryPhoneManager;
|
||||
let db: AccountDatabase;
|
||||
|
||||
beforeAll(async () => {
|
||||
db = await testAccountDatabaseSetup(['accounts', 'recoveryPhones']);
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
providers: [
|
||||
RecoveryPhoneManager,
|
||||
{
|
||||
provide: AccountDbProvider,
|
||||
useValue: db,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
recoveryPhoneManager = moduleRef.get(RecoveryPhoneManager);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await db.destroy();
|
||||
});
|
||||
|
||||
it('should create a recovery phone', async () => {
|
||||
const insertIntoSpy = jest.spyOn(db, 'insertInto');
|
||||
const mockPhone = RecoveryPhoneFactory();
|
||||
await recoveryPhoneManager.registerPhoneNumber(
|
||||
mockPhone.uid.toString('hex'),
|
||||
mockPhone.phoneNumber
|
||||
);
|
||||
|
||||
expect(insertIntoSpy).toBeCalledWith('recoveryPhones');
|
||||
});
|
||||
|
||||
it('should fail for invalid format phone number', async () => {
|
||||
const mockPhone = RecoveryPhoneFactory();
|
||||
const phoneNumber = '1234567890a';
|
||||
await expect(
|
||||
recoveryPhoneManager.registerPhoneNumber(
|
||||
mockPhone.uid.toString('hex'),
|
||||
phoneNumber
|
||||
)
|
||||
).rejects.toThrow('Invalid phone number format');
|
||||
});
|
||||
|
||||
it('should fail to register if recovery phone already exists', async () => {
|
||||
const mockPhone = RecoveryPhoneFactory();
|
||||
const { uid, phoneNumber } = mockPhone;
|
||||
await recoveryPhoneManager.registerPhoneNumber(
|
||||
uid.toString('hex'),
|
||||
phoneNumber
|
||||
);
|
||||
|
||||
await expect(
|
||||
recoveryPhoneManager.registerPhoneNumber(uid.toString('hex'), phoneNumber)
|
||||
).rejects.toThrow('Recovery number already exists');
|
||||
});
|
||||
|
||||
it('should handle database errors gracefully', async () => {
|
||||
jest.spyOn(db, 'insertInto').mockImplementation(() => {
|
||||
throw new Error('Database error');
|
||||
});
|
||||
|
||||
const mockPhone = RecoveryPhoneFactory();
|
||||
const { uid, phoneNumber } = mockPhone;
|
||||
await expect(
|
||||
recoveryPhoneManager.registerPhoneNumber(uid.toString('hex'), phoneNumber)
|
||||
).rejects.toThrow('Database error');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,60 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import {
|
||||
AccountDatabase,
|
||||
AccountDbProvider,
|
||||
} from '@fxa/shared/db/mysql/account';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { registerPhoneNumber } from './recovery-phone.repository';
|
||||
import {
|
||||
RecoveryNumberAlreadyExistsError,
|
||||
RecoveryNumberInvalidFormatError,
|
||||
} from './recovery-phone.errors';
|
||||
|
||||
@Injectable()
|
||||
export class RecoveryPhoneManager {
|
||||
constructor(
|
||||
@Inject(AccountDbProvider) private readonly db: AccountDatabase
|
||||
) {}
|
||||
|
||||
private isE164Format(phoneNumber: string) {
|
||||
const e164Regex = /^\+?[1-9]\d{1,14}$/;
|
||||
return e164Regex.test(phoneNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a phone number for account recovery.
|
||||
*
|
||||
* @throws {RecoveryNumberAlreadyExistsError} if the phone number is already registered.
|
||||
* @param uid
|
||||
* @param phoneNumber Phone number in E.164 format.
|
||||
*/
|
||||
async registerPhoneNumber(uid: string, phoneNumber: string): Promise<any> {
|
||||
if (!this.isE164Format(phoneNumber)) {
|
||||
throw new RecoveryNumberInvalidFormatError(uid, phoneNumber);
|
||||
}
|
||||
|
||||
const uidBuffer = Buffer.from(uid, 'hex');
|
||||
|
||||
// TODO: Perform phone number validation here via https://www.twilio.com/docs/lookup/v2-api#making-a-request
|
||||
const lookupData = {};
|
||||
|
||||
const now = Date.now();
|
||||
try {
|
||||
return await registerPhoneNumber(this.db, {
|
||||
uid: uidBuffer,
|
||||
phoneNumber,
|
||||
createdAt: now,
|
||||
lastConfirmed: now,
|
||||
lookupData: JSON.stringify(lookupData),
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.code === 'ER_DUP_ENTRY') {
|
||||
throw new RecoveryNumberAlreadyExistsError(uid, phoneNumber);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
import { AccountDatabase } from '@fxa/shared/db/mysql/account';
|
||||
import { RecoveryPhone } from './recovery-phone.types';
|
||||
|
||||
export async function registerPhoneNumber(
|
||||
db: AccountDatabase,
|
||||
recoveryPhone: RecoveryPhone
|
||||
) {
|
||||
return await db.insertInto('recoveryPhones').values(recoveryPhone).execute();
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
import { recoveryPhone } from './recovery-phone';
|
||||
|
||||
describe('recoveryPhone', () => {
|
||||
it('should work', () => {
|
||||
expect(recoveryPhone()).toEqual('recovery-phone');
|
||||
});
|
||||
});
|
|
@ -1,3 +0,0 @@
|
|||
export function recoveryPhone(): string {
|
||||
return 'recovery-phone';
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
export interface RecoveryPhone {
|
||||
uid: Buffer;
|
||||
phoneNumber: string;
|
||||
createdAt: number;
|
||||
lastConfirmed: number;
|
||||
lookupData: string;
|
||||
}
|
|
@ -219,6 +219,14 @@ export interface RecoveryKeys {
|
|||
hint: string | null;
|
||||
}
|
||||
|
||||
export interface RecoveryPhones {
|
||||
createdAt: number;
|
||||
lastConfirmed: number;
|
||||
lookupData: Json | null;
|
||||
phoneNumber: string;
|
||||
uid: Buffer;
|
||||
}
|
||||
|
||||
export interface SecurityEventNames {
|
||||
id: Generated<number>;
|
||||
name: string;
|
||||
|
@ -317,6 +325,7 @@ export interface DB {
|
|||
paypalCustomers: PaypalCustomers;
|
||||
recoveryCodes: RecoveryCodes;
|
||||
recoveryKeys: RecoveryKeys;
|
||||
recoveryPhones: RecoveryPhones;
|
||||
securityEventNames: SecurityEventNames;
|
||||
securityEvents: SecurityEvents;
|
||||
sentEmails: SentEmails;
|
||||
|
|
|
@ -16,6 +16,7 @@ export type ACCOUNT_TABLES =
|
|||
| 'paypalCustomers'
|
||||
| 'carts'
|
||||
| 'recoveryCodes'
|
||||
| 'recoveryPhones'
|
||||
| 'emails';
|
||||
|
||||
export async function testAccountDatabaseSetup(
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
CREATE TABLE `recoveryPhones` (
|
||||
`uid` BINARY(16) NOT NULL,
|
||||
`phoneNumber` VARCHAR(15) NOT NULL,
|
||||
`createdAt` BIGINT UNSIGNED NOT NULL,
|
||||
`lastConfirmed` BIGINT UNSIGNED NOT NULL,
|
||||
`lookupData` JSON,
|
||||
PRIMARY KEY `uid` (`uid`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
|
@ -0,0 +1,20 @@
|
|||
SET NAMES utf8mb4 COLLATE utf8mb4_bin;
|
||||
|
||||
CALL assertPatchLevel('158');
|
||||
|
||||
-- Create the 'recoveryPhone' table.
|
||||
-- Per spec, the phone number is only inserted if it has been verified.
|
||||
-- Phone number is in E.164 format and 15 chars max length.
|
||||
-- `lookupData` contains data from https://www.twilio.com/docs/lookup/v2-api
|
||||
CREATE TABLE recoveryPhones (
|
||||
uid BINARY(16) NOT NULL,
|
||||
phoneNumber VARCHAR(15) NOT NULL,
|
||||
createdAt BIGINT UNSIGNED NOT NULL,
|
||||
lastConfirmed BIGINT UNSIGNED NOT NULL,
|
||||
lookupData JSON,
|
||||
PRIMARY KEY (uid),
|
||||
INDEX idx_phoneNumber (phoneNumber),
|
||||
FOREIGN KEY (uid) REFERENCES accounts(uid) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
UPDATE dbMetadata SET value = '159' WHERE name = 'schema-patch-level';
|
|
@ -0,0 +1,3 @@
|
|||
-- DROP TABLE `recoveryPhones`;
|
||||
|
||||
-- UPDATE dbMetadata SET value = '158' WHERE name = 'schema-patch-level';
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"level": 158
|
||||
"level": 159
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче