mirror of
https://gitlab.com/mobicoop/v3/service/auth.git
synced 2026-03-24 11:05:49 +00:00
new authorization
This commit is contained in:
@@ -1,52 +0,0 @@
|
||||
import { RabbitSubscribe } from '@golevelup/nestjs-rabbitmq';
|
||||
import { Controller } from '@nestjs/common';
|
||||
import { CommandBus } from '@nestjs/cqrs';
|
||||
import { UpdateUsernameCommand } from '../../commands/update-username.command';
|
||||
import { Type } from '../../domain/dtos/type.enum';
|
||||
import { UpdateUsernameRequest } from '../../domain/dtos/update-username.request';
|
||||
import { DeleteAuthenticationRequest } from '../../domain/dtos/delete-authentication.request';
|
||||
import { DeleteAuthenticationCommand } from '../../commands/delete-authentication.command';
|
||||
|
||||
@Controller()
|
||||
export class AuthenticationMessagerController {
|
||||
constructor(private readonly commandBus: CommandBus) {}
|
||||
|
||||
@RabbitSubscribe({
|
||||
name: 'userUpdate',
|
||||
})
|
||||
public async userUpdatedHandler(message: string) {
|
||||
const updatedUser = JSON.parse(message);
|
||||
if (!updatedUser.hasOwnProperty('uuid')) throw new Error();
|
||||
if (updatedUser.hasOwnProperty('email') && updatedUser.email) {
|
||||
const updateUsernameRequest = new UpdateUsernameRequest();
|
||||
updateUsernameRequest.uuid = updatedUser.uuid;
|
||||
updateUsernameRequest.username = updatedUser.email;
|
||||
updateUsernameRequest.type = Type.EMAIL;
|
||||
await this.commandBus.execute(
|
||||
new UpdateUsernameCommand(updateUsernameRequest),
|
||||
);
|
||||
}
|
||||
if (updatedUser.hasOwnProperty('phone') && updatedUser.phone) {
|
||||
const updateUsernameRequest = new UpdateUsernameRequest();
|
||||
updateUsernameRequest.uuid = updatedUser.uuid;
|
||||
updateUsernameRequest.username = updatedUser.phone;
|
||||
updateUsernameRequest.type = Type.PHONE;
|
||||
await this.commandBus.execute(
|
||||
new UpdateUsernameCommand(updateUsernameRequest),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@RabbitSubscribe({
|
||||
name: 'userDelete',
|
||||
})
|
||||
public async userDeletedHandler(message: string) {
|
||||
const deletedUser = JSON.parse(message);
|
||||
if (!deletedUser.hasOwnProperty('uuid')) throw new Error();
|
||||
const deleteAuthenticationRequest = new DeleteAuthenticationRequest();
|
||||
deleteAuthenticationRequest.uuid = deletedUser.uuid;
|
||||
await this.commandBus.execute(
|
||||
new DeleteAuthenticationCommand(deleteAuthenticationRequest),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
import { Mapper } from '@automapper/core';
|
||||
import { InjectMapper } from '@automapper/nestjs';
|
||||
import { Controller, UsePipes } from '@nestjs/common';
|
||||
import { CommandBus, QueryBus } from '@nestjs/cqrs';
|
||||
import { GrpcMethod, RpcException } from '@nestjs/microservices';
|
||||
import { DatabaseException } from 'src/modules/database/exceptions/database.exception';
|
||||
import { AddUsernameCommand } from '../../commands/add-username.command';
|
||||
import { CreateAuthenticationCommand } from '../../commands/create-authentication.command';
|
||||
import { DeleteAuthenticationCommand } from '../../commands/delete-authentication.command';
|
||||
import { DeleteUsernameCommand } from '../../commands/delete-username.command';
|
||||
import { UpdatePasswordCommand } from '../../commands/update-password.command';
|
||||
import { UpdateUsernameCommand } from '../../commands/update-username.command';
|
||||
import { AddUsernameRequest } from '../../domain/dtos/add-username.request';
|
||||
import { CreateAuthenticationRequest } from '../../domain/dtos/create-authentication.request';
|
||||
import { DeleteAuthenticationRequest } from '../../domain/dtos/delete-authentication.request';
|
||||
import { DeleteUsernameRequest } from '../../domain/dtos/delete-username.request';
|
||||
import { UpdatePasswordRequest } from '../../domain/dtos/update-password.request';
|
||||
import { UpdateUsernameRequest } from '../../domain/dtos/update-username.request';
|
||||
import { ValidateAuthenticationRequest } from '../../domain/dtos/validate-authentication.request';
|
||||
import { Authentication } from '../../domain/entities/authentication';
|
||||
import { Username } from '../../domain/entities/username';
|
||||
import { ValidateAuthenticationQuery } from '../../queries/validate-authentication.query';
|
||||
import { AuthenticationPresenter } from './authentication.presenter';
|
||||
import { RpcValidationPipe } from '../../../../utils/pipes/rpc.validation-pipe';
|
||||
import { UsernamePresenter } from './username.presenter';
|
||||
|
||||
@UsePipes(
|
||||
new RpcValidationPipe({
|
||||
whitelist: true,
|
||||
forbidUnknownValues: false,
|
||||
}),
|
||||
)
|
||||
@Controller()
|
||||
export class AuthenticationController {
|
||||
constructor(
|
||||
private readonly commandBus: CommandBus,
|
||||
private readonly queryBus: QueryBus,
|
||||
@InjectMapper() private readonly mapper: Mapper,
|
||||
) {}
|
||||
|
||||
@GrpcMethod('AuthenticationService', 'Validate')
|
||||
async validate(
|
||||
data: ValidateAuthenticationRequest,
|
||||
): Promise<AuthenticationPresenter> {
|
||||
try {
|
||||
const authentication: Authentication = await this.queryBus.execute(
|
||||
new ValidateAuthenticationQuery(data.username, data.password),
|
||||
);
|
||||
return this.mapper.map(
|
||||
authentication,
|
||||
Authentication,
|
||||
AuthenticationPresenter,
|
||||
);
|
||||
} catch (e) {
|
||||
throw new RpcException({
|
||||
code: 7,
|
||||
message: 'Permission denied',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@GrpcMethod('AuthenticationService', 'Create')
|
||||
async createUser(
|
||||
data: CreateAuthenticationRequest,
|
||||
): Promise<AuthenticationPresenter> {
|
||||
try {
|
||||
const authentication: Authentication = await this.commandBus.execute(
|
||||
new CreateAuthenticationCommand(data),
|
||||
);
|
||||
return this.mapper.map(
|
||||
authentication,
|
||||
Authentication,
|
||||
AuthenticationPresenter,
|
||||
);
|
||||
} catch (e) {
|
||||
if (e instanceof DatabaseException) {
|
||||
if (e.message.includes('Already exists')) {
|
||||
throw new RpcException({
|
||||
code: 6,
|
||||
message: 'Auth already exists',
|
||||
});
|
||||
}
|
||||
}
|
||||
throw new RpcException({
|
||||
code: 7,
|
||||
message: 'Permission denied',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@GrpcMethod('AuthenticationService', 'AddUsername')
|
||||
async addUsername(data: AddUsernameRequest): Promise<UsernamePresenter> {
|
||||
try {
|
||||
const username: Username = await this.commandBus.execute(
|
||||
new AddUsernameCommand(data),
|
||||
);
|
||||
|
||||
return this.mapper.map(username, Username, UsernamePresenter);
|
||||
} catch (e) {
|
||||
if (e instanceof DatabaseException) {
|
||||
if (e.message.includes('Already exists')) {
|
||||
throw new RpcException({
|
||||
code: 6,
|
||||
message: 'Username already exists',
|
||||
});
|
||||
}
|
||||
}
|
||||
throw new RpcException({
|
||||
code: 7,
|
||||
message: 'Permission denied',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@GrpcMethod('AuthenticationService', 'UpdateUsername')
|
||||
async updateUsername(
|
||||
data: UpdateUsernameRequest,
|
||||
): Promise<UsernamePresenter> {
|
||||
try {
|
||||
const username: Username = await this.commandBus.execute(
|
||||
new UpdateUsernameCommand(data),
|
||||
);
|
||||
|
||||
return this.mapper.map(username, Username, UsernamePresenter);
|
||||
} catch (e) {
|
||||
if (e instanceof DatabaseException) {
|
||||
if (e.message.includes('Already exists')) {
|
||||
throw new RpcException({
|
||||
code: 6,
|
||||
message: 'Username already exists',
|
||||
});
|
||||
}
|
||||
}
|
||||
throw new RpcException({
|
||||
code: 7,
|
||||
message: 'Permission denied',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@GrpcMethod('AuthenticationService', 'UpdatePassword')
|
||||
async updatePassword(
|
||||
data: UpdatePasswordRequest,
|
||||
): Promise<AuthenticationPresenter> {
|
||||
try {
|
||||
const authentication: Authentication = await this.commandBus.execute(
|
||||
new UpdatePasswordCommand(data),
|
||||
);
|
||||
|
||||
return this.mapper.map(
|
||||
authentication,
|
||||
Authentication,
|
||||
AuthenticationPresenter,
|
||||
);
|
||||
} catch (e) {
|
||||
throw new RpcException({
|
||||
code: 7,
|
||||
message: 'Permission denied',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@GrpcMethod('AuthenticationService', 'DeleteUsername')
|
||||
async deleteUsername(data: DeleteUsernameRequest) {
|
||||
try {
|
||||
return await this.commandBus.execute(new DeleteUsernameCommand(data));
|
||||
} catch (e) {
|
||||
throw new RpcException({
|
||||
code: 7,
|
||||
message: 'Permission denied',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@GrpcMethod('AuthenticationService', 'Delete')
|
||||
async deleteAuthentication(data: DeleteAuthenticationRequest) {
|
||||
try {
|
||||
return await this.commandBus.execute(
|
||||
new DeleteAuthenticationCommand(data),
|
||||
);
|
||||
} catch (e) {
|
||||
throw new RpcException({
|
||||
code: 7,
|
||||
message: 'Permission denied',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { AutoMap } from '@automapper/classes';
|
||||
|
||||
export class AuthenticationPresenter {
|
||||
@AutoMap()
|
||||
uuid: string;
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package authentication;
|
||||
|
||||
service AuthenticationService {
|
||||
rpc Validate(AuthenticationByUsernamePassword) returns (Uuid);
|
||||
rpc Create(Authentication) returns (Uuid);
|
||||
rpc AddUsername(Username) returns (Uuid);
|
||||
rpc UpdatePassword(Password) returns (Uuid);
|
||||
rpc UpdateUsername(Username) returns (Uuid);
|
||||
rpc DeleteUsername(Username) returns (Uuid);
|
||||
rpc Delete(Uuid) returns (Empty);
|
||||
}
|
||||
|
||||
message AuthenticationByUsernamePassword {
|
||||
string username = 1;
|
||||
string password = 2;
|
||||
}
|
||||
|
||||
message Authentication {
|
||||
string uuid = 1;
|
||||
string username = 2;
|
||||
string password = 3;
|
||||
string type = 4;
|
||||
}
|
||||
|
||||
message Password {
|
||||
string uuid = 1;
|
||||
string password = 2;
|
||||
}
|
||||
|
||||
message Username {
|
||||
string uuid = 1;
|
||||
string username = 2;
|
||||
string type = 3;
|
||||
}
|
||||
|
||||
message Uuid {
|
||||
string uuid = 1;
|
||||
}
|
||||
|
||||
message Empty {}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { AutoMap } from '@automapper/classes';
|
||||
|
||||
export class UsernamePresenter {
|
||||
@AutoMap()
|
||||
uuid: string;
|
||||
|
||||
@AutoMap()
|
||||
username: string;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthRepository } from '../../../database/domain/auth-repository';
|
||||
import { Authentication } from '../../domain/entities/authentication';
|
||||
|
||||
@Injectable()
|
||||
export class AuthenticationRepository extends AuthRepository<Authentication> {
|
||||
protected model = 'auth';
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { AmqpConnection } from '@golevelup/nestjs-rabbitmq';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { IMessageBroker } from '../../domain/interfaces/message-broker';
|
||||
|
||||
@Injectable()
|
||||
export class Messager extends IMessageBroker {
|
||||
constructor(
|
||||
private readonly amqpConnection: AmqpConnection,
|
||||
configService: ConfigService,
|
||||
) {
|
||||
super(configService.get<string>('RMQ_EXCHANGE'));
|
||||
}
|
||||
|
||||
publish = (routingKey: string, message: string): void => {
|
||||
this.amqpConnection.publish(this.exchange, routingKey, message);
|
||||
};
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthRepository } from '../../../database/domain/auth-repository';
|
||||
import { Username } from '../../domain/entities/username';
|
||||
|
||||
@Injectable()
|
||||
export class UsernameRepository extends AuthRepository<Username> {
|
||||
protected model = 'username';
|
||||
}
|
||||
2
src/modules/authentication/authentication.di-tokens.ts
Normal file
2
src/modules/authentication/authentication.di-tokens.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const AUTHENTICATION_REPOSITORY = Symbol('AUTHENTICATION_REPOSITORY');
|
||||
export const USERNAME_REPOSITORY = Symbol('USERNAME_REPOSITORY');
|
||||
68
src/modules/authentication/authentication.mapper.ts
Normal file
68
src/modules/authentication/authentication.mapper.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { Mapper } from '@mobicoop/ddd-library';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthenticationEntity } from './core/domain/authentication.entity';
|
||||
import { AuthenticationResponseDto } from './interface/dtos/authentication.response.dto';
|
||||
import {
|
||||
AuthenticationReadModel,
|
||||
AuthenticationWriteModel,
|
||||
UsernameModel,
|
||||
} from './infrastructure/authentication.repository';
|
||||
import { Type, UsernameProps } from './core/domain/username.types';
|
||||
|
||||
/**
|
||||
* Mapper constructs objects that are used in different layers:
|
||||
* Record is an object that is stored in a database,
|
||||
* Entity is an object that is used in application domain layer,
|
||||
* and a ResponseDTO is an object returned to a user (usually as json).
|
||||
*/
|
||||
|
||||
@Injectable()
|
||||
export class AuthenticationMapper
|
||||
implements
|
||||
Mapper<
|
||||
AuthenticationEntity,
|
||||
AuthenticationReadModel,
|
||||
AuthenticationWriteModel,
|
||||
AuthenticationResponseDto
|
||||
>
|
||||
{
|
||||
toPersistence = (entity: AuthenticationEntity): AuthenticationWriteModel => {
|
||||
const copy = entity.getProps();
|
||||
const record: AuthenticationWriteModel = {
|
||||
uuid: copy.id,
|
||||
password: copy.password,
|
||||
usernames: {
|
||||
create: copy.usernames.map((username: UsernameProps) => ({
|
||||
username: username.name,
|
||||
type: username.type,
|
||||
})),
|
||||
},
|
||||
createdAt: copy.createdAt,
|
||||
updatedAt: copy.updatedAt,
|
||||
};
|
||||
return record;
|
||||
};
|
||||
|
||||
toDomain = (record: AuthenticationReadModel): AuthenticationEntity => {
|
||||
const entity = new AuthenticationEntity({
|
||||
id: record.uuid,
|
||||
createdAt: new Date(record.createdAt),
|
||||
updatedAt: new Date(record.updatedAt),
|
||||
props: {
|
||||
userId: record.uuid,
|
||||
password: record.password,
|
||||
usernames: record.usernames.map((username: UsernameModel) => ({
|
||||
userId: record.uuid,
|
||||
name: username.username,
|
||||
type: Type[username.type],
|
||||
})),
|
||||
},
|
||||
});
|
||||
return entity;
|
||||
};
|
||||
|
||||
toResponse = (entity: AuthenticationEntity): AuthenticationResponseDto => {
|
||||
const response = new AuthenticationResponseDto(entity);
|
||||
return response;
|
||||
};
|
||||
}
|
||||
@@ -1,66 +1,72 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { Module, Provider } from '@nestjs/common';
|
||||
import { CreateAuthenticationGrpcController } from './interface/grpc-controllers/create-authentication.grpc.controller';
|
||||
import { CreateAuthenticationService } from './core/application/commands/create-authentication/create-authentication.service';
|
||||
import { AuthenticationMapper } from './authentication.mapper';
|
||||
import {
|
||||
AUTHENTICATION_REPOSITORY,
|
||||
USERNAME_REPOSITORY,
|
||||
} from './authentication.di-tokens';
|
||||
import { AuthenticationRepository } from './infrastructure/authentication.repository';
|
||||
import { PrismaService } from './infrastructure/prisma.service';
|
||||
import { CqrsModule } from '@nestjs/cqrs';
|
||||
import { DatabaseModule } from '../database/database.module';
|
||||
import { AuthenticationController } from './adapters/primaries/authentication.controller';
|
||||
import { CreateAuthenticationUseCase } from './domain/usecases/create-authentication.usecase';
|
||||
import { ValidateAuthenticationUseCase } from './domain/usecases/validate-authentication.usecase';
|
||||
import { AuthenticationProfile } from './mappers/authentication.profile';
|
||||
import { AuthenticationRepository } from './adapters/secondaries/authentication.repository';
|
||||
import { UpdateUsernameUseCase } from './domain/usecases/update-username.usecase';
|
||||
import { UsernameProfile } from './mappers/username.profile';
|
||||
import { AddUsernameUseCase } from './domain/usecases/add-username.usecase';
|
||||
import { UpdatePasswordUseCase } from './domain/usecases/update-password.usecase';
|
||||
import { DeleteUsernameUseCase } from './domain/usecases/delete-username.usecase';
|
||||
import { DeleteAuthenticationUseCase } from './domain/usecases/delete-authentication.usecase';
|
||||
import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { AuthenticationMessagerController } from './adapters/primaries/authentication-messager.controller';
|
||||
import { Messager } from './adapters/secondaries/messager';
|
||||
import { DeleteAuthenticationGrpcController } from './interface/grpc-controllers/delete-authentication.grpc.controller';
|
||||
import { DeleteAuthenticationService } from './core/application/commands/delete-authentication/delete-authentication.service';
|
||||
import { MESSAGE_PUBLISHER } from '@src/app.di-tokens';
|
||||
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
|
||||
import { UsernameRepository } from './infrastructure/username.repository';
|
||||
import { UsernameMapper } from './username.mapper';
|
||||
import { AddUsernameGrpcController } from './interface/grpc-controllers/add-username.grpc.controller';
|
||||
import { AddUsernameService } from './core/application/commands/add-usernames/add-username.service';
|
||||
|
||||
const grpcControllers = [
|
||||
CreateAuthenticationGrpcController,
|
||||
DeleteAuthenticationGrpcController,
|
||||
AddUsernameGrpcController,
|
||||
];
|
||||
|
||||
const commandHandlers: Provider[] = [
|
||||
CreateAuthenticationService,
|
||||
DeleteAuthenticationService,
|
||||
AddUsernameService,
|
||||
];
|
||||
|
||||
const mappers: Provider[] = [AuthenticationMapper, UsernameMapper];
|
||||
|
||||
const repositories: Provider[] = [
|
||||
{
|
||||
provide: AUTHENTICATION_REPOSITORY,
|
||||
useClass: AuthenticationRepository,
|
||||
},
|
||||
{
|
||||
provide: USERNAME_REPOSITORY,
|
||||
useClass: UsernameRepository,
|
||||
},
|
||||
];
|
||||
|
||||
const messageBrokers: Provider[] = [
|
||||
{
|
||||
provide: MESSAGE_PUBLISHER,
|
||||
useClass: MessageBrokerPublisher,
|
||||
},
|
||||
];
|
||||
|
||||
const orms: Provider[] = [PrismaService];
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
DatabaseModule,
|
||||
CqrsModule,
|
||||
RabbitMQModule.forRootAsync(RabbitMQModule, {
|
||||
imports: [ConfigModule],
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
exchanges: [
|
||||
{
|
||||
name: configService.get<string>('RMQ_EXCHANGE'),
|
||||
type: 'topic',
|
||||
},
|
||||
],
|
||||
handlers: {
|
||||
userUpdate: {
|
||||
exchange: configService.get<string>('RMQ_EXCHANGE'),
|
||||
routingKey: 'user.update',
|
||||
},
|
||||
userDelete: {
|
||||
exchange: configService.get<string>('RMQ_EXCHANGE'),
|
||||
routingKey: 'user.delete',
|
||||
},
|
||||
},
|
||||
uri: configService.get<string>('RMQ_URI'),
|
||||
connectionInitOptions: { wait: false },
|
||||
enableControllerDiscovery: true,
|
||||
}),
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
],
|
||||
controllers: [AuthenticationController, AuthenticationMessagerController],
|
||||
imports: [CqrsModule],
|
||||
controllers: [...grpcControllers],
|
||||
providers: [
|
||||
AuthenticationProfile,
|
||||
UsernameProfile,
|
||||
AuthenticationRepository,
|
||||
Messager,
|
||||
ValidateAuthenticationUseCase,
|
||||
CreateAuthenticationUseCase,
|
||||
AddUsernameUseCase,
|
||||
UpdateUsernameUseCase,
|
||||
UpdatePasswordUseCase,
|
||||
DeleteUsernameUseCase,
|
||||
DeleteAuthenticationUseCase,
|
||||
...commandHandlers,
|
||||
...mappers,
|
||||
...repositories,
|
||||
...messageBrokers,
|
||||
...orms,
|
||||
],
|
||||
exports: [
|
||||
PrismaService,
|
||||
AuthenticationMapper,
|
||||
AUTHENTICATION_REPOSITORY,
|
||||
USERNAME_REPOSITORY,
|
||||
],
|
||||
exports: [],
|
||||
})
|
||||
export class AuthenticationModule {}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import { AddUsernameRequest } from '../domain/dtos/add-username.request';
|
||||
|
||||
export class AddUsernameCommand {
|
||||
readonly addUsernameRequest: AddUsernameRequest;
|
||||
|
||||
constructor(request: AddUsernameRequest) {
|
||||
this.addUsernameRequest = request;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { CreateAuthenticationRequest } from '../domain/dtos/create-authentication.request';
|
||||
|
||||
export class CreateAuthenticationCommand {
|
||||
readonly createAuthenticationRequest: CreateAuthenticationRequest;
|
||||
|
||||
constructor(request: CreateAuthenticationRequest) {
|
||||
this.createAuthenticationRequest = request;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { DeleteAuthenticationRequest } from '../domain/dtos/delete-authentication.request';
|
||||
|
||||
export class DeleteAuthenticationCommand {
|
||||
readonly deleteAuthenticationRequest: DeleteAuthenticationRequest;
|
||||
|
||||
constructor(request: DeleteAuthenticationRequest) {
|
||||
this.deleteAuthenticationRequest = request;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { DeleteUsernameRequest } from '../domain/dtos/delete-username.request';
|
||||
|
||||
export class DeleteUsernameCommand {
|
||||
readonly deleteUsernameRequest: DeleteUsernameRequest;
|
||||
|
||||
constructor(request: DeleteUsernameRequest) {
|
||||
this.deleteUsernameRequest = request;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { UpdatePasswordRequest } from '../domain/dtos/update-password.request';
|
||||
|
||||
export class UpdatePasswordCommand {
|
||||
readonly updatePasswordRequest: UpdatePasswordRequest;
|
||||
|
||||
constructor(request: UpdatePasswordRequest) {
|
||||
this.updatePasswordRequest = request;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { UpdateUsernameRequest } from '../domain/dtos/update-username.request';
|
||||
|
||||
export class UpdateUsernameCommand {
|
||||
readonly updateUsernameRequest: UpdateUsernameRequest;
|
||||
|
||||
constructor(request: UpdateUsernameRequest) {
|
||||
this.updateUsernameRequest = request;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Command, CommandProps } from '@mobicoop/ddd-library';
|
||||
import { Username } from '../../types/username';
|
||||
|
||||
export class AddUsernameCommand extends Command {
|
||||
readonly userId: string;
|
||||
readonly username: Username;
|
||||
|
||||
constructor(props: CommandProps<AddUsernameCommand>) {
|
||||
super(props);
|
||||
this.userId = props.userId;
|
||||
this.username = props.username;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||
import { Inject } from '@nestjs/common';
|
||||
import {
|
||||
AggregateID,
|
||||
ConflictException,
|
||||
UniqueConstraintException,
|
||||
} from '@mobicoop/ddd-library';
|
||||
import {
|
||||
AUTHENTICATION_REPOSITORY,
|
||||
USERNAME_REPOSITORY,
|
||||
} from '@modules/authentication/authentication.di-tokens';
|
||||
import { AuthenticationRepositoryPort } from '../../ports/authentication.repository.port';
|
||||
import { UsernameAlreadyExistsException } from '@modules/authentication/core/domain/authentication.errors';
|
||||
import { AddUsernameCommand } from './add-username.command';
|
||||
import { UsernameRepositoryPort } from '../../ports/username.repository.port';
|
||||
import { UsernameEntity } from '@modules/authentication/core/domain/username.entity';
|
||||
|
||||
@CommandHandler(AddUsernameCommand)
|
||||
export class AddUsernameService implements ICommandHandler {
|
||||
constructor(
|
||||
@Inject(AUTHENTICATION_REPOSITORY)
|
||||
private readonly authenticationRepository: AuthenticationRepositoryPort,
|
||||
@Inject(USERNAME_REPOSITORY)
|
||||
private readonly usernameRepository: UsernameRepositoryPort,
|
||||
) {}
|
||||
|
||||
async execute(command: AddUsernameCommand): Promise<AggregateID> {
|
||||
await this.authenticationRepository.findOneById(command.userId, {
|
||||
usernames: true,
|
||||
});
|
||||
try {
|
||||
const newUsername = await UsernameEntity.create({
|
||||
name: command.username.name,
|
||||
userId: command.userId,
|
||||
type: command.username.type,
|
||||
});
|
||||
await this.usernameRepository.insert(newUsername);
|
||||
return newUsername.id;
|
||||
} catch (error: any) {
|
||||
if (error instanceof ConflictException) {
|
||||
throw new UsernameAlreadyExistsException(error);
|
||||
}
|
||||
if (
|
||||
error instanceof UniqueConstraintException &&
|
||||
error.message.includes('username')
|
||||
) {
|
||||
throw new UsernameAlreadyExistsException(error);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Command, CommandProps } from '@mobicoop/ddd-library';
|
||||
import { Username } from '../../types/username';
|
||||
|
||||
export class CreateAuthenticationCommand extends Command {
|
||||
readonly userId: string;
|
||||
readonly password: string;
|
||||
readonly usernames: Username[];
|
||||
|
||||
constructor(props: CommandProps<CreateAuthenticationCommand>) {
|
||||
super(props);
|
||||
this.userId = props.userId;
|
||||
this.password = props.password;
|
||||
this.usernames = props.usernames;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||
import { Inject } from '@nestjs/common';
|
||||
import {
|
||||
AggregateID,
|
||||
ConflictException,
|
||||
UniqueConstraintException,
|
||||
} from '@mobicoop/ddd-library';
|
||||
import { CreateAuthenticationCommand } from './create-authentication.command';
|
||||
import { AUTHENTICATION_REPOSITORY } from '@modules/authentication/authentication.di-tokens';
|
||||
import { AuthenticationRepositoryPort } from '../../ports/authentication.repository.port';
|
||||
import { AuthenticationEntity } from '@modules/authentication/core/domain/authentication.entity';
|
||||
import {
|
||||
AuthenticationAlreadyExistsException,
|
||||
UsernameAlreadyExistsException,
|
||||
} from '@modules/authentication/core/domain/authentication.errors';
|
||||
|
||||
@CommandHandler(CreateAuthenticationCommand)
|
||||
export class CreateAuthenticationService implements ICommandHandler {
|
||||
constructor(
|
||||
@Inject(AUTHENTICATION_REPOSITORY)
|
||||
private readonly authenticationRepository: AuthenticationRepositoryPort,
|
||||
) {}
|
||||
|
||||
async execute(command: CreateAuthenticationCommand): Promise<AggregateID> {
|
||||
const authentication: AuthenticationEntity =
|
||||
await AuthenticationEntity.create({
|
||||
userId: command.userId,
|
||||
password: command.password,
|
||||
usernames: command.usernames,
|
||||
});
|
||||
try {
|
||||
await this.authenticationRepository.insert(authentication);
|
||||
return authentication.getProps().userId;
|
||||
} catch (error: any) {
|
||||
if (error instanceof ConflictException) {
|
||||
throw new AuthenticationAlreadyExistsException(error);
|
||||
}
|
||||
if (
|
||||
error instanceof UniqueConstraintException &&
|
||||
error.message.includes('uuid')
|
||||
) {
|
||||
throw new AuthenticationAlreadyExistsException(error);
|
||||
}
|
||||
if (
|
||||
error instanceof UniqueConstraintException &&
|
||||
error.message.includes('username')
|
||||
) {
|
||||
throw new UsernameAlreadyExistsException(error);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Command, CommandProps } from '@mobicoop/ddd-library';
|
||||
|
||||
export class DeleteAuthenticationCommand extends Command {
|
||||
readonly userId: string;
|
||||
|
||||
constructor(props: CommandProps<DeleteAuthenticationCommand>) {
|
||||
super(props);
|
||||
this.userId = props.userId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||
import { DeleteAuthenticationCommand } from './delete-authentication.command';
|
||||
import { AUTHENTICATION_REPOSITORY } from '@modules/authentication/authentication.di-tokens';
|
||||
import { AuthenticationRepositoryPort } from '../../ports/authentication.repository.port';
|
||||
import { AuthenticationEntity } from '@modules/authentication/core/domain/authentication.entity';
|
||||
|
||||
@CommandHandler(DeleteAuthenticationCommand)
|
||||
export class DeleteAuthenticationService implements ICommandHandler {
|
||||
constructor(
|
||||
@Inject(AUTHENTICATION_REPOSITORY)
|
||||
private readonly authenticationRepository: AuthenticationRepositoryPort,
|
||||
) {}
|
||||
|
||||
async execute(command: DeleteAuthenticationCommand): Promise<boolean> {
|
||||
const authentication: AuthenticationEntity =
|
||||
await this.authenticationRepository.findOneById(command.userId, {
|
||||
usernames: true,
|
||||
});
|
||||
authentication.delete();
|
||||
const isDeleted: boolean = await this.authenticationRepository.delete(
|
||||
authentication,
|
||||
);
|
||||
return isDeleted;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { RepositoryPort } from '@mobicoop/ddd-library';
|
||||
import { AuthenticationEntity } from '@modules/authentication/core/domain/authentication.entity';
|
||||
|
||||
export type AuthenticationRepositoryPort = RepositoryPort<AuthenticationEntity>;
|
||||
@@ -0,0 +1,4 @@
|
||||
import { RepositoryPort } from '@mobicoop/ddd-library';
|
||||
import { UsernameEntity } from '../../domain/username.entity';
|
||||
|
||||
export type UsernameRepositoryPort = RepositoryPort<UsernameEntity>;
|
||||
@@ -0,0 +1,6 @@
|
||||
import { Type } from '@modules/authentication/core/domain/username.types';
|
||||
|
||||
export type Username = {
|
||||
name: string;
|
||||
type: Type;
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import {
|
||||
AuthenticationProps,
|
||||
CreateAuthenticationProps,
|
||||
} from './authentication.types';
|
||||
import { AuthenticationCreatedDomainEvent } from './events/authentication-created.domain-event';
|
||||
import { AuthenticationDeletedDomainEvent } from './events/authentication-deleted.domain-event';
|
||||
|
||||
export class AuthenticationEntity extends AggregateRoot<AuthenticationProps> {
|
||||
protected readonly _id: AggregateID;
|
||||
|
||||
static create = async (
|
||||
create: CreateAuthenticationProps,
|
||||
): Promise<AuthenticationEntity> => {
|
||||
const props: AuthenticationProps = { ...create };
|
||||
const hash = await bcrypt.hash(props.password, 10);
|
||||
const authentication = new AuthenticationEntity({
|
||||
id: props.userId,
|
||||
props: {
|
||||
userId: props.userId,
|
||||
password: hash,
|
||||
usernames: props.usernames,
|
||||
},
|
||||
});
|
||||
authentication.addEvent(
|
||||
new AuthenticationCreatedDomainEvent({ aggregateId: props.userId }),
|
||||
);
|
||||
return authentication;
|
||||
};
|
||||
|
||||
delete(): void {
|
||||
this.addEvent(
|
||||
new AuthenticationDeletedDomainEvent({
|
||||
aggregateId: this.id,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
validate(): void {
|
||||
// entity business rules validation to protect it's invariant before saving entity to a database
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { ExceptionBase } from '@mobicoop/ddd-library';
|
||||
|
||||
export class AuthenticationAlreadyExistsException extends ExceptionBase {
|
||||
static readonly message = 'Authentication already exists';
|
||||
|
||||
public readonly code = 'AUTHENTICATION.ALREADY_EXISTS';
|
||||
|
||||
constructor(cause?: Error, metadata?: unknown) {
|
||||
super(AuthenticationAlreadyExistsException.message, cause, metadata);
|
||||
}
|
||||
}
|
||||
|
||||
export class UsernameAlreadyExistsException extends ExceptionBase {
|
||||
static readonly message = 'Username already exists';
|
||||
|
||||
public readonly code = 'USERNAME.ALREADY_EXISTS';
|
||||
|
||||
constructor(cause?: Error, metadata?: unknown) {
|
||||
super(UsernameAlreadyExistsException.message, cause, metadata);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { UsernameProps } from './username.types';
|
||||
|
||||
// All properties that an Authentication has
|
||||
export interface AuthenticationProps {
|
||||
userId: string;
|
||||
password: string;
|
||||
usernames: UsernameProps[];
|
||||
}
|
||||
|
||||
// Properties that are needed for an Authentication creation
|
||||
export interface CreateAuthenticationProps {
|
||||
userId: string;
|
||||
password: string;
|
||||
usernames: UsernameProps[];
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library';
|
||||
|
||||
export class AuthenticationCreatedDomainEvent extends DomainEvent {
|
||||
constructor(props: DomainEventProps<AuthenticationCreatedDomainEvent>) {
|
||||
super(props);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library';
|
||||
|
||||
export class AuthenticationDeletedDomainEvent extends DomainEvent {
|
||||
constructor(props: DomainEventProps<AuthenticationDeletedDomainEvent>) {
|
||||
super(props);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library';
|
||||
|
||||
export class UsernameAddedDomainEvent extends DomainEvent {
|
||||
constructor(props: DomainEventProps<UsernameAddedDomainEvent>) {
|
||||
super(props);
|
||||
}
|
||||
}
|
||||
27
src/modules/authentication/core/domain/username.entity.ts
Normal file
27
src/modules/authentication/core/domain/username.entity.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { AggregateID, AggregateRoot } from '@mobicoop/ddd-library';
|
||||
import { UsernameProps } from './username.types';
|
||||
import { UsernameAddedDomainEvent } from './events/username-added.domain-event';
|
||||
|
||||
export class UsernameEntity extends AggregateRoot<UsernameProps> {
|
||||
protected readonly _id: AggregateID;
|
||||
|
||||
static create = async (create: UsernameProps): Promise<UsernameEntity> => {
|
||||
const props: UsernameProps = { ...create };
|
||||
const username = new UsernameEntity({
|
||||
id: props.name,
|
||||
props: {
|
||||
name: props.name,
|
||||
userId: props.userId,
|
||||
type: props.type,
|
||||
},
|
||||
});
|
||||
username.addEvent(
|
||||
new UsernameAddedDomainEvent({ aggregateId: props.name }),
|
||||
);
|
||||
return username;
|
||||
};
|
||||
|
||||
validate(): void {
|
||||
// entity business rules validation to protect it's invariant before saving entity to a database
|
||||
}
|
||||
}
|
||||
11
src/modules/authentication/core/domain/username.types.ts
Normal file
11
src/modules/authentication/core/domain/username.types.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
// All properties that a Username has
|
||||
export interface UsernameProps {
|
||||
name: string;
|
||||
userId?: string;
|
||||
type: Type;
|
||||
}
|
||||
|
||||
export enum Type {
|
||||
EMAIL = 'EMAIL',
|
||||
PHONE = 'PHONE',
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { AutoMap } from '@automapper/classes';
|
||||
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
|
||||
import { Type } from './type.enum';
|
||||
|
||||
export class AddUsernameRequest {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@AutoMap()
|
||||
uuid: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@AutoMap()
|
||||
username: string;
|
||||
|
||||
@IsEnum(Type)
|
||||
@IsNotEmpty()
|
||||
@AutoMap()
|
||||
type: Type;
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { AutoMap } from '@automapper/classes';
|
||||
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
|
||||
import { Type } from './type.enum';
|
||||
|
||||
export class CreateAuthenticationRequest {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@AutoMap()
|
||||
uuid: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@AutoMap()
|
||||
username: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@AutoMap()
|
||||
password: string;
|
||||
|
||||
@IsEnum(Type)
|
||||
@IsNotEmpty()
|
||||
@AutoMap()
|
||||
type: Type;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { AutoMap } from '@automapper/classes';
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
export class DeleteAuthenticationRequest {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@AutoMap()
|
||||
uuid: string;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { AutoMap } from '@automapper/classes';
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
export class DeleteUsernameRequest {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@AutoMap()
|
||||
username: string;
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export enum Type {
|
||||
EMAIL = 'EMAIL',
|
||||
PHONE = 'PHONE',
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { AutoMap } from '@automapper/classes';
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
export class UpdatePasswordRequest {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@AutoMap()
|
||||
uuid: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@AutoMap()
|
||||
password: string;
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { AutoMap } from '@automapper/classes';
|
||||
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
|
||||
import { Type } from './type.enum';
|
||||
|
||||
export class UpdateUsernameRequest {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@AutoMap()
|
||||
uuid: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@AutoMap()
|
||||
username: string;
|
||||
|
||||
@IsEnum(Type)
|
||||
@IsNotEmpty()
|
||||
@AutoMap()
|
||||
type: Type;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
export class ValidateAuthenticationRequest {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
username: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
password: string;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { AutoMap } from '@automapper/classes';
|
||||
|
||||
export class Authentication {
|
||||
@AutoMap()
|
||||
uuid: string;
|
||||
|
||||
password: string;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { AutoMap } from '@automapper/classes';
|
||||
import { Type } from '../dtos/type.enum';
|
||||
|
||||
export class Username {
|
||||
@AutoMap()
|
||||
uuid: string;
|
||||
|
||||
@AutoMap()
|
||||
username: string;
|
||||
|
||||
@AutoMap()
|
||||
type: Type;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export abstract class IMessageBroker {
|
||||
exchange: string;
|
||||
|
||||
constructor(exchange: string) {
|
||||
this.exchange = exchange;
|
||||
}
|
||||
|
||||
abstract publish(routingKey: string, message: string): void;
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { CommandHandler } from '@nestjs/cqrs';
|
||||
import { Messager } from '../../adapters/secondaries/messager';
|
||||
import { UsernameRepository } from '../../adapters/secondaries/username.repository';
|
||||
import { AddUsernameCommand } from '../../commands/add-username.command';
|
||||
import { Username } from '../entities/username';
|
||||
|
||||
@CommandHandler(AddUsernameCommand)
|
||||
export class AddUsernameUseCase {
|
||||
constructor(
|
||||
private readonly usernameRepository: UsernameRepository,
|
||||
private readonly messager: Messager,
|
||||
) {}
|
||||
|
||||
execute = async (command: AddUsernameCommand): Promise<Username> => {
|
||||
const { uuid, username, type } = command.addUsernameRequest;
|
||||
try {
|
||||
return await this.usernameRepository.create({
|
||||
uuid,
|
||||
type,
|
||||
username,
|
||||
});
|
||||
} catch (error) {
|
||||
this.messager.publish(
|
||||
'logging.auth.username.add.warning',
|
||||
JSON.stringify({
|
||||
command,
|
||||
error,
|
||||
}),
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import { CommandHandler } from '@nestjs/cqrs';
|
||||
import { AuthenticationRepository } from '../../adapters/secondaries/authentication.repository';
|
||||
import { CreateAuthenticationCommand } from '../../commands/create-authentication.command';
|
||||
import { Authentication } from '../entities/authentication';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { UsernameRepository } from '../../adapters/secondaries/username.repository';
|
||||
import { Messager } from '../../adapters/secondaries/messager';
|
||||
|
||||
@CommandHandler(CreateAuthenticationCommand)
|
||||
export class CreateAuthenticationUseCase {
|
||||
constructor(
|
||||
private readonly authenticationRepository: AuthenticationRepository,
|
||||
private readonly usernameRepository: UsernameRepository,
|
||||
private readonly messager: Messager,
|
||||
) {}
|
||||
|
||||
execute = async (
|
||||
command: CreateAuthenticationCommand,
|
||||
): Promise<Authentication> => {
|
||||
const { uuid, password, ...username } = command.createAuthenticationRequest;
|
||||
const hash = await bcrypt.hash(password, 10);
|
||||
|
||||
try {
|
||||
const auth = await this.authenticationRepository.create({
|
||||
uuid,
|
||||
password: hash,
|
||||
});
|
||||
|
||||
await this.usernameRepository.create({
|
||||
uuid,
|
||||
...username,
|
||||
});
|
||||
|
||||
return auth;
|
||||
} catch (error) {
|
||||
this.messager.publish(
|
||||
'logging.auth.create.crit',
|
||||
JSON.stringify({
|
||||
command,
|
||||
error,
|
||||
}),
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import { CommandHandler } from '@nestjs/cqrs';
|
||||
import { AuthenticationRepository } from '../../adapters/secondaries/authentication.repository';
|
||||
import { Messager } from '../../adapters/secondaries/messager';
|
||||
import { UsernameRepository } from '../../adapters/secondaries/username.repository';
|
||||
import { DeleteAuthenticationCommand } from '../../commands/delete-authentication.command';
|
||||
import { Authentication } from '../entities/authentication';
|
||||
|
||||
@CommandHandler(DeleteAuthenticationCommand)
|
||||
export class DeleteAuthenticationUseCase {
|
||||
constructor(
|
||||
private readonly authenticationRepository: AuthenticationRepository,
|
||||
private readonly usernameRepository: UsernameRepository,
|
||||
private readonly messager: Messager,
|
||||
) {}
|
||||
|
||||
execute = async (
|
||||
command: DeleteAuthenticationCommand,
|
||||
): Promise<Authentication> => {
|
||||
try {
|
||||
await this.usernameRepository.deleteMany({
|
||||
uuid: command.deleteAuthenticationRequest.uuid,
|
||||
});
|
||||
return await this.authenticationRepository.delete(
|
||||
command.deleteAuthenticationRequest.uuid,
|
||||
);
|
||||
} catch (error) {
|
||||
this.messager.publish(
|
||||
'logging.auth.delete.crit',
|
||||
JSON.stringify({
|
||||
command,
|
||||
error,
|
||||
}),
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { UnauthorizedException } from '@nestjs/common';
|
||||
import { CommandHandler } from '@nestjs/cqrs';
|
||||
import { Messager } from '../../adapters/secondaries/messager';
|
||||
import { UsernameRepository } from '../../adapters/secondaries/username.repository';
|
||||
import { DeleteUsernameCommand } from '../../commands/delete-username.command';
|
||||
|
||||
@CommandHandler(DeleteUsernameCommand)
|
||||
export class DeleteUsernameUseCase {
|
||||
constructor(
|
||||
private readonly usernameRepository: UsernameRepository,
|
||||
private readonly messager: Messager,
|
||||
) {}
|
||||
|
||||
execute = async (command: DeleteUsernameCommand): Promise<void> => {
|
||||
try {
|
||||
const { username } = command.deleteUsernameRequest;
|
||||
const usernameFound = await this.usernameRepository.findOne({
|
||||
username,
|
||||
});
|
||||
const usernames = await this.usernameRepository.findAll(1, 1, {
|
||||
uuid: usernameFound.uuid,
|
||||
});
|
||||
if (usernames.total > 1) {
|
||||
return await this.usernameRepository.deleteMany({ username });
|
||||
}
|
||||
throw new UnauthorizedException();
|
||||
} catch (error) {
|
||||
this.messager.publish(
|
||||
'logging.auth.username.delete.warning',
|
||||
JSON.stringify({
|
||||
command,
|
||||
error,
|
||||
}),
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { CommandHandler } from '@nestjs/cqrs';
|
||||
import { AuthenticationRepository } from '../../adapters/secondaries/authentication.repository';
|
||||
import { Authentication } from '../entities/authentication';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { UpdatePasswordCommand } from '../../commands/update-password.command';
|
||||
import { Messager } from '../../adapters/secondaries/messager';
|
||||
|
||||
@CommandHandler(UpdatePasswordCommand)
|
||||
export class UpdatePasswordUseCase {
|
||||
constructor(
|
||||
private readonly authenticationRepository: AuthenticationRepository,
|
||||
private readonly messager: Messager,
|
||||
) {}
|
||||
|
||||
execute = async (command: UpdatePasswordCommand): Promise<Authentication> => {
|
||||
const { uuid, password } = command.updatePasswordRequest;
|
||||
const hash = await bcrypt.hash(password, 10);
|
||||
|
||||
try {
|
||||
return await this.authenticationRepository.update(uuid, {
|
||||
password: hash,
|
||||
});
|
||||
} catch (error) {
|
||||
this.messager.publish(
|
||||
'logging.auth.password.update.warning',
|
||||
JSON.stringify({
|
||||
command,
|
||||
error,
|
||||
}),
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
import { Mapper } from '@automapper/core';
|
||||
import { InjectMapper } from '@automapper/nestjs';
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { CommandBus, CommandHandler } from '@nestjs/cqrs';
|
||||
import { Messager } from '../../adapters/secondaries/messager';
|
||||
import { UsernameRepository } from '../../adapters/secondaries/username.repository';
|
||||
import { AddUsernameCommand } from '../../commands/add-username.command';
|
||||
import { UpdateUsernameCommand } from '../../commands/update-username.command';
|
||||
import { AddUsernameRequest } from '../dtos/add-username.request';
|
||||
import { UpdateUsernameRequest } from '../dtos/update-username.request';
|
||||
import { Username } from '../entities/username';
|
||||
|
||||
@CommandHandler(UpdateUsernameCommand)
|
||||
export class UpdateUsernameUseCase {
|
||||
constructor(
|
||||
private readonly usernameRepository: UsernameRepository,
|
||||
private readonly commandBus: CommandBus,
|
||||
@InjectMapper() private readonly mapper: Mapper,
|
||||
private readonly messager: Messager,
|
||||
) {}
|
||||
|
||||
execute = async (command: UpdateUsernameCommand): Promise<Username> => {
|
||||
const { uuid, username, type } = command.updateUsernameRequest;
|
||||
if (!username) throw new BadRequestException();
|
||||
// update username if it exists, otherwise create it
|
||||
const existingUsername = await this.usernameRepository.findOne({
|
||||
uuid,
|
||||
type,
|
||||
});
|
||||
if (existingUsername) {
|
||||
try {
|
||||
return await this.usernameRepository.updateWhere(
|
||||
{
|
||||
uuid_type: {
|
||||
uuid,
|
||||
type,
|
||||
},
|
||||
},
|
||||
{
|
||||
username,
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
this.messager.publish(
|
||||
'logging.auth.username.update.warning',
|
||||
JSON.stringify({
|
||||
command,
|
||||
error,
|
||||
}),
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
const addUsernameRequest = this.mapper.map(
|
||||
command.updateUsernameRequest,
|
||||
UpdateUsernameRequest,
|
||||
AddUsernameRequest,
|
||||
);
|
||||
try {
|
||||
return await this.commandBus.execute(
|
||||
new AddUsernameCommand(addUsernameRequest),
|
||||
);
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { QueryHandler } from '@nestjs/cqrs';
|
||||
import { AuthenticationRepository } from '../../adapters/secondaries/authentication.repository';
|
||||
import { ValidateAuthenticationQuery as ValidateAuthenticationQuery } from '../../queries/validate-authentication.query';
|
||||
import { Authentication } from '../entities/authentication';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { NotFoundException, UnauthorizedException } from '@nestjs/common';
|
||||
import { UsernameRepository } from '../../adapters/secondaries/username.repository';
|
||||
import { Username } from '../entities/username';
|
||||
|
||||
@QueryHandler(ValidateAuthenticationQuery)
|
||||
export class ValidateAuthenticationUseCase {
|
||||
constructor(
|
||||
private readonly authenticationRepository: AuthenticationRepository,
|
||||
private readonly usernameRepository: UsernameRepository,
|
||||
) {}
|
||||
|
||||
execute = async (
|
||||
validate: ValidateAuthenticationQuery,
|
||||
): Promise<Authentication> => {
|
||||
let username = new Username();
|
||||
try {
|
||||
username = await this.usernameRepository.findOne({
|
||||
username: validate.username,
|
||||
});
|
||||
} catch (e) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
try {
|
||||
const auth = await this.authenticationRepository.findOne({
|
||||
uuid: username.uuid,
|
||||
});
|
||||
if (auth) {
|
||||
const isMatch = await bcrypt.compare(validate.password, auth.password);
|
||||
if (isMatch) return auth;
|
||||
}
|
||||
throw new UnauthorizedException();
|
||||
} catch (e) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import {
|
||||
LoggerBase,
|
||||
MessagePublisherPort,
|
||||
PrismaRepositoryBase,
|
||||
} from '@mobicoop/ddd-library';
|
||||
import { AuthenticationEntity } from '../core/domain/authentication.entity';
|
||||
import { AuthenticationRepositoryPort } from '../core/application/ports/authentication.repository.port';
|
||||
import { PrismaService } from './prisma.service';
|
||||
import { AuthenticationMapper } from '../authentication.mapper';
|
||||
import { MESSAGE_PUBLISHER } from '@src/app.di-tokens';
|
||||
|
||||
export type AuthenticationBaseModel = {
|
||||
uuid: string;
|
||||
password: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
|
||||
export type AuthenticationReadModel = AuthenticationBaseModel & {
|
||||
usernames: UsernameModel[];
|
||||
};
|
||||
|
||||
export type AuthenticationWriteModel = AuthenticationBaseModel & {
|
||||
usernames: {
|
||||
create: UsernameModel[];
|
||||
};
|
||||
};
|
||||
|
||||
export type UsernameModel = {
|
||||
username: string;
|
||||
type: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Repository is used for retrieving/saving domain entities
|
||||
* */
|
||||
@Injectable()
|
||||
export class AuthenticationRepository
|
||||
extends PrismaRepositoryBase<
|
||||
AuthenticationEntity,
|
||||
AuthenticationReadModel,
|
||||
AuthenticationWriteModel
|
||||
>
|
||||
implements AuthenticationRepositoryPort
|
||||
{
|
||||
constructor(
|
||||
prisma: PrismaService,
|
||||
mapper: AuthenticationMapper,
|
||||
eventEmitter: EventEmitter2,
|
||||
@Inject(MESSAGE_PUBLISHER)
|
||||
protected readonly messagePublisher: MessagePublisherPort,
|
||||
) {
|
||||
super(
|
||||
prisma.auth,
|
||||
prisma,
|
||||
mapper,
|
||||
eventEmitter,
|
||||
new LoggerBase({
|
||||
logger: new Logger(AuthenticationRepository.name),
|
||||
domain: 'auth',
|
||||
messagePublisher,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
15
src/modules/authentication/infrastructure/prisma.service.ts
Normal file
15
src/modules/authentication/infrastructure/prisma.service.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { INestApplication, Injectable, OnModuleInit } from '@nestjs/common';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
@Injectable()
|
||||
export class PrismaService extends PrismaClient implements OnModuleInit {
|
||||
async onModuleInit() {
|
||||
await this.$connect();
|
||||
}
|
||||
|
||||
async enableShutdownHooks(app: INestApplication) {
|
||||
this.$on('beforeExit', async () => {
|
||||
await app.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import {
|
||||
LoggerBase,
|
||||
MessagePublisherPort,
|
||||
PrismaRepositoryBase,
|
||||
} from '@mobicoop/ddd-library';
|
||||
import { PrismaService } from './prisma.service';
|
||||
import { MESSAGE_PUBLISHER } from '@src/app.di-tokens';
|
||||
import { UsernameEntity } from '../core/domain/username.entity';
|
||||
import { UsernameRepositoryPort } from '../core/application/ports/username.repository.port';
|
||||
import { UsernameMapper } from '../username.mapper';
|
||||
|
||||
export type UsernameModel = {
|
||||
username: string;
|
||||
authUuid: string;
|
||||
type: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
|
||||
/**
|
||||
* Repository is used for retrieving/saving domain entities
|
||||
* */
|
||||
@Injectable()
|
||||
export class UsernameRepository
|
||||
extends PrismaRepositoryBase<UsernameEntity, UsernameModel, UsernameModel>
|
||||
implements UsernameRepositoryPort
|
||||
{
|
||||
constructor(
|
||||
prisma: PrismaService,
|
||||
mapper: UsernameMapper,
|
||||
eventEmitter: EventEmitter2,
|
||||
@Inject(MESSAGE_PUBLISHER)
|
||||
protected readonly messagePublisher: MessagePublisherPort,
|
||||
) {
|
||||
super(
|
||||
prisma.username,
|
||||
prisma,
|
||||
mapper,
|
||||
eventEmitter,
|
||||
new LoggerBase({
|
||||
logger: new Logger(UsernameRepository.name),
|
||||
domain: 'auth',
|
||||
messagePublisher,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { PaginatedResponseDto } from '@mobicoop/ddd-library';
|
||||
import { AuthenticationResponseDto } from './authentication.response.dto';
|
||||
|
||||
export class AauthenticationPaginatedResponseDto extends PaginatedResponseDto<AuthenticationResponseDto> {
|
||||
readonly data: readonly AuthenticationResponseDto[];
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { ResponseBase } from '@mobicoop/ddd-library';
|
||||
|
||||
export class AuthenticationResponseDto extends ResponseBase {}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { ResponseBase } from '@mobicoop/ddd-library';
|
||||
|
||||
export class UsernameResponseDto extends ResponseBase {}
|
||||
@@ -0,0 +1,49 @@
|
||||
import {
|
||||
AggregateID,
|
||||
IdResponse,
|
||||
RpcExceptionCode,
|
||||
RpcValidationPipe,
|
||||
} from '@mobicoop/ddd-library';
|
||||
import { Controller, UsePipes } from '@nestjs/common';
|
||||
import { CommandBus } from '@nestjs/cqrs';
|
||||
import { GrpcMethod, RpcException } from '@nestjs/microservices';
|
||||
import { UsernameAlreadyExistsException } from '@modules/authentication/core/domain/authentication.errors';
|
||||
import { AddUsernameRequestDto } from './dtos/add-username.request.dto';
|
||||
import { AddUsernameCommand } from '@modules/authentication/core/application/commands/add-usernames/add-username.command';
|
||||
|
||||
@UsePipes(
|
||||
new RpcValidationPipe({
|
||||
whitelist: true,
|
||||
forbidUnknownValues: false,
|
||||
}),
|
||||
)
|
||||
@Controller()
|
||||
export class AddUsernameGrpcController {
|
||||
constructor(private readonly commandBus: CommandBus) {}
|
||||
|
||||
@GrpcMethod('AuthenticationService', 'AddUsername')
|
||||
async addUsername(data: AddUsernameRequestDto): Promise<IdResponse> {
|
||||
try {
|
||||
const aggregateID: AggregateID = await this.commandBus.execute(
|
||||
new AddUsernameCommand({
|
||||
userId: data.userId,
|
||||
username: {
|
||||
name: data.name,
|
||||
type: data.type,
|
||||
},
|
||||
}),
|
||||
);
|
||||
return new IdResponse(aggregateID);
|
||||
} catch (error: any) {
|
||||
if (error instanceof UsernameAlreadyExistsException)
|
||||
throw new RpcException({
|
||||
code: RpcExceptionCode.ALREADY_EXISTS,
|
||||
message: error.message,
|
||||
});
|
||||
throw new RpcException({
|
||||
code: RpcExceptionCode.UNKNOWN,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package authentication;
|
||||
|
||||
service AuthenticationService {
|
||||
rpc Validate(AuthenticationByUsernamePassword) returns (Id);
|
||||
rpc Create(Authentication) returns (Id);
|
||||
rpc AddUsername(Username) returns (Id);
|
||||
rpc UpdatePassword(Password) returns (Id);
|
||||
rpc UpdateUsername(Username) returns (Id);
|
||||
rpc DeleteUsername(Username) returns (Id);
|
||||
rpc Delete(UserId) returns (Empty);
|
||||
}
|
||||
|
||||
message AuthenticationByUsernamePassword {
|
||||
string username = 1;
|
||||
string password = 2;
|
||||
}
|
||||
|
||||
message Authentication {
|
||||
string userId = 1;
|
||||
repeated Username usernames = 2;
|
||||
string password = 3;
|
||||
}
|
||||
|
||||
message Password {
|
||||
string userId = 1;
|
||||
string password = 2;
|
||||
}
|
||||
|
||||
message Username {
|
||||
string userId = 1;
|
||||
string name = 2;
|
||||
string type = 3;
|
||||
}
|
||||
|
||||
message Id {
|
||||
string id = 1;
|
||||
}
|
||||
|
||||
message UserId {
|
||||
string userId = 1;
|
||||
}
|
||||
|
||||
message Empty {}
|
||||
@@ -0,0 +1,43 @@
|
||||
import {
|
||||
AggregateID,
|
||||
IdResponse,
|
||||
RpcExceptionCode,
|
||||
RpcValidationPipe,
|
||||
} from '@mobicoop/ddd-library';
|
||||
import { Controller, UsePipes } from '@nestjs/common';
|
||||
import { CommandBus } from '@nestjs/cqrs';
|
||||
import { GrpcMethod, RpcException } from '@nestjs/microservices';
|
||||
import { CreateAuthenticationRequestDto } from './dtos/create-authentication.request.dto';
|
||||
import { CreateAuthenticationCommand } from '@modules/authentication/core/application/commands/create-authentication/create-authentication.command';
|
||||
import { AuthenticationAlreadyExistsException } from '@modules/authentication/core/domain/authentication.errors';
|
||||
|
||||
@UsePipes(
|
||||
new RpcValidationPipe({
|
||||
whitelist: true,
|
||||
forbidUnknownValues: false,
|
||||
}),
|
||||
)
|
||||
@Controller()
|
||||
export class CreateAuthenticationGrpcController {
|
||||
constructor(private readonly commandBus: CommandBus) {}
|
||||
|
||||
@GrpcMethod('AuthenticationService', 'Create')
|
||||
async create(data: CreateAuthenticationRequestDto): Promise<IdResponse> {
|
||||
try {
|
||||
const aggregateID: AggregateID = await this.commandBus.execute(
|
||||
new CreateAuthenticationCommand(data),
|
||||
);
|
||||
return new IdResponse(aggregateID);
|
||||
} catch (error: any) {
|
||||
if (error instanceof AuthenticationAlreadyExistsException)
|
||||
throw new RpcException({
|
||||
code: RpcExceptionCode.ALREADY_EXISTS,
|
||||
message: error.message,
|
||||
});
|
||||
throw new RpcException({
|
||||
code: RpcExceptionCode.UNKNOWN,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import {
|
||||
DatabaseErrorException,
|
||||
NotFoundException,
|
||||
RpcExceptionCode,
|
||||
RpcValidationPipe,
|
||||
} from '@mobicoop/ddd-library';
|
||||
import { Controller, UsePipes } from '@nestjs/common';
|
||||
import { CommandBus } from '@nestjs/cqrs';
|
||||
import { GrpcMethod, RpcException } from '@nestjs/microservices';
|
||||
import { DeleteAuthenticationCommand } from '@modules/authentication/core/application/commands/delete-authentication/delete-authentication.command';
|
||||
import { DeleteAuthenticationRequestDto } from './dtos/delete-authentication.request.dto';
|
||||
|
||||
@UsePipes(
|
||||
new RpcValidationPipe({
|
||||
whitelist: true,
|
||||
forbidUnknownValues: false,
|
||||
}),
|
||||
)
|
||||
@Controller()
|
||||
export class DeleteAuthenticationGrpcController {
|
||||
constructor(private readonly commandBus: CommandBus) {}
|
||||
|
||||
@GrpcMethod('AuthenticationService', 'Delete')
|
||||
async delete(data: DeleteAuthenticationRequestDto): Promise<void> {
|
||||
try {
|
||||
await this.commandBus.execute(new DeleteAuthenticationCommand(data));
|
||||
} catch (error: any) {
|
||||
if (error instanceof NotFoundException)
|
||||
throw new RpcException({
|
||||
code: RpcExceptionCode.NOT_FOUND,
|
||||
message: error.message,
|
||||
});
|
||||
|
||||
if (error instanceof DatabaseErrorException)
|
||||
throw new RpcException({
|
||||
code: RpcExceptionCode.INTERNAL,
|
||||
message: error.message,
|
||||
});
|
||||
throw new RpcException({
|
||||
code: RpcExceptionCode.UNKNOWN,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
import { UsernameDto } from './username.dto';
|
||||
|
||||
export class AddUsernameRequestDto extends UsernameDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
userId: string;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
ArrayMinSize,
|
||||
IsArray,
|
||||
IsNotEmpty,
|
||||
IsString,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { UsernameDto } from './username.dto';
|
||||
|
||||
export class CreateAuthenticationRequestDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
userId: string;
|
||||
|
||||
@Type(() => UsernameDto)
|
||||
@IsArray()
|
||||
@ArrayMinSize(1)
|
||||
@ValidateNested({ each: true })
|
||||
usernames: UsernameDto[];
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
password: string;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
export class DeleteAuthenticationRequestDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
userId: string;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Type } from '@modules/authentication/core/domain/username.types';
|
||||
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
|
||||
import { IsValidUsername } from './validators/decorators/is-valid-username.decorator';
|
||||
|
||||
export class UsernameDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@IsValidUsername({
|
||||
message: 'Invalid username',
|
||||
})
|
||||
name: string;
|
||||
|
||||
@IsEnum(Type)
|
||||
@IsNotEmpty()
|
||||
type: Type;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Type } from '@modules/authentication/core/domain/username.types';
|
||||
import {
|
||||
registerDecorator,
|
||||
ValidationOptions,
|
||||
ValidationArguments,
|
||||
isEmail,
|
||||
isPhoneNumber,
|
||||
} from 'class-validator';
|
||||
|
||||
export function IsValidUsername(validationOptions?: ValidationOptions) {
|
||||
return function (object: any, propertyName: string) {
|
||||
registerDecorator({
|
||||
name: 'isValidUsername',
|
||||
target: object.constructor,
|
||||
propertyName: propertyName,
|
||||
options: validationOptions,
|
||||
validator: {
|
||||
validate(value: any, args: ValidationArguments) {
|
||||
const usernameType: Type = args.object['type'];
|
||||
switch (usernameType) {
|
||||
case Type.PHONE:
|
||||
return isPhoneNumber(value);
|
||||
case Type.EMAIL:
|
||||
return isEmail(value);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { createMap, Mapper } from '@automapper/core';
|
||||
import { AutomapperProfile, InjectMapper } from '@automapper/nestjs';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthenticationPresenter } from '../adapters/primaries/authentication.presenter';
|
||||
import { Authentication } from '../domain/entities/authentication';
|
||||
|
||||
@Injectable()
|
||||
export class AuthenticationProfile extends AutomapperProfile {
|
||||
constructor(@InjectMapper() mapper: Mapper) {
|
||||
super(mapper);
|
||||
}
|
||||
|
||||
override get profile() {
|
||||
return (mapper: any) => {
|
||||
createMap(mapper, Authentication, AuthenticationPresenter);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { createMap, Mapper } from '@automapper/core';
|
||||
import { AutomapperProfile, InjectMapper } from '@automapper/nestjs';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { UsernamePresenter } from '../adapters/primaries/username.presenter';
|
||||
import { AddUsernameRequest } from '../domain/dtos/add-username.request';
|
||||
import { UpdateUsernameRequest } from '../domain/dtos/update-username.request';
|
||||
import { Username } from '../domain/entities/username';
|
||||
|
||||
@Injectable()
|
||||
export class UsernameProfile extends AutomapperProfile {
|
||||
constructor(@InjectMapper() mapper: Mapper) {
|
||||
super(mapper);
|
||||
}
|
||||
|
||||
override get profile() {
|
||||
return (mapper: any) => {
|
||||
createMap(mapper, Username, UsernamePresenter);
|
||||
createMap(mapper, UpdateUsernameRequest, AddUsernameRequest);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
export class ValidateAuthenticationQuery {
|
||||
readonly username: string;
|
||||
readonly password: string;
|
||||
|
||||
constructor(username: string, password: string) {
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
}
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
import { TestingModule, Test } from '@nestjs/testing';
|
||||
import { DatabaseModule } from '../../../database/database.module';
|
||||
import { PrismaService } from '../../../database/adapters/secondaries/prisma-service';
|
||||
import { DatabaseException } from '../../../database/exceptions/database.exception';
|
||||
import { AuthenticationRepository } from '../../adapters/secondaries/authentication.repository';
|
||||
import { v4 } from 'uuid';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { Authentication } from '../../domain/entities/authentication';
|
||||
|
||||
describe('AuthenticationRepository', () => {
|
||||
let prismaService: PrismaService;
|
||||
let authenticationRepository: AuthenticationRepository;
|
||||
|
||||
const createAuthentications = async (nbToCreate = 10) => {
|
||||
for (let i = 0; i < nbToCreate; i++) {
|
||||
await prismaService.auth.create({
|
||||
data: {
|
||||
uuid: v4(),
|
||||
password: bcrypt.hashSync(`password-${i}`, 10),
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [DatabaseModule],
|
||||
providers: [AuthenticationRepository, PrismaService],
|
||||
}).compile();
|
||||
|
||||
prismaService = module.get<PrismaService>(PrismaService);
|
||||
authenticationRepository = module.get<AuthenticationRepository>(
|
||||
AuthenticationRepository,
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await prismaService.$disconnect();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await prismaService.auth.deleteMany();
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('should return an empty data array', async () => {
|
||||
const res = await authenticationRepository.findAll();
|
||||
expect(res).toEqual({
|
||||
data: [],
|
||||
total: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a data array with 8 auths', async () => {
|
||||
await createAuthentications(8);
|
||||
const auths = await authenticationRepository.findAll();
|
||||
expect(auths.data.length).toBe(8);
|
||||
expect(auths.total).toBe(8);
|
||||
});
|
||||
|
||||
it('should return a data array limited to 10 authentications', async () => {
|
||||
await createAuthentications(20);
|
||||
const auths = await authenticationRepository.findAll();
|
||||
expect(auths.data.length).toBe(10);
|
||||
expect(auths.total).toBe(20);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOneByUuid', () => {
|
||||
it('should return an authentication', async () => {
|
||||
const authToFind = await prismaService.auth.create({
|
||||
data: {
|
||||
uuid: v4(),
|
||||
password: bcrypt.hashSync(`password`, 10),
|
||||
},
|
||||
});
|
||||
|
||||
const auth = await authenticationRepository.findOneByUuid(
|
||||
authToFind.uuid,
|
||||
);
|
||||
expect(auth.uuid).toBe(authToFind.uuid);
|
||||
});
|
||||
|
||||
it('should return null', async () => {
|
||||
const auth = await authenticationRepository.findOneByUuid(
|
||||
'544572be-11fb-4244-8235-587221fc9104',
|
||||
);
|
||||
expect(auth).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create an authentication', async () => {
|
||||
const beforeCount = await prismaService.auth.count();
|
||||
|
||||
const authenticationToCreate: Authentication = new Authentication();
|
||||
authenticationToCreate.uuid = v4();
|
||||
authenticationToCreate.password = bcrypt.hashSync(`password`, 10);
|
||||
const authentication = await authenticationRepository.create(
|
||||
authenticationToCreate,
|
||||
);
|
||||
|
||||
const afterCount = await prismaService.auth.count();
|
||||
|
||||
expect(afterCount - beforeCount).toBe(1);
|
||||
expect(authentication.uuid).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update authentication password', async () => {
|
||||
const authenticationToUpdate = await prismaService.auth.create({
|
||||
data: {
|
||||
uuid: v4(),
|
||||
password: bcrypt.hashSync(`password`, 10),
|
||||
},
|
||||
});
|
||||
|
||||
const toUpdate: Authentication = new Authentication();
|
||||
toUpdate.password = bcrypt.hashSync(`newPassword`, 10);
|
||||
const updatedAuthentication = await authenticationRepository.update(
|
||||
authenticationToUpdate.uuid,
|
||||
toUpdate,
|
||||
);
|
||||
|
||||
expect(updatedAuthentication.uuid).toBe(authenticationToUpdate.uuid);
|
||||
});
|
||||
|
||||
it('should throw DatabaseException', async () => {
|
||||
const toUpdate: Authentication = new Authentication();
|
||||
toUpdate.password = bcrypt.hashSync(`newPassword`, 10);
|
||||
|
||||
await expect(
|
||||
authenticationRepository.update(
|
||||
'544572be-11fb-4244-8235-587221fc9104',
|
||||
toUpdate,
|
||||
),
|
||||
).rejects.toBeInstanceOf(DatabaseException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should delete an authentication', async () => {
|
||||
const authenticationToRemove = await prismaService.auth.create({
|
||||
data: {
|
||||
uuid: v4(),
|
||||
password: bcrypt.hashSync(`password`, 10),
|
||||
},
|
||||
});
|
||||
await authenticationRepository.delete(authenticationToRemove.uuid);
|
||||
|
||||
const count = await prismaService.auth.count();
|
||||
expect(count).toBe(0);
|
||||
});
|
||||
|
||||
it('should throw DatabaseException', async () => {
|
||||
await expect(
|
||||
authenticationRepository.delete('544572be-11fb-4244-8235-587221fc9104'),
|
||||
).rejects.toBeInstanceOf(DatabaseException);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,282 +0,0 @@
|
||||
import { TestingModule, Test } from '@nestjs/testing';
|
||||
import { DatabaseModule } from '../../../database/database.module';
|
||||
import { PrismaService } from '../../../database/adapters/secondaries/prisma-service';
|
||||
import { DatabaseException } from '../../../database/exceptions/database.exception';
|
||||
import { v4 } from 'uuid';
|
||||
import { Type } from '../../domain/dtos/type.enum';
|
||||
import { UsernameRepository } from '../../adapters/secondaries/username.repository';
|
||||
import { Username } from '../../domain/entities/username';
|
||||
|
||||
describe('UsernameRepository', () => {
|
||||
let prismaService: PrismaService;
|
||||
let usernameRepository: UsernameRepository;
|
||||
|
||||
const createUsernames = async (nbToCreate = 10) => {
|
||||
for (let i = 0; i < nbToCreate; i++) {
|
||||
await prismaService.username.create({
|
||||
data: {
|
||||
uuid: v4(),
|
||||
username: `john.doe.${i}@email.com`,
|
||||
type: Type.EMAIL,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [DatabaseModule],
|
||||
providers: [UsernameRepository, PrismaService],
|
||||
}).compile();
|
||||
|
||||
prismaService = module.get<PrismaService>(PrismaService);
|
||||
usernameRepository = module.get<UsernameRepository>(UsernameRepository);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await prismaService.$disconnect();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await prismaService.username.deleteMany();
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('should return an empty data array', async () => {
|
||||
const res = await usernameRepository.findAll();
|
||||
expect(res).toEqual({
|
||||
data: [],
|
||||
total: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a data array with 8 usernames', async () => {
|
||||
await createUsernames(8);
|
||||
const usernames = await usernameRepository.findAll();
|
||||
expect(usernames.data.length).toBe(8);
|
||||
expect(usernames.total).toBe(8);
|
||||
});
|
||||
|
||||
it('should return a data array limited to 10 usernames', async () => {
|
||||
await createUsernames(20);
|
||||
const usernames = await usernameRepository.findAll();
|
||||
expect(usernames.data.length).toBe(10);
|
||||
expect(usernames.total).toBe(20);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOne', () => {
|
||||
it('should return a username with uuid and email', async () => {
|
||||
const usernameToFind = await prismaService.username.create({
|
||||
data: {
|
||||
uuid: v4(),
|
||||
username: 'john.doe@email.com',
|
||||
type: Type.EMAIL,
|
||||
},
|
||||
});
|
||||
|
||||
const username = await usernameRepository.findOne({
|
||||
username: 'john.doe@email.com',
|
||||
type: Type.EMAIL,
|
||||
});
|
||||
expect(username.uuid).toBe(usernameToFind.uuid);
|
||||
});
|
||||
|
||||
it('should return null', async () => {
|
||||
const username = await usernameRepository.findOne({
|
||||
username: 'jane.doe@email.com',
|
||||
type: Type.EMAIL,
|
||||
});
|
||||
expect(username).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a username with an email', async () => {
|
||||
const beforeCount = await prismaService.username.count();
|
||||
|
||||
const usernameToCreate: Username = new Username();
|
||||
usernameToCreate.uuid = v4();
|
||||
usernameToCreate.username = 'john.doe@email.com';
|
||||
usernameToCreate.type = Type.EMAIL;
|
||||
const username = await usernameRepository.create(usernameToCreate);
|
||||
|
||||
const afterCount = await prismaService.username.count();
|
||||
|
||||
expect(afterCount - beforeCount).toBe(1);
|
||||
expect(username.uuid).toBeDefined();
|
||||
});
|
||||
it('should create a username with a phone number', async () => {
|
||||
const beforeCount = await prismaService.username.count();
|
||||
|
||||
const usernameToCreate: Username = new Username();
|
||||
usernameToCreate.uuid = v4();
|
||||
usernameToCreate.username = '+33611223344';
|
||||
usernameToCreate.type = Type.PHONE;
|
||||
const username = await usernameRepository.create(usernameToCreate);
|
||||
|
||||
const afterCount = await prismaService.username.count();
|
||||
|
||||
expect(afterCount - beforeCount).toBe(1);
|
||||
expect(username.uuid).toBeDefined();
|
||||
});
|
||||
it('should create a username with an email for an existing uuid', async () => {
|
||||
const beforeCount = await prismaService.username.count();
|
||||
|
||||
const uuid = v4();
|
||||
|
||||
const firstUsernameToCreate: Username = new Username();
|
||||
firstUsernameToCreate.uuid = uuid;
|
||||
firstUsernameToCreate.username = '+33611223344';
|
||||
firstUsernameToCreate.type = Type.PHONE;
|
||||
const firstUsername = await usernameRepository.create(
|
||||
firstUsernameToCreate,
|
||||
);
|
||||
|
||||
const secondUsernameToCreate: Username = new Username();
|
||||
secondUsernameToCreate.uuid = uuid;
|
||||
secondUsernameToCreate.username = 'john.doe@email.com';
|
||||
secondUsernameToCreate.type = Type.EMAIL;
|
||||
const secondUsername = await usernameRepository.create(
|
||||
secondUsernameToCreate,
|
||||
);
|
||||
|
||||
const afterCount = await prismaService.username.count();
|
||||
|
||||
expect(afterCount - beforeCount).toBe(2);
|
||||
expect(firstUsername.uuid).toEqual(secondUsername.uuid);
|
||||
});
|
||||
it('should throw DatabaseException if username already exists for a given type', async () => {
|
||||
const uuid = v4();
|
||||
|
||||
const firstUsernameToCreate: Username = new Username();
|
||||
firstUsernameToCreate.uuid = uuid;
|
||||
firstUsernameToCreate.username = 'john.doe@email.com';
|
||||
firstUsernameToCreate.type = Type.EMAIL;
|
||||
await usernameRepository.create(firstUsernameToCreate);
|
||||
|
||||
const secondUsernameToCreate: Username = new Username();
|
||||
secondUsernameToCreate.uuid = uuid;
|
||||
secondUsernameToCreate.username = 'jane.doe@email.com';
|
||||
secondUsernameToCreate.type = Type.EMAIL;
|
||||
|
||||
await expect(
|
||||
usernameRepository.create(secondUsernameToCreate),
|
||||
).rejects.toBeInstanceOf(DatabaseException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update username email', async () => {
|
||||
const usernameToUpdate = await prismaService.username.create({
|
||||
data: {
|
||||
uuid: v4(),
|
||||
username: `john.doe@email.com`,
|
||||
type: Type.EMAIL,
|
||||
},
|
||||
});
|
||||
|
||||
const toUpdate: Username = new Username();
|
||||
toUpdate.username = 'jane.doe@email.com';
|
||||
const updatedUsername = await usernameRepository.updateWhere(
|
||||
{
|
||||
uuid_type: {
|
||||
uuid: usernameToUpdate.uuid,
|
||||
type: usernameToUpdate.type,
|
||||
},
|
||||
},
|
||||
{
|
||||
username: toUpdate.username,
|
||||
},
|
||||
);
|
||||
|
||||
expect(updatedUsername.uuid).toBe(usernameToUpdate.uuid);
|
||||
expect(updatedUsername.username).toBe('jane.doe@email.com');
|
||||
});
|
||||
|
||||
it('should update username phone', async () => {
|
||||
const usernameToUpdate = await prismaService.username.create({
|
||||
data: {
|
||||
uuid: v4(),
|
||||
username: `+33611223344`,
|
||||
type: Type.PHONE,
|
||||
},
|
||||
});
|
||||
|
||||
const toUpdate: Username = new Username();
|
||||
toUpdate.username = '+33622334455';
|
||||
const updatedUsername = await usernameRepository.updateWhere(
|
||||
{
|
||||
uuid_type: {
|
||||
uuid: usernameToUpdate.uuid,
|
||||
type: usernameToUpdate.type,
|
||||
},
|
||||
},
|
||||
{
|
||||
username: toUpdate.username,
|
||||
},
|
||||
);
|
||||
|
||||
expect(updatedUsername.uuid).toBe(usernameToUpdate.uuid);
|
||||
expect(updatedUsername.username).toBe('+33622334455');
|
||||
});
|
||||
it('should throw DatabaseException if email not found', async () => {
|
||||
const toUpdate: Username = new Username();
|
||||
toUpdate.username = 'jane.doe@email.com';
|
||||
|
||||
await expect(
|
||||
usernameRepository.updateWhere(
|
||||
{
|
||||
uuid_type: {
|
||||
uuid: '544572be-11fb-4244-8235-587221fc9104',
|
||||
type: Type.EMAIL,
|
||||
},
|
||||
},
|
||||
{
|
||||
username: toUpdate.username,
|
||||
},
|
||||
),
|
||||
).rejects.toBeInstanceOf(DatabaseException);
|
||||
});
|
||||
it('should throw DatabaseException if phone not found', async () => {
|
||||
const toUpdate: Username = new Username();
|
||||
toUpdate.username = '+33611223344';
|
||||
|
||||
await expect(
|
||||
usernameRepository.updateWhere(
|
||||
{
|
||||
uuid_type: {
|
||||
uuid: '544572be-11fb-4244-8235-587221fc9104',
|
||||
type: Type.PHONE,
|
||||
},
|
||||
},
|
||||
{
|
||||
username: toUpdate.username,
|
||||
},
|
||||
),
|
||||
).rejects.toBeInstanceOf(DatabaseException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should delete a username', async () => {
|
||||
const usernameToRemove = await prismaService.username.create({
|
||||
data: {
|
||||
uuid: v4(),
|
||||
username: `+33611223344`,
|
||||
type: Type.PHONE,
|
||||
},
|
||||
});
|
||||
await usernameRepository.deleteMany({ uuid: usernameToRemove.uuid });
|
||||
|
||||
const count = await prismaService.username.count();
|
||||
expect(count).toBe(0);
|
||||
});
|
||||
|
||||
it('should throw DatabaseException', async () => {
|
||||
await expect(
|
||||
usernameRepository.delete('544572be-11fb-4244-8235-587221fc9104'),
|
||||
).rejects.toBeInstanceOf(DatabaseException);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,79 +0,0 @@
|
||||
import { classes } from '@automapper/classes';
|
||||
import { AutomapperModule } from '@automapper/nestjs';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AuthenticationProfile } from '../../mappers/authentication.profile';
|
||||
import { UsernameRepository } from '../../adapters/secondaries/username.repository';
|
||||
import { Username } from '../../domain/entities/username';
|
||||
import { Type } from '../../domain/dtos/type.enum';
|
||||
import { AddUsernameRequest } from '../../domain/dtos/add-username.request';
|
||||
import { AddUsernameCommand } from '../../commands/add-username.command';
|
||||
import { AddUsernameUseCase } from '../../domain/usecases/add-username.usecase';
|
||||
import { Messager } from '../../adapters/secondaries/messager';
|
||||
|
||||
const addUsernameRequest: AddUsernameRequest = {
|
||||
uuid: 'bb281075-1b98-4456-89d6-c643d3044a91',
|
||||
username: '0611223344',
|
||||
type: Type.PHONE,
|
||||
};
|
||||
const addUsernameCommand: AddUsernameCommand = new AddUsernameCommand(
|
||||
addUsernameRequest,
|
||||
);
|
||||
|
||||
const mockUsernameRepository = {
|
||||
create: jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => {
|
||||
return Promise.resolve(addUsernameRequest);
|
||||
})
|
||||
.mockImplementation(() => {
|
||||
throw new Error('Already exists');
|
||||
}),
|
||||
};
|
||||
|
||||
const mockMessager = {
|
||||
publish: jest.fn().mockImplementation(),
|
||||
};
|
||||
|
||||
describe('AddUsernameUseCase', () => {
|
||||
let addUsernameUseCase: AddUsernameUseCase;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })],
|
||||
providers: [
|
||||
{
|
||||
provide: UsernameRepository,
|
||||
useValue: mockUsernameRepository,
|
||||
},
|
||||
{
|
||||
provide: Messager,
|
||||
useValue: mockMessager,
|
||||
},
|
||||
AddUsernameUseCase,
|
||||
AuthenticationProfile,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
addUsernameUseCase = module.get<AddUsernameUseCase>(AddUsernameUseCase);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(addUsernameUseCase).toBeDefined();
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should add a username for phone type', async () => {
|
||||
const addedUsername: Username = await addUsernameUseCase.execute(
|
||||
addUsernameCommand,
|
||||
);
|
||||
|
||||
expect(addedUsername.username).toBe(addUsernameRequest.username);
|
||||
expect(addedUsername.type).toBe(addUsernameRequest.type);
|
||||
});
|
||||
it('should throw an error if user already exists', async () => {
|
||||
await expect(
|
||||
addUsernameUseCase.execute(addUsernameCommand),
|
||||
).rejects.toBeInstanceOf(Error);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,99 +0,0 @@
|
||||
import { classes } from '@automapper/classes';
|
||||
import { AutomapperModule } from '@automapper/nestjs';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AuthenticationRepository } from '../../adapters/secondaries/authentication.repository';
|
||||
import { CreateAuthenticationCommand } from '../../commands/create-authentication.command';
|
||||
import { CreateAuthenticationRequest } from '../../domain/dtos/create-authentication.request';
|
||||
import { Authentication } from '../../domain/entities/authentication';
|
||||
import { CreateAuthenticationUseCase } from '../../domain/usecases/create-authentication.usecase';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { UsernameRepository } from '../../adapters/secondaries/username.repository';
|
||||
import { Type } from '../../domain/dtos/type.enum';
|
||||
import { Messager } from '../../adapters/secondaries/messager';
|
||||
|
||||
const newAuthenticationRequest: CreateAuthenticationRequest =
|
||||
new CreateAuthenticationRequest();
|
||||
newAuthenticationRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a91';
|
||||
newAuthenticationRequest.username = 'john.doe@email.com';
|
||||
newAuthenticationRequest.password = 'John123';
|
||||
newAuthenticationRequest.type = Type.EMAIL;
|
||||
const newAuthCommand: CreateAuthenticationCommand =
|
||||
new CreateAuthenticationCommand(newAuthenticationRequest);
|
||||
|
||||
const mockAuthenticationRepository = {
|
||||
create: jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
uuid: newAuthenticationRequest.uuid,
|
||||
password: bcrypt.hashSync(newAuthenticationRequest.password, 10),
|
||||
});
|
||||
})
|
||||
.mockImplementation(() => {
|
||||
throw new Error('Already exists');
|
||||
}),
|
||||
};
|
||||
|
||||
const mockUsernameRepository = {
|
||||
create: jest.fn().mockResolvedValue({
|
||||
uuid: newAuthenticationRequest.uuid,
|
||||
username: newAuthenticationRequest.username,
|
||||
type: newAuthenticationRequest.type,
|
||||
}),
|
||||
};
|
||||
|
||||
const mockMessager = {
|
||||
publish: jest.fn().mockImplementation(),
|
||||
};
|
||||
|
||||
describe('CreateAuthenticationUseCase', () => {
|
||||
let createAuthenticationUseCase: CreateAuthenticationUseCase;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })],
|
||||
providers: [
|
||||
{
|
||||
provide: AuthenticationRepository,
|
||||
useValue: mockAuthenticationRepository,
|
||||
},
|
||||
{
|
||||
provide: UsernameRepository,
|
||||
useValue: mockUsernameRepository,
|
||||
},
|
||||
{
|
||||
provide: Messager,
|
||||
useValue: mockMessager,
|
||||
},
|
||||
CreateAuthenticationUseCase,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
createAuthenticationUseCase = module.get<CreateAuthenticationUseCase>(
|
||||
CreateAuthenticationUseCase,
|
||||
);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(createAuthenticationUseCase).toBeDefined();
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should create an authentication with an encrypted password', async () => {
|
||||
const newAuthentication: Authentication =
|
||||
await createAuthenticationUseCase.execute(newAuthCommand);
|
||||
|
||||
expect(
|
||||
bcrypt.compareSync(
|
||||
newAuthenticationRequest.password,
|
||||
newAuthentication.password,
|
||||
),
|
||||
).toBeTruthy();
|
||||
});
|
||||
it('should throw an error if user already exists', async () => {
|
||||
await expect(
|
||||
createAuthenticationUseCase.execute(newAuthCommand),
|
||||
).rejects.toBeInstanceOf(Error);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,102 +0,0 @@
|
||||
import { classes } from '@automapper/classes';
|
||||
import { AutomapperModule } from '@automapper/nestjs';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AuthenticationRepository } from '../../adapters/secondaries/authentication.repository';
|
||||
import { Messager } from '../../adapters/secondaries/messager';
|
||||
import { UsernameRepository } from '../../adapters/secondaries/username.repository';
|
||||
import { DeleteAuthenticationCommand } from '../../commands/delete-authentication.command';
|
||||
import { DeleteAuthenticationRequest } from '../../domain/dtos/delete-authentication.request';
|
||||
import { Type } from '../../domain/dtos/type.enum';
|
||||
import { DeleteAuthenticationUseCase } from '../../domain/usecases/delete-authentication.usecase';
|
||||
|
||||
const usernames = {
|
||||
data: [
|
||||
{
|
||||
uuid: 'cf76af29-f75d-4f6e-bb40-4ecbcfa3356e',
|
||||
username: 'john.doe@email.com',
|
||||
type: Type.EMAIL,
|
||||
},
|
||||
{
|
||||
uuid: 'cf76af29-f75d-4f6e-bb40-4ecbcfa3356e',
|
||||
username: '0611223344',
|
||||
type: Type.PHONE,
|
||||
},
|
||||
],
|
||||
total: 2,
|
||||
};
|
||||
|
||||
const deleteAuthenticationRequest: DeleteAuthenticationRequest =
|
||||
new DeleteAuthenticationRequest();
|
||||
deleteAuthenticationRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a91';
|
||||
const deleteAuthenticationCommand: DeleteAuthenticationCommand =
|
||||
new DeleteAuthenticationCommand(deleteAuthenticationRequest);
|
||||
|
||||
const mockAuthenticationRepository = {
|
||||
delete: jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce(undefined)
|
||||
.mockImplementation(() => {
|
||||
throw new Error('Error');
|
||||
}),
|
||||
};
|
||||
|
||||
const mockUsernameRepository = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
findAll: jest.fn().mockImplementation((page, perPage, query) => {
|
||||
return Promise.resolve(usernames);
|
||||
}),
|
||||
delete: jest.fn().mockResolvedValue(undefined),
|
||||
deleteMany: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
const mockMessager = {
|
||||
publish: jest.fn().mockImplementation(),
|
||||
};
|
||||
|
||||
describe('DeleteAuthenticationUseCase', () => {
|
||||
let deleteAuthenticationUseCase: DeleteAuthenticationUseCase;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })],
|
||||
providers: [
|
||||
{
|
||||
provide: AuthenticationRepository,
|
||||
useValue: mockAuthenticationRepository,
|
||||
},
|
||||
{
|
||||
provide: UsernameRepository,
|
||||
useValue: mockUsernameRepository,
|
||||
},
|
||||
{
|
||||
provide: Messager,
|
||||
useValue: mockMessager,
|
||||
},
|
||||
DeleteAuthenticationUseCase,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
deleteAuthenticationUseCase = module.get<DeleteAuthenticationUseCase>(
|
||||
DeleteAuthenticationUseCase,
|
||||
);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(deleteAuthenticationUseCase).toBeDefined();
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should delete an authentication and its usernames', async () => {
|
||||
const deletedAuthentication = await deleteAuthenticationUseCase.execute(
|
||||
deleteAuthenticationCommand,
|
||||
);
|
||||
|
||||
expect(deletedAuthentication).toBe(undefined);
|
||||
});
|
||||
it('should throw an error if authentication does not exist', async () => {
|
||||
await expect(
|
||||
deleteAuthenticationUseCase.execute(deleteAuthenticationCommand),
|
||||
).rejects.toBeInstanceOf(Error);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,115 +0,0 @@
|
||||
import { classes } from '@automapper/classes';
|
||||
import { AutomapperModule } from '@automapper/nestjs';
|
||||
import { UnauthorizedException } from '@nestjs/common';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Messager } from '../../adapters/secondaries/messager';
|
||||
import { UsernameRepository } from '../../adapters/secondaries/username.repository';
|
||||
import { DeleteUsernameCommand } from '../../commands/delete-username.command';
|
||||
import { DeleteUsernameRequest } from '../../domain/dtos/delete-username.request';
|
||||
import { Type } from '../../domain/dtos/type.enum';
|
||||
import { DeleteUsernameUseCase } from '../../domain/usecases/delete-username.usecase';
|
||||
|
||||
const usernamesEmail = {
|
||||
data: [
|
||||
{
|
||||
uuid: 'cf76af29-f75d-4f6e-bb40-4ecbcfa3356e',
|
||||
username: 'john.doe@email.com',
|
||||
type: Type.EMAIL,
|
||||
},
|
||||
{
|
||||
uuid: 'cf76af29-f75d-4f6e-bb40-4ecbcfa3356e',
|
||||
username: '0611223344',
|
||||
type: Type.PHONE,
|
||||
},
|
||||
],
|
||||
total: 2,
|
||||
};
|
||||
|
||||
const usernamesPhone = {
|
||||
data: [
|
||||
{
|
||||
uuid: 'a7fa221f-dd77-481c-bb77-ae89da662c87',
|
||||
username: '0611223344',
|
||||
type: Type.PHONE,
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
};
|
||||
|
||||
const deleteUsernameEmailRequest: DeleteUsernameRequest =
|
||||
new DeleteUsernameRequest();
|
||||
deleteUsernameEmailRequest.username = 'john.doe@email.com';
|
||||
|
||||
const deleteUsernamePhoneRequest: DeleteUsernameRequest =
|
||||
new DeleteUsernameRequest();
|
||||
deleteUsernamePhoneRequest.username = '0611223344';
|
||||
|
||||
const deleteUsernameEmailCommand: DeleteUsernameCommand =
|
||||
new DeleteUsernameCommand(deleteUsernameEmailRequest);
|
||||
|
||||
const deleteUsernamePhoneCommand: DeleteUsernameCommand =
|
||||
new DeleteUsernameCommand(deleteUsernamePhoneRequest);
|
||||
|
||||
const mockUsernameRepository = {
|
||||
findOne: jest.fn().mockImplementation((where) => {
|
||||
if (where.username == 'john.doe@email.com') {
|
||||
return { uuid: 'cf76af29-f75d-4f6e-bb40-4ecbcfa3356e' };
|
||||
}
|
||||
return { uuid: 'a7fa221f-dd77-481c-bb77-ae89da662c87' };
|
||||
}),
|
||||
findAll: jest.fn().mockImplementation((page, perPage, query) => {
|
||||
if (query.uuid == 'cf76af29-f75d-4f6e-bb40-4ecbcfa3356e') {
|
||||
return Promise.resolve(usernamesEmail);
|
||||
}
|
||||
return Promise.resolve(usernamesPhone);
|
||||
}),
|
||||
deleteMany: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
const mockMessager = {
|
||||
publish: jest.fn().mockImplementation(),
|
||||
};
|
||||
|
||||
describe('DeleteUsernameUseCase', () => {
|
||||
let deleteUsernameUseCase: DeleteUsernameUseCase;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })],
|
||||
providers: [
|
||||
{
|
||||
provide: UsernameRepository,
|
||||
useValue: mockUsernameRepository,
|
||||
},
|
||||
{
|
||||
provide: Messager,
|
||||
useValue: mockMessager,
|
||||
},
|
||||
DeleteUsernameUseCase,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
deleteUsernameUseCase = module.get<DeleteUsernameUseCase>(
|
||||
DeleteUsernameUseCase,
|
||||
);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(deleteUsernameUseCase).toBeDefined();
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should delete a username', async () => {
|
||||
const deletedEmailUsername = await deleteUsernameUseCase.execute(
|
||||
deleteUsernameEmailCommand,
|
||||
);
|
||||
expect(deletedEmailUsername).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should throw an exception if auth has only one username', async () => {
|
||||
await expect(
|
||||
deleteUsernameUseCase.execute(deleteUsernamePhoneCommand),
|
||||
).rejects.toBeInstanceOf(UnauthorizedException);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
import { IdResponse } from '@mobicoop/ddd-library';
|
||||
import { RpcExceptionCode } from '@mobicoop/ddd-library';
|
||||
import { UsernameAlreadyExistsException } from '@modules/authentication/core/domain/authentication.errors';
|
||||
import { Type } from '@modules/authentication/core/domain/username.types';
|
||||
import { AddUsernameGrpcController } from '@modules/authentication/interface/grpc-controllers/add-username.grpc.controller';
|
||||
import { AddUsernameRequestDto } from '@modules/authentication/interface/grpc-controllers/dtos/add-username.request.dto';
|
||||
import { CommandBus } from '@nestjs/cqrs';
|
||||
import { RpcException } from '@nestjs/microservices';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
const addUsernameRequest: AddUsernameRequestDto = {
|
||||
userId: '78153e03-4861-4f58-a705-88526efee53b',
|
||||
name: 'john.doe@email.com',
|
||||
type: Type.EMAIL,
|
||||
};
|
||||
|
||||
const mockCommandBus = {
|
||||
execute: jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => 'john.doe@email.com')
|
||||
.mockImplementationOnce(() => {
|
||||
throw new UsernameAlreadyExistsException();
|
||||
})
|
||||
.mockImplementationOnce(() => {
|
||||
throw new Error();
|
||||
}),
|
||||
};
|
||||
|
||||
describe('Add Username Grpc Controller', () => {
|
||||
let addUsernameGrpcController: AddUsernameGrpcController;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: CommandBus,
|
||||
useValue: mockCommandBus,
|
||||
},
|
||||
AddUsernameGrpcController,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
addUsernameGrpcController = module.get<AddUsernameGrpcController>(
|
||||
AddUsernameGrpcController,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(addUsernameGrpcController).toBeDefined();
|
||||
});
|
||||
|
||||
it('should add a new username', async () => {
|
||||
jest.spyOn(mockCommandBus, 'execute');
|
||||
const result: IdResponse = await addUsernameGrpcController.addUsername(
|
||||
addUsernameRequest,
|
||||
);
|
||||
expect(result).toBeInstanceOf(IdResponse);
|
||||
expect(result.id).toBe('john.doe@email.com');
|
||||
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw a dedicated RpcException if username already exists', async () => {
|
||||
jest.spyOn(mockCommandBus, 'execute');
|
||||
expect.assertions(3);
|
||||
try {
|
||||
await addUsernameGrpcController.addUsername(addUsernameRequest);
|
||||
} catch (e: any) {
|
||||
expect(e).toBeInstanceOf(RpcException);
|
||||
expect(e.error.code).toBe(RpcExceptionCode.ALREADY_EXISTS);
|
||||
}
|
||||
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw a generic RpcException', async () => {
|
||||
jest.spyOn(mockCommandBus, 'execute');
|
||||
expect.assertions(3);
|
||||
try {
|
||||
await addUsernameGrpcController.addUsername(addUsernameRequest);
|
||||
} catch (e: any) {
|
||||
expect(e).toBeInstanceOf(RpcException);
|
||||
expect(e.error.code).toBe(RpcExceptionCode.UNKNOWN);
|
||||
}
|
||||
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,99 @@
|
||||
import { IdResponse } from '@mobicoop/ddd-library';
|
||||
import { RpcExceptionCode } from '@mobicoop/ddd-library';
|
||||
import { AuthenticationAlreadyExistsException } from '@modules/authentication/core/domain/authentication.errors';
|
||||
import { Type } from '@modules/authentication/core/domain/username.types';
|
||||
import { CreateAuthenticationGrpcController } from '@modules/authentication/interface/grpc-controllers/create-authentication.grpc.controller';
|
||||
import { CreateAuthenticationRequestDto } from '@modules/authentication/interface/grpc-controllers/dtos/create-authentication.request.dto';
|
||||
import { CommandBus } from '@nestjs/cqrs';
|
||||
import { RpcException } from '@nestjs/microservices';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
const createAuthenticationRequest: CreateAuthenticationRequestDto = {
|
||||
userId: '78153e03-4861-4f58-a705-88526efee53b',
|
||||
password: 'John123',
|
||||
usernames: [
|
||||
{
|
||||
name: 'john.doe@email.com',
|
||||
type: Type.EMAIL,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const mockCommandBus = {
|
||||
execute: jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => '78153e03-4861-4f58-a705-88526efee53b')
|
||||
.mockImplementationOnce(() => {
|
||||
throw new AuthenticationAlreadyExistsException();
|
||||
})
|
||||
.mockImplementationOnce(() => {
|
||||
throw new Error();
|
||||
}),
|
||||
};
|
||||
|
||||
describe('Create Authentication Grpc Controller', () => {
|
||||
let createAuthenticationGrpcController: CreateAuthenticationGrpcController;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: CommandBus,
|
||||
useValue: mockCommandBus,
|
||||
},
|
||||
CreateAuthenticationGrpcController,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
createAuthenticationGrpcController =
|
||||
module.get<CreateAuthenticationGrpcController>(
|
||||
CreateAuthenticationGrpcController,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(createAuthenticationGrpcController).toBeDefined();
|
||||
});
|
||||
|
||||
it('should create a new authentication', async () => {
|
||||
jest.spyOn(mockCommandBus, 'execute');
|
||||
const result: IdResponse = await createAuthenticationGrpcController.create(
|
||||
createAuthenticationRequest,
|
||||
);
|
||||
expect(result).toBeInstanceOf(IdResponse);
|
||||
expect(result.id).toBe('78153e03-4861-4f58-a705-88526efee53b');
|
||||
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw a dedicated RpcException if authentication already exists', async () => {
|
||||
jest.spyOn(mockCommandBus, 'execute');
|
||||
expect.assertions(3);
|
||||
try {
|
||||
await createAuthenticationGrpcController.create(
|
||||
createAuthenticationRequest,
|
||||
);
|
||||
} catch (e: any) {
|
||||
expect(e).toBeInstanceOf(RpcException);
|
||||
expect(e.error.code).toBe(RpcExceptionCode.ALREADY_EXISTS);
|
||||
}
|
||||
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw a generic RpcException', async () => {
|
||||
jest.spyOn(mockCommandBus, 'execute');
|
||||
expect.assertions(3);
|
||||
try {
|
||||
await createAuthenticationGrpcController.create(
|
||||
createAuthenticationRequest,
|
||||
);
|
||||
} catch (e: any) {
|
||||
expect(e).toBeInstanceOf(RpcException);
|
||||
expect(e.error.code).toBe(RpcExceptionCode.UNKNOWN);
|
||||
}
|
||||
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
import {
|
||||
DatabaseErrorException,
|
||||
NotFoundException,
|
||||
} from '@mobicoop/ddd-library';
|
||||
import { RpcExceptionCode } from '@mobicoop/ddd-library';
|
||||
import { DeleteAuthenticationGrpcController } from '@modules/authentication/interface/grpc-controllers/delete-authentication.grpc.controller';
|
||||
import { DeleteAuthenticationRequestDto } from '@modules/authentication/interface/grpc-controllers/dtos/delete-authentication.request.dto';
|
||||
import { CommandBus } from '@nestjs/cqrs';
|
||||
import { RpcException } from '@nestjs/microservices';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
const deleteAuthenticationRequest: DeleteAuthenticationRequestDto = {
|
||||
userId: '78153e03-4861-4f58-a705-88526efee53b',
|
||||
};
|
||||
|
||||
const mockCommandBus = {
|
||||
execute: jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => ({}))
|
||||
.mockImplementationOnce(() => {
|
||||
throw new NotFoundException();
|
||||
})
|
||||
.mockImplementationOnce(() => {
|
||||
throw new DatabaseErrorException();
|
||||
})
|
||||
.mockImplementationOnce(() => {
|
||||
throw new Error();
|
||||
}),
|
||||
};
|
||||
|
||||
describe('Delete Authentication Grpc Controller', () => {
|
||||
let deleteAuthenticationGrpcController: DeleteAuthenticationGrpcController;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: CommandBus,
|
||||
useValue: mockCommandBus,
|
||||
},
|
||||
DeleteAuthenticationGrpcController,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
deleteAuthenticationGrpcController =
|
||||
module.get<DeleteAuthenticationGrpcController>(
|
||||
DeleteAuthenticationGrpcController,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(deleteAuthenticationGrpcController).toBeDefined();
|
||||
});
|
||||
|
||||
it('should create a new authentication', async () => {
|
||||
jest.spyOn(mockCommandBus, 'execute');
|
||||
await deleteAuthenticationGrpcController.delete(
|
||||
deleteAuthenticationRequest,
|
||||
);
|
||||
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw a dedicated RpcException if authentication does not exist', async () => {
|
||||
jest.spyOn(mockCommandBus, 'execute');
|
||||
expect.assertions(3);
|
||||
try {
|
||||
await deleteAuthenticationGrpcController.delete(
|
||||
deleteAuthenticationRequest,
|
||||
);
|
||||
} catch (e: any) {
|
||||
expect(e).toBeInstanceOf(RpcException);
|
||||
expect(e.error.code).toBe(RpcExceptionCode.NOT_FOUND);
|
||||
}
|
||||
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw a dedicated RpcException if a database error occurs', async () => {
|
||||
jest.spyOn(mockCommandBus, 'execute');
|
||||
expect.assertions(3);
|
||||
try {
|
||||
await deleteAuthenticationGrpcController.delete(
|
||||
deleteAuthenticationRequest,
|
||||
);
|
||||
} catch (e: any) {
|
||||
expect(e).toBeInstanceOf(RpcException);
|
||||
expect(e.error.code).toBe(RpcExceptionCode.INTERNAL);
|
||||
}
|
||||
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw a generic RpcException', async () => {
|
||||
jest.spyOn(mockCommandBus, 'execute');
|
||||
expect.assertions(3);
|
||||
try {
|
||||
await deleteAuthenticationGrpcController.delete(
|
||||
deleteAuthenticationRequest,
|
||||
);
|
||||
} catch (e: any) {
|
||||
expect(e).toBeInstanceOf(RpcException);
|
||||
expect(e.error.code).toBe(RpcExceptionCode.UNKNOWN);
|
||||
}
|
||||
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Type } from '@modules/authentication/core/domain/username.types';
|
||||
import { IsValidUsername } from '@modules/authentication/interface/grpc-controllers/dtos/validators/decorators/is-valid-username.decorator';
|
||||
import { Validator } from 'class-validator';
|
||||
|
||||
describe('Username decorator', () => {
|
||||
class MyClass {
|
||||
@IsValidUsername({
|
||||
message: 'Invalid username',
|
||||
})
|
||||
name: string;
|
||||
|
||||
type: Type;
|
||||
}
|
||||
it('should return a property decorator has a function', () => {
|
||||
const isValidUsername = IsValidUsername();
|
||||
expect(typeof isValidUsername).toBe('function');
|
||||
});
|
||||
it('should validate a valid phone username', async () => {
|
||||
const myClassInstance = new MyClass();
|
||||
myClassInstance.name = '+33611223344';
|
||||
myClassInstance.type = Type.PHONE;
|
||||
const validator = new Validator();
|
||||
const validation = await validator.validate(myClassInstance);
|
||||
expect(validation.length).toBe(0);
|
||||
});
|
||||
it('should validate a valid email username', async () => {
|
||||
const myClassInstance = new MyClass();
|
||||
myClassInstance.name = 'john.doe@email.com';
|
||||
myClassInstance.type = Type.EMAIL;
|
||||
const validator = new Validator();
|
||||
const validation = await validator.validate(myClassInstance);
|
||||
expect(validation.length).toBe(0);
|
||||
});
|
||||
it('should not validate an invalid phone username', async () => {
|
||||
const myClassInstance = new MyClass();
|
||||
myClassInstance.name = '11223344';
|
||||
myClassInstance.type = Type.PHONE;
|
||||
const validator = new Validator();
|
||||
const validation = await validator.validate(myClassInstance);
|
||||
expect(validation.length).toBe(1);
|
||||
});
|
||||
it('should not validate an invalid email username', async () => {
|
||||
const myClassInstance = new MyClass();
|
||||
myClassInstance.name = 'john.doe.email.com';
|
||||
myClassInstance.type = Type.EMAIL;
|
||||
const validator = new Validator();
|
||||
const validation = await validator.validate(myClassInstance);
|
||||
expect(validation.length).toBe(1);
|
||||
});
|
||||
it('should not validate if type is not set', async () => {
|
||||
const myClassInstance = new MyClass();
|
||||
myClassInstance.name = 'john.doe@email.com';
|
||||
const validator = new Validator();
|
||||
const validation = await validator.validate(myClassInstance);
|
||||
expect(validation.length).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -1,47 +0,0 @@
|
||||
import { AmqpConnection } from '@golevelup/nestjs-rabbitmq';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Messager } from '../../adapters/secondaries/messager';
|
||||
|
||||
const mockAmqpConnection = {
|
||||
publish: jest.fn().mockImplementation(),
|
||||
};
|
||||
|
||||
const mockConfigService = {
|
||||
get: jest.fn().mockResolvedValue({
|
||||
RMQ_EXCHANGE: 'mobicoop',
|
||||
}),
|
||||
};
|
||||
|
||||
describe('Messager', () => {
|
||||
let messager: Messager;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [],
|
||||
providers: [
|
||||
Messager,
|
||||
{
|
||||
provide: AmqpConnection,
|
||||
useValue: mockAmqpConnection,
|
||||
},
|
||||
{
|
||||
provide: ConfigService,
|
||||
useValue: mockConfigService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
messager = module.get<Messager>(Messager);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(messager).toBeDefined();
|
||||
});
|
||||
|
||||
it('should publish a message', async () => {
|
||||
jest.spyOn(mockAmqpConnection, 'publish');
|
||||
messager.publish('authentication.create.info', 'my-test');
|
||||
expect(mockAmqpConnection.publish).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -1,81 +0,0 @@
|
||||
import { classes } from '@automapper/classes';
|
||||
import { AutomapperModule } from '@automapper/nestjs';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AuthenticationRepository } from '../../adapters/secondaries/authentication.repository';
|
||||
import { Authentication } from '../../domain/entities/authentication';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { UpdatePasswordRequest } from '../../domain/dtos/update-password.request';
|
||||
import { UpdatePasswordCommand } from '../../commands/update-password.command';
|
||||
import { UpdatePasswordUseCase } from '../../domain/usecases/update-password.usecase';
|
||||
import { Messager } from '../../adapters/secondaries/messager';
|
||||
|
||||
const updatePasswordRequest: UpdatePasswordRequest =
|
||||
new UpdatePasswordRequest();
|
||||
updatePasswordRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a91';
|
||||
updatePasswordRequest.password = 'John123';
|
||||
|
||||
const updatePasswordCommand: UpdatePasswordCommand = new UpdatePasswordCommand(
|
||||
updatePasswordRequest,
|
||||
);
|
||||
|
||||
const mockAuthenticationRepository = {
|
||||
update: jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
uuid: updatePasswordRequest.uuid,
|
||||
password: bcrypt.hashSync(updatePasswordRequest.password, 10),
|
||||
})
|
||||
.mockImplementation(() => {
|
||||
throw new Error('Error');
|
||||
}),
|
||||
};
|
||||
|
||||
const mockMessager = {
|
||||
publish: jest.fn().mockImplementation(),
|
||||
};
|
||||
|
||||
describe('UpdatePasswordUseCase', () => {
|
||||
let updatePasswordUseCase: UpdatePasswordUseCase;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })],
|
||||
providers: [
|
||||
{
|
||||
provide: AuthenticationRepository,
|
||||
useValue: mockAuthenticationRepository,
|
||||
},
|
||||
{
|
||||
provide: Messager,
|
||||
useValue: mockMessager,
|
||||
},
|
||||
UpdatePasswordUseCase,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
updatePasswordUseCase = module.get<UpdatePasswordUseCase>(
|
||||
UpdatePasswordUseCase,
|
||||
);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(updatePasswordUseCase).toBeDefined();
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should update an auth with an new encrypted password', async () => {
|
||||
const newAuth: Authentication = await updatePasswordUseCase.execute(
|
||||
updatePasswordCommand,
|
||||
);
|
||||
|
||||
expect(
|
||||
bcrypt.compareSync(updatePasswordRequest.password, newAuth.password),
|
||||
).toBeTruthy();
|
||||
});
|
||||
it('should throw an error if auth does not exist', async () => {
|
||||
await expect(
|
||||
updatePasswordUseCase.execute(updatePasswordCommand),
|
||||
).rejects.toBeInstanceOf(Error);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,151 +0,0 @@
|
||||
import { classes } from '@automapper/classes';
|
||||
import { AutomapperModule } from '@automapper/nestjs';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { UsernameRepository } from '../../adapters/secondaries/username.repository';
|
||||
import { Username } from '../../domain/entities/username';
|
||||
import { UpdateUsernameRequest } from '../../domain/dtos/update-username.request';
|
||||
import { UpdateUsernameCommand } from '../../commands/update-username.command';
|
||||
import { Type } from '../../domain/dtos/type.enum';
|
||||
import { UpdateUsernameUseCase } from '../../domain/usecases/update-username.usecase';
|
||||
import { CommandBus } from '@nestjs/cqrs';
|
||||
import { UsernameProfile } from '../../mappers/username.profile';
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { Messager } from '../../adapters/secondaries/messager';
|
||||
|
||||
const existingUsername = {
|
||||
uuid: 'bb281075-1b98-4456-89d6-c643d3044a91',
|
||||
username: 'john.doe@email.com',
|
||||
type: Type.EMAIL,
|
||||
};
|
||||
|
||||
const newUsernameRequest: UpdateUsernameRequest = new UpdateUsernameRequest();
|
||||
newUsernameRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a90';
|
||||
newUsernameRequest.username = '+33611223344';
|
||||
newUsernameRequest.type = Type.PHONE;
|
||||
|
||||
const updateUsernameRequest: UpdateUsernameRequest =
|
||||
new UpdateUsernameRequest();
|
||||
updateUsernameRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a91';
|
||||
updateUsernameRequest.username = 'johnny.doe@email.com';
|
||||
updateUsernameRequest.type = Type.EMAIL;
|
||||
|
||||
const unknownUsernameRequest: UpdateUsernameRequest =
|
||||
new UpdateUsernameRequest();
|
||||
unknownUsernameRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a92';
|
||||
unknownUsernameRequest.username = 'unknown@email.com';
|
||||
unknownUsernameRequest.type = Type.EMAIL;
|
||||
|
||||
const invalidUpdateUsernameRequest: UpdateUsernameRequest =
|
||||
new UpdateUsernameRequest();
|
||||
invalidUpdateUsernameRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a93';
|
||||
invalidUpdateUsernameRequest.username = '';
|
||||
invalidUpdateUsernameRequest.type = Type.EMAIL;
|
||||
|
||||
const newUsernameCommand: UpdateUsernameCommand = new UpdateUsernameCommand(
|
||||
newUsernameRequest,
|
||||
);
|
||||
|
||||
const updateUsernameCommand: UpdateUsernameCommand = new UpdateUsernameCommand(
|
||||
updateUsernameRequest,
|
||||
);
|
||||
|
||||
const invalidUpdateUsernameCommand: UpdateUsernameCommand =
|
||||
new UpdateUsernameCommand(invalidUpdateUsernameRequest);
|
||||
|
||||
const unknownUpdateUsernameCommand: UpdateUsernameCommand =
|
||||
new UpdateUsernameCommand(unknownUsernameRequest);
|
||||
|
||||
const mockUsernameRepository = {
|
||||
findOne: jest.fn().mockImplementation((request) => {
|
||||
if (request.uuid == 'bb281075-1b98-4456-89d6-c643d3044a90') {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
return Promise.resolve(existingUsername);
|
||||
}),
|
||||
updateWhere: jest.fn().mockImplementation((request) => {
|
||||
if (request.uuid_type.uuid == 'bb281075-1b98-4456-89d6-c643d3044a90') {
|
||||
return Promise.resolve(newUsernameRequest);
|
||||
}
|
||||
if (request.uuid_type.uuid == 'bb281075-1b98-4456-89d6-c643d3044a91') {
|
||||
return Promise.resolve(updateUsernameRequest);
|
||||
}
|
||||
if (request.uuid_type.uuid == 'bb281075-1b98-4456-89d6-c643d3044a92') {
|
||||
throw new Error('Error');
|
||||
}
|
||||
return Promise.resolve(invalidUpdateUsernameRequest);
|
||||
}),
|
||||
};
|
||||
|
||||
const mockAddUsernameCommand = {
|
||||
execute: jest.fn().mockResolvedValue(newUsernameRequest),
|
||||
};
|
||||
|
||||
const mockMessager = {
|
||||
publish: jest.fn().mockImplementation(),
|
||||
};
|
||||
|
||||
describe('UpdateUsernameUseCase', () => {
|
||||
let updateUsernameUseCase: UpdateUsernameUseCase;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })],
|
||||
providers: [
|
||||
{
|
||||
provide: UsernameRepository,
|
||||
useValue: mockUsernameRepository,
|
||||
},
|
||||
{
|
||||
provide: CommandBus,
|
||||
useValue: mockAddUsernameCommand,
|
||||
},
|
||||
{
|
||||
provide: Messager,
|
||||
useValue: mockMessager,
|
||||
},
|
||||
UpdateUsernameUseCase,
|
||||
UsernameProfile,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
updateUsernameUseCase = module.get<UpdateUsernameUseCase>(
|
||||
UpdateUsernameUseCase,
|
||||
);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(updateUsernameUseCase).toBeDefined();
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should create a new username', async () => {
|
||||
const newUsername: Username = await updateUsernameUseCase.execute(
|
||||
newUsernameCommand,
|
||||
);
|
||||
|
||||
expect(newUsername.username).toBe(newUsernameRequest.username);
|
||||
expect(newUsername.type).toBe(newUsernameRequest.type);
|
||||
});
|
||||
|
||||
it('should update a username for email type', async () => {
|
||||
const updatedUsername: Username = await updateUsernameUseCase.execute(
|
||||
updateUsernameCommand,
|
||||
);
|
||||
|
||||
expect(updatedUsername.username).toBe(updateUsernameRequest.username);
|
||||
expect(updatedUsername.type).toBe(updateUsernameRequest.type);
|
||||
});
|
||||
|
||||
it('should throw an error if username does not exist', async () => {
|
||||
await expect(
|
||||
updateUsernameUseCase.execute(unknownUpdateUsernameCommand),
|
||||
).rejects.toBeInstanceOf(Error);
|
||||
});
|
||||
|
||||
it('should throw an exception if username is invalid', async () => {
|
||||
await expect(
|
||||
updateUsernameUseCase.execute(invalidUpdateUsernameCommand),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,107 +0,0 @@
|
||||
import { classes } from '@automapper/classes';
|
||||
import { AutomapperModule } from '@automapper/nestjs';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AuthenticationRepository } from '../../adapters/secondaries/authentication.repository';
|
||||
import { Authentication } from '../../domain/entities/authentication';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { ValidateAuthenticationUseCase } from '../../domain/usecases/validate-authentication.usecase';
|
||||
import { ValidateAuthenticationQuery } from '../../queries/validate-authentication.query';
|
||||
import { UsernameRepository } from '../../adapters/secondaries/username.repository';
|
||||
import { Type } from '../../domain/dtos/type.enum';
|
||||
import { NotFoundException, UnauthorizedException } from '@nestjs/common';
|
||||
import { DatabaseException } from '../../../database/exceptions/database.exception';
|
||||
import { ValidateAuthenticationRequest } from '../../domain/dtos/validate-authentication.request';
|
||||
|
||||
const mockAuthenticationRepository = {
|
||||
findOne: jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => ({
|
||||
uuid: 'bb281075-1b98-4456-89d6-c643d3044a91',
|
||||
password: bcrypt.hashSync('John123', 10),
|
||||
}))
|
||||
.mockImplementationOnce(() => ({
|
||||
uuid: 'bb281075-1b98-4456-89d6-c643d3044a91',
|
||||
password: bcrypt.hashSync('John123', 10),
|
||||
})),
|
||||
};
|
||||
|
||||
const mockUsernameRepository = {
|
||||
findOne: jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => ({
|
||||
uuid: 'bb281075-1b98-4456-89d6-c643d3044a91',
|
||||
username: 'john.doe@email.com',
|
||||
type: Type.EMAIL,
|
||||
}))
|
||||
.mockImplementationOnce(() => {
|
||||
throw new DatabaseException();
|
||||
})
|
||||
.mockImplementationOnce(() => ({
|
||||
uuid: 'bb281075-1b98-4456-89d6-c643d3044a91',
|
||||
username: 'john.doe@email.com',
|
||||
type: Type.EMAIL,
|
||||
})),
|
||||
};
|
||||
|
||||
describe('ValidateAuthenticationUseCase', () => {
|
||||
let validateAuthenticationUseCase: ValidateAuthenticationUseCase;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })],
|
||||
providers: [
|
||||
{
|
||||
provide: AuthenticationRepository,
|
||||
useValue: mockAuthenticationRepository,
|
||||
},
|
||||
{
|
||||
provide: UsernameRepository,
|
||||
useValue: mockUsernameRepository,
|
||||
},
|
||||
ValidateAuthenticationUseCase,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
validateAuthenticationUseCase = module.get<ValidateAuthenticationUseCase>(
|
||||
ValidateAuthenticationUseCase,
|
||||
);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(validateAuthenticationUseCase).toBeDefined();
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should validate an authentication and returns entity object', async () => {
|
||||
const validateAuthenticationRequest: ValidateAuthenticationRequest =
|
||||
new ValidateAuthenticationRequest();
|
||||
validateAuthenticationRequest.username = 'john.doe@email.com';
|
||||
validateAuthenticationRequest.password = 'John123';
|
||||
const authentication: Authentication =
|
||||
await validateAuthenticationUseCase.execute(
|
||||
new ValidateAuthenticationQuery(
|
||||
validateAuthenticationRequest.username,
|
||||
validateAuthenticationRequest.password,
|
||||
),
|
||||
);
|
||||
|
||||
expect(authentication.uuid).toBe('bb281075-1b98-4456-89d6-c643d3044a91');
|
||||
});
|
||||
|
||||
it('should not validate an authentication with unknown username and returns not found exception', async () => {
|
||||
await expect(
|
||||
validateAuthenticationUseCase.execute(
|
||||
new ValidateAuthenticationQuery('jane.doe@email.com', 'Jane123'),
|
||||
),
|
||||
).rejects.toBeInstanceOf(NotFoundException);
|
||||
});
|
||||
|
||||
it('should not validate an authentication with wrong password and returns unauthorized exception', async () => {
|
||||
await expect(
|
||||
validateAuthenticationUseCase.execute(
|
||||
new ValidateAuthenticationQuery('john.doe@email.com', 'John1234'),
|
||||
),
|
||||
).rejects.toBeInstanceOf(UnauthorizedException);
|
||||
});
|
||||
});
|
||||
});
|
||||
49
src/modules/authentication/username.mapper.ts
Normal file
49
src/modules/authentication/username.mapper.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Mapper } from '@mobicoop/ddd-library';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Type } from './core/domain/username.types';
|
||||
import { UsernameEntity } from './core/domain/username.entity';
|
||||
import { UsernameModel } from './infrastructure/username.repository';
|
||||
import { UsernameResponseDto } from './interface/dtos/username.response.dto';
|
||||
|
||||
/**
|
||||
* Mapper constructs objects that are used in different layers:
|
||||
* Record is an object that is stored in a database,
|
||||
* Entity is an object that is used in application domain layer,
|
||||
* and a ResponseDTO is an object returned to a user (usually as json).
|
||||
*/
|
||||
|
||||
@Injectable()
|
||||
export class UsernameMapper
|
||||
implements
|
||||
Mapper<UsernameEntity, UsernameModel, UsernameModel, UsernameResponseDto>
|
||||
{
|
||||
toPersistence = (entity: UsernameEntity): UsernameModel => {
|
||||
const copy = entity.getProps();
|
||||
const record: UsernameModel = {
|
||||
authUuid: copy.userId,
|
||||
username: copy.name,
|
||||
type: copy.type,
|
||||
createdAt: copy.createdAt,
|
||||
updatedAt: copy.updatedAt,
|
||||
};
|
||||
return record;
|
||||
};
|
||||
|
||||
toDomain = (record: UsernameModel): UsernameEntity => {
|
||||
const entity = new UsernameEntity({
|
||||
id: record.authUuid,
|
||||
createdAt: new Date(record.createdAt),
|
||||
updatedAt: new Date(record.updatedAt),
|
||||
props: {
|
||||
name: record.username,
|
||||
type: Type[record.type],
|
||||
},
|
||||
});
|
||||
return entity;
|
||||
};
|
||||
|
||||
toResponse = (entity: UsernameEntity): UsernameResponseDto => {
|
||||
const response = new UsernameResponseDto(entity);
|
||||
return response;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user