feat(auth): add generating recovery codes

This commit is contained in:
Karol Sójko
2023-01-04 15:29:15 +01:00
parent 6ccc6ee42f
commit a360231fd0
6 changed files with 127 additions and 0 deletions
@@ -0,0 +1,65 @@
import { CryptoNode } from '@standardnotes/sncrypto-node'
import { SettingServiceInterface } from '../../Setting/SettingServiceInterface'
import { User } from '../../User/User'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { GenerateRecoveryCodes } from './GenerateRecoveryCodes'
describe('GenerateRecoveryCodes', () => {
let userRepository: UserRepositoryInterface
let settingService: SettingServiceInterface
let cryptoNode: CryptoNode
const createUseCase = () => new GenerateRecoveryCodes(userRepository, settingService, cryptoNode)
beforeEach(() => {
userRepository = {} as jest.Mocked<UserRepositoryInterface>
userRepository.findOneByUuid = jest.fn().mockReturnValue({} as jest.Mocked<User>)
settingService = {} as jest.Mocked<SettingServiceInterface>
settingService.createOrReplace = jest.fn()
cryptoNode = {} as jest.Mocked<CryptoNode>
cryptoNode.generateRandomKey = jest.fn().mockReturnValue('randomKey123')
})
it('should generate recovery codes', async () => {
const useCase = createUseCase()
const result = await useCase.execute({ userUuid: '2221101c-1da9-4d2b-9b32-b8be2a8d1c82' })
expect(settingService.createOrReplace).toHaveBeenCalled()
expect(result.isFailed()).toBeFalsy()
expect(result.getValue()).toEqual('RAND OMKE Y123')
})
it('should return error if empty random string', async () => {
cryptoNode.generateRandomKey = jest.fn().mockReturnValue('')
const useCase = createUseCase()
const result = await useCase.execute({ userUuid: '2221101c-1da9-4d2b-9b32-b8be2a8d1c82' })
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('Could not generate recovery codes: random key is invalid')
})
it('should return error if user not found', async () => {
userRepository.findOneByUuid = jest.fn().mockReturnValue(null)
const useCase = createUseCase()
const result = await useCase.execute({ userUuid: '2221101c-1da9-4d2b-9b32-b8be2a8d1c82' })
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('Could not generate recovery codes: user not found')
})
it('should return error if user uuid is invalid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({ userUuid: 'invalid' })
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('Could not generate recovery codes: Given value is not a valid uuid: invalid')
})
})
@@ -0,0 +1,47 @@
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import { SettingName } from '@standardnotes/settings'
import { CryptoNode } from '@standardnotes/sncrypto-node'
import { EncryptionVersion } from '../../Encryption/EncryptionVersion'
import { SettingServiceInterface } from '../../Setting/SettingServiceInterface'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { GenerateRecoveryCodesDTO } from './GenerateRecoveryCodesDTO'
export class GenerateRecoveryCodes implements UseCaseInterface<string> {
constructor(
private userRepository: UserRepositoryInterface,
private settingService: SettingServiceInterface,
private cryptoNode: CryptoNode,
) {}
async execute(dto: GenerateRecoveryCodesDTO): Promise<Result<string>> {
const userUuidOrError = Uuid.create(dto.userUuid)
if (userUuidOrError.isFailed()) {
return Result.fail(`Could not generate recovery codes: ${userUuidOrError.getError()}`)
}
const userUuid = userUuidOrError.getValue()
const user = await this.userRepository.findOneByUuid(userUuid.value)
if (user === null) {
return Result.fail('Could not generate recovery codes: user not found')
}
const randomKey = await this.cryptoNode.generateRandomKey(160)
const recoveryCodesSplit = randomKey.toUpperCase().match(/.{1,4}/g)
if (!recoveryCodesSplit) {
return Result.fail('Could not generate recovery codes: random key is invalid')
}
const recoveryCodes = recoveryCodesSplit.join(' ')
await this.settingService.createOrReplace({
user,
props: {
name: SettingName.RecoveryCodes,
unencryptedValue: recoveryCodes,
serverEncryptionVersion: EncryptionVersion.Default,
sensitive: true,
},
})
return Result.ok(recoveryCodes)
}
}
@@ -0,0 +1,3 @@
export interface GenerateRecoveryCodesDTO {
userUuid: string
}