mirror of
https://github.com/standardnotes/server
synced 2026-09-12 15:45:26 -04:00
feat(auth): add removing authenticator
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { inject } from 'inversify'
|
||||
import { Request, Response } from 'express'
|
||||
import { controller, BaseHttpController, httpPost, httpGet } from 'inversify-express-utils'
|
||||
import { controller, BaseHttpController, httpPost, httpGet, httpDelete } from 'inversify-express-utils'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
|
||||
@@ -11,6 +11,16 @@ export class AuthenticatorsController extends BaseHttpController {
|
||||
super()
|
||||
}
|
||||
|
||||
@httpDelete('/:authenticatorId')
|
||||
async delete(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(
|
||||
request,
|
||||
response,
|
||||
`authenticators/${request.params.authenticatorId}`,
|
||||
request.body,
|
||||
)
|
||||
}
|
||||
|
||||
@httpGet('/')
|
||||
async list(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(request, response, 'authenticators/', request.body)
|
||||
|
||||
@@ -221,6 +221,7 @@ import { AuthenticatorsController } from '../Controller/AuthenticatorsController
|
||||
import { ListAuthenticators } from '../Domain/UseCase/ListAuthenticators/ListAuthenticators'
|
||||
import { AuthenticatorHttpProjection } from '../Infra/Http/Projection/AuthenticatorHttpProjection'
|
||||
import { AuthenticatorHttpMapper } from '../Mapping/AuthenticatorHttpMapper'
|
||||
import { DeleteAuthenticator } from '../Domain/UseCase/DeleteAuthenticator/DeleteAuthenticator'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const newrelicFormatter = require('@newrelic/winston-enricher')
|
||||
@@ -592,6 +593,9 @@ export class ContainerConfigLoader {
|
||||
container
|
||||
.bind<ListAuthenticators>(TYPES.ListAuthenticators)
|
||||
.toConstantValue(new ListAuthenticators(container.get(TYPES.AuthenticatorRepository)))
|
||||
container
|
||||
.bind<DeleteAuthenticator>(TYPES.DeleteAuthenticator)
|
||||
.toConstantValue(new DeleteAuthenticator(container.get(TYPES.AuthenticatorRepository)))
|
||||
|
||||
container
|
||||
.bind<CleanupSessionTraces>(TYPES.CleanupSessionTraces)
|
||||
@@ -657,6 +661,7 @@ export class ContainerConfigLoader {
|
||||
container.get(TYPES.GenerateAuthenticatorAuthenticationOptions),
|
||||
container.get(TYPES.VerifyAuthenticatorAuthenticationResponse),
|
||||
container.get(TYPES.ListAuthenticators),
|
||||
container.get(TYPES.DeleteAuthenticator),
|
||||
container.get(TYPES.AuthenticatorHttpMapper),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -140,6 +140,7 @@ const TYPES = {
|
||||
GenerateAuthenticatorAuthenticationOptions: Symbol.for('GenerateAuthenticatorAuthenticationOptions'),
|
||||
VerifyAuthenticatorAuthenticationResponse: Symbol.for('VerifyAuthenticatorAuthenticationResponse'),
|
||||
ListAuthenticators: Symbol.for('ListAuthenticators'),
|
||||
DeleteAuthenticator: Symbol.for('DeleteAuthenticator'),
|
||||
// Handlers
|
||||
UserRegisteredEventHandler: Symbol.for('UserRegisteredEventHandler'),
|
||||
AccountDeletionRequestedEventHandler: Symbol.for('AccountDeletionRequestedEventHandler'),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { HttpStatusCode } from '@standardnotes/api'
|
||||
import { MapperInterface } from '@standardnotes/domain-core'
|
||||
import { Authenticator } from '../Domain/Authenticator/Authenticator'
|
||||
import { DeleteAuthenticator } from '../Domain/UseCase/DeleteAuthenticator/DeleteAuthenticator'
|
||||
|
||||
import { GenerateAuthenticatorAuthenticationOptions } from '../Domain/UseCase/GenerateAuthenticatorAuthenticationOptions/GenerateAuthenticatorAuthenticationOptions'
|
||||
import { GenerateAuthenticatorRegistrationOptions } from '../Domain/UseCase/GenerateAuthenticatorRegistrationOptions/GenerateAuthenticatorRegistrationOptions'
|
||||
@@ -8,11 +9,13 @@ import { ListAuthenticators } from '../Domain/UseCase/ListAuthenticators/ListAut
|
||||
import { VerifyAuthenticatorAuthenticationResponse } from '../Domain/UseCase/VerifyAuthenticatorAuthenticationResponse/VerifyAuthenticatorAuthenticationResponse'
|
||||
import { VerifyAuthenticatorRegistrationResponse } from '../Domain/UseCase/VerifyAuthenticatorRegistrationResponse/VerifyAuthenticatorRegistrationResponse'
|
||||
import { AuthenticatorHttpProjection } from '../Infra/Http/Projection/AuthenticatorHttpProjection'
|
||||
import { DeleteAuthenticatorRequestParams } from '../Infra/Http/Request/DeleteAuthenticatorRequestParams'
|
||||
import { GenerateAuthenticatorAuthenticationOptionsRequestParams } from '../Infra/Http/Request/GenerateAuthenticatorAuthenticationOptionsRequestParams'
|
||||
import { GenerateAuthenticatorRegistrationOptionsRequestParams } from '../Infra/Http/Request/GenerateAuthenticatorRegistrationOptionsRequestParams'
|
||||
import { ListAuthenticatorsRequestParams } from '../Infra/Http/Request/ListAuthenticatorsRequestParams'
|
||||
import { VerifyAuthenticatorAuthenticationResponseRequestParams } from '../Infra/Http/Request/VerifyAuthenticatorAuthenticationResponseRequestParams'
|
||||
import { VerifyAuthenticatorRegistrationResponseRequestParams } from '../Infra/Http/Request/VerifyAuthenticatorRegistrationResponseRequestParams'
|
||||
import { DeleteAuthenticatorResponse } from '../Infra/Http/Response/DeleteAuthenticatorResponse'
|
||||
import { GenerateAuthenticatorAuthenticationOptionsResponse } from '../Infra/Http/Response/GenerateAuthenticatorAuthenticationOptionsResponse'
|
||||
import { GenerateAuthenticatorRegistrationOptionsResponse } from '../Infra/Http/Response/GenerateAuthenticatorRegistrationOptionsResponse'
|
||||
import { ListAuthenticatorsResponse } from '../Infra/Http/Response/ListAuthenticatorsResponse'
|
||||
@@ -26,6 +29,7 @@ export class AuthenticatorsController {
|
||||
private generateAuthenticatorAuthenticationOptions: GenerateAuthenticatorAuthenticationOptions,
|
||||
private verifyAuthenticatorAuthenticationResponse: VerifyAuthenticatorAuthenticationResponse,
|
||||
private listAuthenticators: ListAuthenticators,
|
||||
private deleteAuthenticator: DeleteAuthenticator,
|
||||
private authenticatorHttpMapper: MapperInterface<Authenticator, AuthenticatorHttpProjection>,
|
||||
) {}
|
||||
|
||||
@@ -44,6 +48,20 @@ export class AuthenticatorsController {
|
||||
}
|
||||
}
|
||||
|
||||
async delete(params: DeleteAuthenticatorRequestParams): Promise<DeleteAuthenticatorResponse> {
|
||||
const result = await this.deleteAuthenticator.execute({
|
||||
userUuid: params.userUuid,
|
||||
authenticatorId: params.authenticatorId,
|
||||
})
|
||||
|
||||
return {
|
||||
status: HttpStatusCode.Success,
|
||||
data: {
|
||||
message: result.getValue(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async generateRegistrationOptions(
|
||||
params: GenerateAuthenticatorRegistrationOptionsRequestParams,
|
||||
): Promise<GenerateAuthenticatorRegistrationOptionsResponse> {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
import { UniqueEntityId, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import { Authenticator } from './Authenticator'
|
||||
|
||||
export interface AuthenticatorRepositoryInterface {
|
||||
findByUserUuid(userUuid: Uuid): Promise<Authenticator[]>
|
||||
findById(id: UniqueEntityId): Promise<Authenticator | null>
|
||||
findByUserUuidAndCredentialId(userUuid: Uuid, credentialId: Buffer): Promise<Authenticator | null>
|
||||
save(authenticator: Authenticator): Promise<void>
|
||||
remove(authenticator: Authenticator): Promise<void>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { Dates, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import { Authenticator } from '../../Authenticator/Authenticator'
|
||||
import { AuthenticatorRepositoryInterface } from '../../Authenticator/AuthenticatorRepositoryInterface'
|
||||
import { DeleteAuthenticator } from './DeleteAuthenticator'
|
||||
|
||||
describe('DeleteAuthenticator', () => {
|
||||
let authenticatorRepository: AuthenticatorRepositoryInterface
|
||||
let authenticator: Authenticator
|
||||
const createUseCase = () => new DeleteAuthenticator(authenticatorRepository)
|
||||
|
||||
beforeEach(() => {
|
||||
authenticator = Authenticator.create({
|
||||
counter: 1,
|
||||
name: 'my-key',
|
||||
credentialBackedUp: true,
|
||||
credentialDeviceType: 'singleDevice',
|
||||
credentialId: Buffer.from('credentialId'),
|
||||
credentialPublicKey: Buffer.from('credentialPublicKey'),
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
dates: Dates.create(new Date(1), new Date(1)).getValue(),
|
||||
transports: ['usb'],
|
||||
}).getValue()
|
||||
|
||||
authenticatorRepository = {} as jest.Mocked<AuthenticatorRepositoryInterface>
|
||||
authenticatorRepository.findById = jest.fn().mockReturnValue(authenticator)
|
||||
authenticatorRepository.remove = jest.fn()
|
||||
})
|
||||
|
||||
it('should return error if authenticator not found', async () => {
|
||||
authenticatorRepository.findById = jest.fn().mockReturnValue(null)
|
||||
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
authenticatorId: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
expect(result.getError()).toEqual('Authenticator not found')
|
||||
})
|
||||
|
||||
it('should return error if authenticator does not belong to user', async () => {
|
||||
authenticatorRepository.findById = jest.fn().mockReturnValue({
|
||||
...authenticator,
|
||||
props: {
|
||||
...authenticator.props,
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-00000000a000').getValue(),
|
||||
},
|
||||
})
|
||||
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
authenticatorId: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
expect(result.getError()).toEqual('Authenticator not found')
|
||||
})
|
||||
|
||||
it('should delete authenticator', async () => {
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
authenticatorId: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(false)
|
||||
expect(result.getValue()).toEqual('Authenticator deleted')
|
||||
expect(authenticatorRepository.remove).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Result, UniqueEntityId, UseCaseInterface } from '@standardnotes/domain-core'
|
||||
import { AuthenticatorRepositoryInterface } from '../../Authenticator/AuthenticatorRepositoryInterface'
|
||||
import { DeleteAuthenticatorDTO } from './DeleteAuthenticatorDTO'
|
||||
|
||||
export class DeleteAuthenticator implements UseCaseInterface<string> {
|
||||
constructor(private authenticatorRepository: AuthenticatorRepositoryInterface) {}
|
||||
async execute(dto: DeleteAuthenticatorDTO): Promise<Result<string>> {
|
||||
const authenticator = await this.authenticatorRepository.findById(new UniqueEntityId(dto.authenticatorId))
|
||||
if (!authenticator || authenticator.props.userUuid.value !== dto.userUuid) {
|
||||
return Result.fail('Authenticator not found')
|
||||
}
|
||||
|
||||
await this.authenticatorRepository.remove(authenticator)
|
||||
|
||||
return Result.ok('Authenticator deleted')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface DeleteAuthenticatorDTO {
|
||||
userUuid: string
|
||||
authenticatorId: string
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface DeleteAuthenticatorRequestParams {
|
||||
userUuid: string
|
||||
authenticatorId: string
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { HttpErrorResponseBody, HttpResponse } from '@standardnotes/api'
|
||||
import { Either } from '@standardnotes/common'
|
||||
|
||||
import { DeleteAuthenticatorResponseBody } from './DeleteAuthenticatorResponseBody'
|
||||
|
||||
export interface DeleteAuthenticatorResponse extends HttpResponse {
|
||||
data: Either<DeleteAuthenticatorResponseBody, HttpErrorResponseBody>
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface DeleteAuthenticatorResponseBody {
|
||||
message: string
|
||||
}
|
||||
+11
@@ -3,6 +3,7 @@ import { inject } from 'inversify'
|
||||
import {
|
||||
BaseHttpController,
|
||||
controller,
|
||||
httpDelete,
|
||||
httpGet,
|
||||
httpPost,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@@ -26,6 +27,16 @@ export class InversifyExpressAuthenticatorsController extends BaseHttpController
|
||||
return this.json(result.data, result.status)
|
||||
}
|
||||
|
||||
@httpDelete('/:authenticatorId')
|
||||
async delete(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const result = await this.authenticatorsController.delete({
|
||||
userUuid: response.locals.user.uuid,
|
||||
authenticatorId: request.params.authenticatorId,
|
||||
})
|
||||
|
||||
return this.json(result.data, result.status)
|
||||
}
|
||||
|
||||
@httpGet('/generate-registration-options')
|
||||
async generateRegistrationOptions(_request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const result = await this.authenticatorsController.generateRegistrationOptions({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MapperInterface, Uuid } from '@standardnotes/domain-core'
|
||||
import { MapperInterface, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
|
||||
import { Repository } from 'typeorm'
|
||||
|
||||
import { Authenticator } from '../../Domain/Authenticator/Authenticator'
|
||||
@@ -11,6 +11,25 @@ export class MySQLAuthenticatorRepository implements AuthenticatorRepositoryInte
|
||||
private mapper: MapperInterface<Authenticator, TypeORMAuthenticator>,
|
||||
) {}
|
||||
|
||||
async findById(id: UniqueEntityId): Promise<Authenticator | null> {
|
||||
const persistence = await this.ormRepository
|
||||
.createQueryBuilder('authenticator')
|
||||
.where('authenticator.uuid = :id', {
|
||||
id: id.toString(),
|
||||
})
|
||||
.getOne()
|
||||
|
||||
if (persistence === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return this.mapper.toDomain(persistence)
|
||||
}
|
||||
|
||||
async remove(authenticator: Authenticator): Promise<void> {
|
||||
await this.ormRepository.remove(this.mapper.toProjection(authenticator))
|
||||
}
|
||||
|
||||
async findByUserUuidAndCredentialId(userUuid: Uuid, credentialId: Buffer): Promise<Authenticator | null> {
|
||||
const persistence = await this.ormRepository
|
||||
.createQueryBuilder('authenticator')
|
||||
|
||||
Reference in New Issue
Block a user