delete authentication, use custom logger

This commit is contained in:
sbriat 2023-07-05 17:32:21 +02:00
parent f33f679e12
commit bbcd2cdb9e
24 changed files with 269 additions and 54 deletions

View File

@ -6,10 +6,10 @@ HEALTH_SERVICE_PORT=6002
# PRISMA
DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=auth"
# RABBIT MQ
RMQ_URI=amqp://v3-broker:5672
RMQ_EXCHANGE=mobicoop
# MESSAGE BROKER
MESSAGE_BROKER_URI=amqp://v3-broker:5672
MESSAGE_BROKER_EXCHANGE=mobicoop
# OPA
OPA_IMAGE=openpolicyagent/opa:0.48.0-rootless
OPA_IMAGE=openpolicyagent/opa:0.54.0
OPA_URL=http://v3-auth-opa:8181/v1/data/

View File

@ -6,5 +6,5 @@ SERVICE_PORT=5002
DATABASE_URL="postgresql://mobicoop:mobicoop@localhost:5432/mobicoop-test?schema=auth"
# OPA
OPA_IMAGE=openpolicyagent/opa:0.48.0-rootless
OPA_IMAGE=openpolicyagent/opa:0.54.0
OPA_URL=http://v3-auth-opa:8181/v1/data/

54
package-lock.json generated
View File

@ -16,6 +16,7 @@
"@grpc/grpc-js": "^1.8.0",
"@grpc/proto-loader": "^0.7.4",
"@mobicoop/ddd-library": "file:../../packages/dddlibrary",
"@mobicoop/message-broker-module": "^1.2.0",
"@nestjs/axios": "^1.0.1",
"@nestjs/common": "^9.0.0",
"@nestjs/config": "^2.2.0",
@ -72,6 +73,24 @@
"node": ">=0.10.0"
}
},
"node_modules/@acuminous/bitsyntax": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/@acuminous/bitsyntax/-/bitsyntax-0.1.2.tgz",
"integrity": "sha512-29lUK80d1muEQqiUsSo+3A0yP6CdspgC95EnKBMi22Xlwt79i/En4Vr67+cXhU+cZjbti3TgGGC5wy1stIywVQ==",
"dependencies": {
"buffer-more-ints": "~1.0.0",
"debug": "^4.3.4",
"safe-buffer": "~5.1.2"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/@acuminous/bitsyntax/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"node_modules/@ampproject/remapping": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz",
@ -1500,6 +1519,33 @@
"reflect-metadata": "^0.1.12"
}
},
"node_modules/@mobicoop/message-broker-module": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@mobicoop/message-broker-module/-/message-broker-module-1.2.0.tgz",
"integrity": "sha512-RoSHHK1GyQ/QVDmm3JS/wBfh171oChvyEp6YWmJd12krFLrPVn9MoEvZdyT3I5J31oBiUabMPle5Kdpw+Nrmww==",
"dependencies": {
"@golevelup/nestjs-rabbitmq": "^3.6.0",
"@types/amqplib": "^0.10.1",
"amqplib": "^0.10.3"
},
"peerDependencies": {
"@nestjs/common": "^9.4.2"
}
},
"node_modules/@mobicoop/message-broker-module/node_modules/amqplib": {
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/amqplib/-/amqplib-0.10.3.tgz",
"integrity": "sha512-UHmuSa7n8vVW/a5HGh2nFPqAEr8+cD4dEZ6u9GjP91nHfr1a54RyAKyra7Sb5NH7NBKOUlyQSMXIp0qAixKexw==",
"dependencies": {
"@acuminous/bitsyntax": "^0.1.2",
"buffer-more-ints": "~1.0.0",
"readable-stream": "1.x >=1.1.9",
"url-parse": "~1.5.10"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@nestjs/axios": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-1.0.1.tgz",
@ -2189,6 +2235,14 @@
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
"dev": true
},
"node_modules/@types/amqplib": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@types/amqplib/-/amqplib-0.10.1.tgz",
"integrity": "sha512-j6ANKT79ncUDnAs/+9r9eDujxbeJoTjoVu33gHHcaPfmLQaMhvfbH2GqSe8KUM444epAp1Vl3peVOQfZk3UIqA==",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/babel__core": {
"version": "7.20.1",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.1.tgz",

View File

@ -37,6 +37,7 @@
"@grpc/grpc-js": "^1.8.0",
"@grpc/proto-loader": "^0.7.4",
"@mobicoop/ddd-library": "file:../../packages/dddlibrary",
"@mobicoop/message-broker-module": "^1.2.0",
"@nestjs/axios": "^1.0.1",
"@nestjs/common": "^9.0.0",
"@nestjs/config": "^2.2.0",

View File

@ -2,7 +2,8 @@
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
provider = "prisma-client-js"
binaryTargets = ["linux-musl", "debian-openssl-3.0.x"]
}
datasource db {

1
src/app.di-tokens.ts Normal file
View File

@ -0,0 +1 @@
export const MESSAGE_PUBLISHER = Symbol();

View File

@ -1,20 +1,35 @@
import { classes } from '@automapper/classes';
import { AutomapperModule } from '@automapper/nestjs';
// import { classes } from '@automapper/classes';
// import { AutomapperModule } from '@automapper/nestjs';
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AuthorizationModule } from './modules/authorization/authorization.module';
import { HealthModule } from './modules/health/health.module';
import { ConfigModule, ConfigService } from '@nestjs/config';
// import { AuthorizationModule } from './modules/authorization/authorization.module';
// import { HealthModule } from './modules/health/health.module';
import { AuthenticationModule } from '@modules/newauthentication/authentication.module';
import { EventEmitterModule } from '@nestjs/event-emitter';
import {
MessageBrokerModule,
MessageBrokerModuleOptions,
} from '@mobicoop/message-broker-module';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
EventEmitterModule.forRoot(),
AutomapperModule.forRoot({ strategyInitializer: classes() }),
MessageBrokerModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (
configService: ConfigService,
): Promise<MessageBrokerModuleOptions> => ({
uri: configService.get<string>('MESSAGE_BROKER_URI'),
exchange: configService.get<string>('MESSAGE_BROKER_EXCHANGE'),
name: 'auth',
}),
}),
// AutomapperModule.forRoot({ strategyInitializer: classes() }),
AuthenticationModule,
AuthorizationModule,
HealthModule,
// AuthorizationModule,
// HealthModule,
],
controllers: [],
providers: [],

