зеркало из https://github.com/mozilla/fxa.git
feat(2fa): Add `backup-code` libs
This commit is contained in:
Родитель
3e8bbdc416
Коммит
320a84f436
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"extends": ["../../../.eslintrc.json"],
|
||||
"ignorePatterns": ["!**/*"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": ["*.ts", "*.tsx"],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": ["*.js", "*.jsx"],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": ["*.json"],
|
||||
"parser": "jsonc-eslint-parser",
|
||||
"rules": {}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
# accounts-two-factor
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
|
||||
## Building
|
||||
|
||||
Run `nx build accounts-two-factor` to build the library.
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `nx test accounts-two-factor` to execute the unit tests via [Jest](https://jestjs.io).
|
|
@ -0,0 +1,11 @@
|
|||
/* eslint-disable */
|
||||
export default {
|
||||
displayName: 'accounts-two-factor',
|
||||
preset: '../../../jest.preset.js',
|
||||
testEnvironment: 'node',
|
||||
transform: {
|
||||
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'js', 'html'],
|
||||
coverageDirectory: '../../../coverage/libs/accounts/two-factor',
|
||||
};
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"name": "@fxa/accounts/two-factor",
|
||||
"version": "0.0.1"
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
"name": "accounts-two-factor",
|
||||
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "libs/accounts/two-factor/src",
|
||||
"projectType": "library",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@nx/esbuild:esbuild",
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"defaultConfiguration": "production",
|
||||
"options": {
|
||||
"outputPath": "dist/libs/accounts/two-factor",
|
||||
"tsConfig": "libs/accounts/two-factor/tsconfig.lib.json",
|
||||
"packageJson": "libs/accounts/two-factor/package.json",
|
||||
"main": "libs/accounts/two-factor/src/index.ts",
|
||||
"assets": ["libs/accounts/two-factor/*.md"],
|
||||
"platform": "node"
|
||||
},
|
||||
"configurations": {
|
||||
"development": {
|
||||
"minify": false
|
||||
},
|
||||
"production": {
|
||||
"minify": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"test-unit": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||
"options": {
|
||||
"jestConfig": "libs/accounts/two-factor/jest.config.ts",
|
||||
"testPathPattern": ["^(?!.*\\.in\\.spec\\.ts$).*$"]
|
||||
}
|
||||
},
|
||||
"test-integration": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||
"options": {
|
||||
"jestConfig": "libs/accounts/two-factor/jest.config.ts",
|
||||
"testPathPattern": ["\\.in\\.spec\\.ts$"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from './lib/backup-code.manager';
|
|
@ -0,0 +1,36 @@
|
|||
/* 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 { RecoveryCode } from './backup-code.types';
|
||||
|
||||
export const RecoveryCodeFactory = (
|
||||
override?: Partial<RecoveryCode>
|
||||
): RecoveryCode => ({
|
||||
uid: Buffer.from(
|
||||
faker.string.hexadecimal({
|
||||
length: 32,
|
||||
prefix: '',
|
||||
casing: 'lower',
|
||||
}),
|
||||
'hex'
|
||||
),
|
||||
codeHash: Buffer.from(
|
||||
faker.string.hexadecimal({
|
||||
length: 32,
|
||||
prefix: '',
|
||||
casing: 'lower',
|
||||
}),
|
||||
'hex'
|
||||
),
|
||||
salt: Buffer.from(
|
||||
faker.string.hexadecimal({
|
||||
length: 32,
|
||||
prefix: '',
|
||||
casing: 'lower',
|
||||
}),
|
||||
'hex'
|
||||
),
|
||||
...override,
|
||||
});
|
|
@ -0,0 +1,95 @@
|
|||
import { BackupCodeManager } from './backup-code.manager';
|
||||
import {
|
||||
AccountDatabase,
|
||||
AccountDbProvider,
|
||||
testAccountDatabaseSetup,
|
||||
} from '@fxa/shared/db/mysql/account';
|
||||
import { RecoveryCodeFactory } from './backup-code.factories';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { Test } from '@nestjs/testing';
|
||||
|
||||
function getMockUid() {
|
||||
return faker.string.hexadecimal({
|
||||
length: 32,
|
||||
prefix: '',
|
||||
casing: 'lower',
|
||||
});
|
||||
}
|
||||
|
||||
describe('BackupCodeManager', () => {
|
||||
let backupCodeManager: BackupCodeManager;
|
||||
let db: AccountDatabase;
|
||||
|
||||
async function createRecoveryCode(db: AccountDatabase, uid: string) {
|
||||
return db
|
||||
.insertInto('recoveryCodes')
|
||||
.values({
|
||||
...RecoveryCodeFactory({
|
||||
uid: Buffer.from(uid, 'hex'),
|
||||
}),
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
db = await testAccountDatabaseSetup(['accounts', 'recoveryCodes']);
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
providers: [
|
||||
BackupCodeManager,
|
||||
{
|
||||
provide: AccountDbProvider,
|
||||
useValue: db,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
backupCodeManager = moduleRef.get(BackupCodeManager);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await db.destroy();
|
||||
});
|
||||
|
||||
it('should return that the user has backup codes and count them', async () => {
|
||||
const mockUid = getMockUid();
|
||||
await createRecoveryCode(db, mockUid);
|
||||
await createRecoveryCode(db, mockUid);
|
||||
await createRecoveryCode(db, mockUid);
|
||||
|
||||
const result = await backupCodeManager.getCountForUserId(mockUid);
|
||||
expect(result.hasBackupCodes).toBe(true);
|
||||
expect(result.count).toBe(3);
|
||||
});
|
||||
|
||||
it('should return that the user has no backup codes', async () => {
|
||||
const result = await backupCodeManager.getCountForUserId('abcd');
|
||||
expect(result.hasBackupCodes).toBe(false);
|
||||
expect(result.count).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle multiple users with different backup code counts', async () => {
|
||||
const mockUid1 = getMockUid();
|
||||
const mockUid2 = getMockUid();
|
||||
await createRecoveryCode(db, mockUid1);
|
||||
await createRecoveryCode(db, mockUid1);
|
||||
await createRecoveryCode(db, mockUid2);
|
||||
|
||||
const result1 = await backupCodeManager.getCountForUserId(mockUid1);
|
||||
const result2 = await backupCodeManager.getCountForUserId(mockUid2);
|
||||
|
||||
expect(result1.hasBackupCodes).toBe(true);
|
||||
expect(result1.count).toBe(2);
|
||||
expect(result2.hasBackupCodes).toBe(true);
|
||||
expect(result2.count).toBe(1);
|
||||
});
|
||||
|
||||
it('should handle database errors gracefully', async () => {
|
||||
jest.spyOn(db, 'selectFrom').mockImplementation(() => {
|
||||
throw new Error('Database error');
|
||||
});
|
||||
|
||||
await expect(
|
||||
backupCodeManager.getCountForUserId(getMockUid())
|
||||
).rejects.toThrow('Database error');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,37 @@
|
|||
/* 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 { getRecoveryCodes } from './backup-code.repository';
|
||||
|
||||
@Injectable()
|
||||
export class BackupCodeManager {
|
||||
constructor(
|
||||
@Inject(AccountDbProvider) private readonly db: AccountDatabase
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Gets the count of recover codes for a given uid.
|
||||
*
|
||||
* @param uid - The uid in hexadecimal string format.
|
||||
* @returns An object containing whether the user has backup codes and the count of backup codes.
|
||||
*/
|
||||
async getCountForUserId(
|
||||
uid: string
|
||||
): Promise<{ hasBackupCodes: boolean; count: number }> {
|
||||
const recoveryCodes = await getRecoveryCodes(
|
||||
this.db,
|
||||
Buffer.from(uid, 'hex')
|
||||
);
|
||||
|
||||
return {
|
||||
hasBackupCodes: recoveryCodes.length > 0,
|
||||
count: recoveryCodes.length,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
|
||||
export async function getRecoveryCodes(db: AccountDatabase, uid: Buffer) {
|
||||
return await db
|
||||
.selectFrom('recoveryCodes')
|
||||
.where('uid', '=', uid)
|
||||
.selectAll()
|
||||
.execute();
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
/* 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 RecoveryCode {
|
||||
uid: Buffer;
|
||||
codeHash: Buffer;
|
||||
salt: Buffer;
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs"
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"declaration": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"]
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"module": "commonjs",
|
||||
"types": ["jest", "node"]
|
||||
},
|
||||
"include": [
|
||||
"jest.config.ts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
|
@ -15,6 +15,7 @@ export type ACCOUNT_TABLES =
|
|||
| 'accountCustomers'
|
||||
| 'paypalCustomers'
|
||||
| 'carts'
|
||||
| 'recoveryCodes'
|
||||
| 'emails';
|
||||
|
||||
export async function testAccountDatabaseSetup(
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
CREATE TABLE `recoveryCodes` (
|
||||
`id` bigint AUTO_INCREMENT PRIMARY KEY,
|
||||
`uid` binary(16) DEFAULT NULL,
|
||||
`codeHash` binary(32) DEFAULT NULL,
|
||||
`salt` binary(32) DEFAULT NULL,
|
||||
KEY `uid` (`uid`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
|
@ -27,6 +27,9 @@
|
|||
"@fxa/accounts/recovery-phone": [
|
||||
"libs/accounts/recovery-phone/src/index.ts"
|
||||
],
|
||||
"@fxa/accounts/two-factor": [
|
||||
"libs/accounts/two-factor/src/index.ts"
|
||||
],
|
||||
"@fxa/payments/capability": ["libs/payments/capability/src/index.ts"],
|
||||
"@fxa/payments/cart": ["libs/payments/cart/src/index.ts"],
|
||||
"@fxa/payments/currency": ["libs/payments/currency/src/index.ts"],
|
||||
|
@ -93,7 +96,9 @@
|
|||
"@fxa/vendored/jwtool": ["libs/vendored/jwtool/src/index.ts"],
|
||||
"@fxa/vendored/typesafe-node-firestore": [
|
||||
"libs/vendored/typesafe-node-firestore/src/index.ts"
|
||||
]
|
||||
],
|
||||
"accounts/recovery-phone": ["libs/accounts/recovery-phone/src/index.ts"],
|
||||
"accounts/two-factor": ["libs/accounts/two-factor/src/index.ts"]
|
||||
},
|
||||
"typeRoots": [
|
||||
"./types",
|
||||
|
|
Загрузка…
Ссылка в новой задаче