View File

@ -49,6 +49,7 @@ export class AuthenticationMapper
createdAt: new Date(record.createdAt),
updatedAt: new Date(record.updatedAt),
props: {
userId: record.uuid,
password: record.password,
usernames: record.usernames.map((username: UsernameModel) => ({
name: username.username,

View File

@ -6,10 +6,20 @@ import { AUTHENTICATION_REPOSITORY } from './authentication.di-tokens';
import { AuthenticationRepository } from './infrastructure/authentication.repository';
import { PrismaService } from './infrastructure/prisma.service';
import { CqrsModule } from '@nestjs/cqrs';
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';
const grpcControllers = [CreateAuthenticationGrpcController];
const grpcControllers = [
CreateAuthenticationGrpcController,
DeleteAuthenticationGrpcController,
];
const commandHandlers: Provider[] = [CreateAuthenticationService];
const commandHandlers: Provider[] = [
CreateAuthenticationService,
DeleteAuthenticationService,
];
const mappers: Provider[] = [AuthenticationMapper];
@ -20,12 +30,25 @@ const repositories: Provider[] = [
},
];
const messageBrokers: Provider[] = [
{
provide: MESSAGE_PUBLISHER,
useClass: MessageBrokerPublisher,
},
];
const orms: Provider[] = [PrismaService];
@Module({
imports: [CqrsModule],
controllers: [...grpcControllers],
providers: [...commandHandlers, ...mappers, ...repositories, ...orms],
providers: [
...commandHandlers,
...mappers,
...repositories,
...messageBrokers,
...orms,
],
exports: [PrismaService, AuthenticationMapper, AUTHENTICATION_REPOSITORY],
})
export class AuthenticationModule {}

View File

@ -2,11 +2,13 @@ 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;
}

View File

@ -3,7 +3,7 @@ import { Inject } from '@nestjs/common';
import {
AggregateID,
ConflictException,
UniqueConflictException,
UniqueConstraintException,
} from '@mobicoop/ddd-library';
import { CreateAuthenticationCommand } from './create-authentication.command';
import { AUTHENTICATION_REPOSITORY } from '@modules/newauthentication/authentication.di-tokens';
@ -22,21 +22,27 @@ export class CreateAuthenticationService implements ICommandHandler {
) {}
async execute(command: CreateAuthenticationCommand): Promise<AggregateID> {
const authentication = await AuthenticationEntity.create({
password: command.password,
usernames: command.usernames,
});
const authentication: AuthenticationEntity =
await AuthenticationEntity.create({
userId: command.userId,
password: command.password,
usernames: command.usernames,
});
try {
await this.repository.insert(authentication);
return authentication.id;
return authentication.getProps().userId;
} catch (error: any) {
console.log('error', error.cause);
if (error instanceof ConflictException) {
throw new AuthenticationAlreadyExistsException(error);
}
if (
error instanceof UniqueConflictException &&
error instanceof UniqueConstraintException &&
error.message.includes('uuid')
) {
throw new AuthenticationAlreadyExistsException(error);
}
if (
error instanceof UniqueConstraintException &&
error.message.includes('username')
) {
throw new UsernameAlreadyExistsException(error);

View File

@ -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;
}
}

View File

@ -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/newauthentication/authentication.di-tokens';
import { AuthenticationRepositoryPort } from '../ports/authentication.repository.port';
import { AuthenticationEntity } from '@modules/newauthentication/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 deleted: boolean = await this.authenticationRepository.delete(
authentication,
);
return deleted;
}
}

View File

@ -1,11 +1,11 @@
import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library';
import { v4 } from 'uuid';
import * as bcrypt from 'bcrypt';
import {
AuthenticationProps,
CreateAuthenticationProps,
} from './authentication.types';
import { AuthenticationCreatedDomainEvent } from './events/authentication-created.domain-events';
import { AuthenticationDeletedDomainEvent } from './events/authentication-deleted.domain-event';
export class AuthenticationEntity extends AggregateRoot<AuthenticationProps> {
protected readonly _id: AggregateID;
@ -13,22 +13,30 @@ export class AuthenticationEntity extends AggregateRoot<AuthenticationProps> {
static create = async (
create: CreateAuthenticationProps,
): Promise<AuthenticationEntity> => {
const id = v4();
const props: AuthenticationProps = { ...create };
const hash = await bcrypt.hash(props.password, 10);
const authentication = new AuthenticationEntity({
id,
id: props.userId,
props: {
userId: props.userId,
password: hash,
usernames: props.usernames,
},
});
authentication.addEvent(
new AuthenticationCreatedDomainEvent({ aggregateId: id }),
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
}

View File

@ -2,12 +2,14 @@ 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[];
}

View File

@ -0,0 +1,7 @@
import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library';
export class AuthenticationDeletedDomainEvent extends DomainEvent {
constructor(props: DomainEventProps<AuthenticationDeletedDomainEvent>) {
super(props);
}
}

View File

@ -1,9 +0,0 @@
import { Entity } from '@mobicoop/ddd-library';
import { UsernameProps } from './username.types';
export class UsernameEntity extends Entity<UsernameProps> {
protected _id: string;
validate(): void {
throw new Error('Method not implemented.');
}
}

View File

@ -4,12 +4,6 @@ export interface UsernameProps {
type: Type;
}
// Properties that are needed for a Username creation
export interface CreateUsernameProps {
name: string;
type: Type;
}
export enum Type {
EMAIL = 'EMAIL',
PHONE = 'PHONE',

View File

@ -1,10 +1,15 @@
import { Injectable, Logger } from '@nestjs/common';
import { Inject, Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { PrismaRepositoryBase } from '@mobicoop/ddd-library';
import {
LoggerBase,
MessagePublisherPort,
PrismaRepositoryBase,
} from '@mobicoop/ddd-library';
import { AuthenticationEntity } from '../core/domain/authentication.entity';
import { AuthenticationRepositoryPort } from '../core/application/commands/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;
@ -44,13 +49,20 @@ export class AuthenticationRepository
prisma: PrismaService,
mapper: AuthenticationMapper,
eventEmitter: EventEmitter2,
@Inject(MESSAGE_PUBLISHER)
protected readonly messagePublisher: MessagePublisherPort,
) {
super(
prisma.auth,
prisma,
mapper,
eventEmitter,
new Logger(AuthenticationRepository.name),
new LoggerBase(
AuthenticationRepository.name,
'logging',
'auth',
messagePublisher,
),
);
}
}

View File

@ -9,7 +9,7 @@ service AuthenticationService {
rpc UpdatePassword(Password) returns (Id);
rpc UpdateUsername(Username) returns (Id);
rpc DeleteUsername(Username) returns (Id);
rpc Delete(Id) returns (Empty);
rpc Delete(UserId) returns (Empty);
}
message AuthenticationByUsernamePassword {
@ -18,17 +18,18 @@ message AuthenticationByUsernamePassword {
}
message Authentication {
repeated Username usernames = 1;
string password = 2;
string userId = 1;
repeated Username usernames = 2;
string password = 3;
}
message Password {
string id = 1;
string userId = 1;
string password = 2;
}
message Username {
string id = 1;
string userId = 1;
string name = 2;
string type = 3;
}
@ -37,4 +38,8 @@ message Id {
string id = 1;
}
message UserId {
string userId = 1;
}
message Empty {}

View File

@ -22,7 +22,7 @@ export class CreateAuthenticationGrpcController {
constructor(private readonly commandBus: CommandBus) {}
@GrpcMethod('AuthenticationService', 'Create')
async validate(data: CreateAuthenticationRequestDto): Promise<IdResponse> {
async create(data: CreateAuthenticationRequestDto): Promise<IdResponse> {
try {
const aggregateID: AggregateID = await this.commandBus.execute(
new CreateAuthenticationCommand(data),

View File

@ -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/newauthentication/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,
});
}
}
}

View File

@ -9,6 +9,10 @@ import {
import { UsernameDto } from './username.dto';
export class CreateAuthenticationRequestDto {
@IsString()
@IsNotEmpty()
userId: string;
@Type(() => UsernameDto)
@IsArray()
@ArrayMinSize(1)

View File

@ -0,0 +1,7 @@
import { IsNotEmpty, IsString } from 'class-validator';
export class DeleteAuthenticationRequestDto {
@IsString()
@IsNotEmpty()
userId: string;
}