Merge branch 'dddhexagon' into 'main'

Massive upgrade to follow DDD principles

See merge request v3/service/auth!41
This commit is contained in:
Sylvain Briat 2023-07-13 08:22:49 +00:00
commit d099a4e42a
190 changed files with 5796 additions and 11136 deletions

View File

@ -6,10 +6,10 @@ HEALTH_SERVICE_PORT=6002
# PRISMA # PRISMA
DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=auth" DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=auth"
# RABBIT MQ # MESSAGE BROKER
RMQ_URI=amqp://v3-broker:5672 MESSAGE_BROKER_URI=amqp://v3-broker:5672
RMQ_EXCHANGE=mobicoop MESSAGE_BROKER_EXCHANGE=mobicoop
# OPA # 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/ OPA_URL=http://v3-auth-opa:8181/v1/data/

View File

@ -5,6 +5,10 @@ SERVICE_PORT=5002
# PRISMA # PRISMA
DATABASE_URL="postgresql://mobicoop:mobicoop@localhost:5432/mobicoop-test?schema=auth" DATABASE_URL="postgresql://mobicoop:mobicoop@localhost:5432/mobicoop-test?schema=auth"
# MESSAGE BROKER
MESSAGE_BROKER_URI=amqp://v3-broker:5672
MESSAGE_BROKER_EXCHANGE=mobicoop
# OPA # 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/ OPA_URL=http://v3-auth-opa:8181/v1/data/

View File

@ -59,23 +59,27 @@ Note that all usernames are unique in the system : many users can't have the sam
For AuthN, the app exposes the following [gRPC](https://grpc.io/) services : For AuthN, the app exposes the following [gRPC](https://grpc.io/) services :
- **Create** : create an auth with one username / password (you can't create multiple usernames at once) - **Create** : create an auth with usernames and a password
```json ```json
{ {
"uuid": "30f49838-3f24-42bb-a489-8ffb480173ae", "userId": "30f49838-3f24-42bb-a489-8ffb480173ae",
"username": "john.doe@email.com", "usernames": [
"password": "John123", {
"name": "john.doe@email.com",
"type": "EMAIL" "type": "EMAIL"
} }
],
"password": "John123"
}
``` ```
- **AddUsername** : add a username to an auth - **AddUsername** : add a username to an auth
```json ```json
{ {
"uuid": "30f49838-3f24-42bb-a489-8ffb480173ae", "userId": "30f49838-3f24-42bb-a489-8ffb480173ae",
"username": "+33611223344", "name": "+33611223344",
"type": "PHONE" "type": "PHONE"
} }
``` ```
@ -84,8 +88,8 @@ For AuthN, the app exposes the following [gRPC](https://grpc.io/) services :
```json ```json
{ {
"uuid": "30f49838-3f24-42bb-a489-8ffb480173ae", "userId": "30f49838-3f24-42bb-a489-8ffb480173ae",
"username": "johnny.doe@email.com", "name": "johnny.doe@email.com",
"type": "EMAIL" "type": "EMAIL"
} }
``` ```
@ -94,7 +98,7 @@ For AuthN, the app exposes the following [gRPC](https://grpc.io/) services :
```json ```json
{ {
"username": "+33611223344" "name": "+33611223344"
} }
``` ```
@ -102,16 +106,16 @@ For AuthN, the app exposes the following [gRPC](https://grpc.io/) services :
```json ```json
{ {
"uuid": "30f49838-3f24-42bb-a489-8ffb480173ae", "userId": "30f49838-3f24-42bb-a489-8ffb480173ae",
"password": "Johnny123" "password": "Johnny123"
} }
``` ```
- **Validate** : validate an auth (= authentication with username/password) - **Validate** : validate an auth (= authentication with name/password)
```json ```json
{ {
"username": "john.doe@email.com", "name": "john.doe@email.com",
"password": "Johnny123" "password": "Johnny123"
} }
``` ```
@ -120,7 +124,7 @@ For AuthN, the app exposes the following [gRPC](https://grpc.io/) services :
```json ```json
{ {
"uuid": "30f49838-3f24-42bb-a489-8ffb480173ae" "userId": "30f49838-3f24-42bb-a489-8ffb480173ae"
} }
``` ```
@ -134,7 +138,7 @@ For AuthZ, the app exposes the following [gRPC](https://grpc.io/) services :
```json ```json
{ {
"uuid": "96d99d44-e0a6-458e-a656-de2a400d60a9", "userId": "96d99d44-e0a6-458e-a656-de2a400d60a9",
"domain": "USER", "domain": "USER",
"action": "READ", "action": "READ",
"context": [ "context": [

View File

@ -3,7 +3,7 @@ package USER.DELETE
default allow := false default allow := false
allow { allow {
input.uuid == input.owner input.id == input.owner
} }
allow { allow {

View File

@ -3,7 +3,7 @@ package USER.READ
default allow := false default allow := false
allow { allow {
input.uuid == input.owner input.id == input.owner
} }
allow { allow {

View File

@ -3,7 +3,7 @@ package USER.UPDATE
default allow := false default allow := false
allow { allow {
input.uuid == input.owner input.id == input.owner
} }
allow { allow {

8822
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "@mobicoop/auth", "name": "@mobicoop/auth",
"version": "0.0.1", "version": "0.1.0",
"description": "Mobicoop V3 Auth Service", "description": "Mobicoop V3 Auth Service",
"author": "sbriat", "author": "sbriat",
"private": true, "private": true,
@ -17,11 +17,11 @@
"lint:check": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix-dry-run --ignore-path .gitignore", "lint:check": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix-dry-run --ignore-path .gitignore",
"pretty:check": "./node_modules/.bin/prettier --check .", "pretty:check": "./node_modules/.bin/prettier --check .",
"pretty": "./node_modules/.bin/prettier --write .", "pretty": "./node_modules/.bin/prettier --write .",
"test": "npm run migrate:test && dotenv -e .env.test jest", "test": "npm run test:unit && npm run test:integration",
"test:unit": "jest --testPathPattern 'tests/unit/' --verbose", "test:unit": "jest --testPathPattern 'tests/unit/' --verbose",
"test:unit:ci": "jest --testPathPattern 'tests/unit/' --coverage", "test:unit:ci": "jest --testPathPattern 'tests/unit/' --coverage",
"test:integration": "npm run migrate:test && dotenv -e .env.test -- jest --testPathPattern 'tests/integration/' --verbose", "test:integration": "npm run migrate:test && dotenv -e .env.test -- jest --testPathPattern 'tests/integration/' --verbose --runInBand",
"test:integration:ci": "npm run migrate:test:ci && dotenv -e ci/.env.ci -- jest --testPathPattern 'tests/integration/'", "test:integration:ci": "npm run migrate:test:ci && dotenv -e ci/.env.ci -- jest --testPathPattern 'tests/integration/' --runInBand",
"test:cov": "jest --testPathPattern 'tests/unit/' --coverage", "test:cov": "jest --testPathPattern 'tests/unit/' --coverage",
"test:e2e": "jest --config ./test/jest-e2e.json", "test:e2e": "jest --config ./test/jest-e2e.json",
"migrate": "docker exec v3-auth-api sh -c 'npx prisma migrate dev'", "migrate": "docker exec v3-auth-api sh -c 'npx prisma migrate dev'",
@ -36,11 +36,14 @@
"@golevelup/nestjs-rabbitmq": "^3.4.0", "@golevelup/nestjs-rabbitmq": "^3.4.0",
"@grpc/grpc-js": "^1.8.0", "@grpc/grpc-js": "^1.8.0",
"@grpc/proto-loader": "^0.7.4", "@grpc/proto-loader": "^0.7.4",
"@mobicoop/ddd-library": "^0.3.0",
"@mobicoop/message-broker-module": "^1.2.0",
"@nestjs/axios": "^1.0.1", "@nestjs/axios": "^1.0.1",
"@nestjs/common": "^9.0.0", "@nestjs/common": "^9.0.0",
"@nestjs/config": "^2.2.0", "@nestjs/config": "^2.2.0",
"@nestjs/core": "^9.0.0", "@nestjs/core": "^9.0.0",
"@nestjs/cqrs": "^9.0.1", "@nestjs/cqrs": "^9.0.1",
"@nestjs/event-emitter": "^2.0.0",
"@nestjs/microservices": "^9.2.1", "@nestjs/microservices": "^9.2.1",
"@nestjs/platform-express": "^9.0.0", "@nestjs/platform-express": "^9.0.0",
"@nestjs/terminus": "^9.2.2", "@nestjs/terminus": "^9.2.2",
@ -88,12 +91,12 @@
"ts" "ts"
], ],
"modulePathIgnorePatterns": [ "modulePathIgnorePatterns": [
".controller.ts",
".module.ts", ".module.ts",
".request.ts", ".dto.ts",
".presenter.ts", ".di-tokens.ts",
".profile.ts", ".response.ts",
".exception.ts", ".port.ts",
"prisma.service.ts",
"main.ts" "main.ts"
], ],
"rootDir": "src", "rootDir": "src",
@ -105,15 +108,19 @@
"**/*.(t|j)s" "**/*.(t|j)s"
], ],
"coveragePathIgnorePatterns": [ "coveragePathIgnorePatterns": [
".controller.ts",
".module.ts", ".module.ts",
".request.ts", ".dto.ts",
".presenter.ts", ".di-tokens.ts",
".profile.ts", ".response.ts",
".exception.ts", ".port.ts",
"prisma.service.ts",
"main.ts" "main.ts"
], ],
"coverageDirectory": "../coverage", "coverageDirectory": "../coverage",
"moduleNameMapper": {
"^@modules(.*)": "<rootDir>/modules/$1",
"^@src(.*)": "<rootDir>$1"
},
"testEnvironment": "node" "testEnvironment": "node"
} }
} }

View File

@ -14,7 +14,7 @@ CREATE TABLE "auth" (
-- CreateTable -- CreateTable
CREATE TABLE "username" ( CREATE TABLE "username" (
"username" TEXT NOT NULL, "username" TEXT NOT NULL,
"uuid" UUID NOT NULL, "authUuid" UUID NOT NULL,
"type" "Type" NOT NULL DEFAULT 'EMAIL', "type" "Type" NOT NULL DEFAULT 'EMAIL',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL, "updatedAt" TIMESTAMP(3) NOT NULL,
@ -23,4 +23,7 @@ CREATE TABLE "username" (
); );
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "username_uuid_type_key" ON "username"("uuid", "type"); CREATE UNIQUE INDEX "username_authUuid_type_key" ON "username"("authUuid", "type");
-- AddForeignKey
ALTER TABLE "username" ADD CONSTRAINT "username_authUuid_fkey" FOREIGN KEY ("authUuid") REFERENCES "auth"("uuid") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -3,6 +3,7 @@
generator client { generator client {
provider = "prisma-client-js" provider = "prisma-client-js"
binaryTargets = ["linux-musl", "debian-openssl-3.0.x"]
} }
datasource db { datasource db {
@ -11,22 +12,24 @@ datasource db {
} }
model Auth { model Auth {
uuid String @id @db.Uuid uuid String @id @default(uuid()) @db.Uuid
password String password String
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
usernames Username[]
@@map("auth") @@map("auth")
} }
model Username { model Username {
username String @id username String @id
uuid String @db.Uuid authUuid String @db.Uuid
type Type @default(EMAIL) // type is needed in case of username update type Type @default(EMAIL) // type is needed in case of username update
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
Auth Auth @relation(fields: [authUuid], references: [uuid], onDelete: Cascade)
@@unique([uuid, type]) @@unique([authUuid, type])
@@map("username") @@map("username")
} }

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

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

View File

@ -1,15 +1,39 @@
import { classes } from '@automapper/classes';
import { AutomapperModule } from '@automapper/nestjs';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config'; import { ConfigModule, ConfigService } from '@nestjs/config';
import { AuthenticationModule } from './modules/authentication/authentication.module'; import { AuthenticationModule } from '@modules/authentication/authentication.module';
import { AuthorizationModule } from './modules/authorization/authorization.module'; import { EventEmitterModule } from '@nestjs/event-emitter';
import { HealthModule } from './modules/health/health.module'; import {
MessageBrokerModule,
MessageBrokerModuleOptions,
} from '@mobicoop/message-broker-module';
import { HealthModule } from '@modules/health/health.module';
import { AuthorizationModule } from '@modules/authorization/authorization.module';
@Module({ @Module({
imports: [ imports: [
ConfigModule.forRoot({ isGlobal: true }), ConfigModule.forRoot({ isGlobal: true }),
AutomapperModule.forRoot({ strategyInitializer: classes() }), EventEmitterModule.forRoot(),
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',
handlers: {
userUpdated: {
routingKey: 'user.updated',
queue: 'auth-user-updated',
},
userDeleted: {
routingKey: 'user.deleted',
queue: 'auth-user-deleted',
},
},
}),
}),
AuthenticationModule, AuthenticationModule,
AuthorizationModule, AuthorizationModule,
HealthModule, HealthModule,

View File

@ -15,13 +15,16 @@ async function bootstrap() {
protoPath: [ protoPath: [
join( join(
__dirname, __dirname,
'modules/authentication/adapters/primaries/authentication.proto', 'modules/authentication/interface/grpc-controllers/authentication.proto',
), ),
join( join(
__dirname, __dirname,
'modules/authorization/adapters/primaries/authorization.proto', 'modules/authorization/interface/grpc-controllers/authorization.proto',
),
join(
__dirname,
'modules/health/interface/grpc-controllers/health.proto',
), ),
join(__dirname, 'modules/health/adapters/primaries/health.proto'),
], ],
url: process.env.SERVICE_URL + ':' + process.env.SERVICE_PORT, url: process.env.SERVICE_URL + ':' + process.env.SERVICE_PORT,
loader: { keepCase: true, enums: String }, loader: { keepCase: true, enums: String },

View File

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

View File

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

View File

@ -1,6 +0,0 @@
import { AutoMap } from '@automapper/classes';
export class AuthenticationPresenter {
@AutoMap()
uuid: string;
}

View File

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

View File

@ -1,9 +0,0 @@
import { AutoMap } from '@automapper/classes';
export class UsernamePresenter {
@AutoMap()
uuid: string;
@AutoMap()
username: string;
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,2 @@
export const AUTHENTICATION_REPOSITORY = Symbol('AUTHENTICATION_REPOSITORY');
export const USERNAME_REPOSITORY = Symbol('USERNAME_REPOSITORY');

View 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: copy.usernames
? {
create: copy.usernames.map((username: UsernameProps) => ({
username: username.name,
type: username.type,
})),
}
: undefined,
};
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;
};
}

View File

@ -1,66 +1,96 @@
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 { CqrsModule } from '@nestjs/cqrs';
import { DatabaseModule } from '../database/database.module'; import { DeleteAuthenticationGrpcController } from './interface/grpc-controllers/delete-authentication.grpc.controller';
import { AuthenticationController } from './adapters/primaries/authentication.controller'; import { DeleteAuthenticationService } from './core/application/commands/delete-authentication/delete-authentication.service';
import { CreateAuthenticationUseCase } from './domain/usecases/create-authentication.usecase'; import { MESSAGE_PUBLISHER } from '@src/app.di-tokens';
import { ValidateAuthenticationUseCase } from './domain/usecases/validate-authentication.usecase'; import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
import { AuthenticationProfile } from './mappers/authentication.profile'; import { UsernameRepository } from './infrastructure/username.repository';
import { AuthenticationRepository } from './adapters/secondaries/authentication.repository'; import { UsernameMapper } from './username.mapper';
import { UpdateUsernameUseCase } from './domain/usecases/update-username.usecase'; import { AddUsernameGrpcController } from './interface/grpc-controllers/add-username.grpc.controller';
import { UsernameProfile } from './mappers/username.profile'; import { AddUsernameService } from './core/application/commands/add-username/add-username.service';
import { AddUsernameUseCase } from './domain/usecases/add-username.usecase'; import { DeleteUsernameGrpcController } from './interface/grpc-controllers/delete-username.grpc.controller';
import { UpdatePasswordUseCase } from './domain/usecases/update-password.usecase'; import { DeleteUsernameService } from './core/application/commands/delete-username/delete-username.service';
import { DeleteUsernameUseCase } from './domain/usecases/delete-username.usecase'; import { UpdateUsernameGrpcController } from './interface/grpc-controllers/update-username.grpc.controller';
import { DeleteAuthenticationUseCase } from './domain/usecases/delete-authentication.usecase'; import { UpdateUsernameService } from './core/application/commands/update-username/update-username.service';
import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq'; import { UpdatePasswordGrpcController } from './interface/grpc-controllers/update-password.grpc.controller';
import { ConfigModule, ConfigService } from '@nestjs/config'; import { UpdatePasswordService } from './core/application/commands/update-password/update-password.service';
import { AuthenticationMessagerController } from './adapters/primaries/authentication-messager.controller'; import { ValidateAuthenticationGrpcController } from './interface/grpc-controllers/validate-authentication.grpc.controller';
import { Messager } from './adapters/secondaries/messager'; import { ValidateAuthenticationQueryHandler } from './core/application/queries/validate-authentication/validate-authentication.query-handler';
import { UserUpdatedMessageHandler } from './interface/message-handlers/user-updated.message-handler';
import { UserDeletedMessageHandler } from './interface/message-handlers/user-deleted.message-handler';
const grpcControllers = [
CreateAuthenticationGrpcController,
DeleteAuthenticationGrpcController,
AddUsernameGrpcController,
UpdateUsernameGrpcController,
DeleteUsernameGrpcController,
UpdatePasswordGrpcController,
ValidateAuthenticationGrpcController,
];
const messageHandlers = [UserUpdatedMessageHandler, UserDeletedMessageHandler];
const commandHandlers: Provider[] = [
CreateAuthenticationService,
DeleteAuthenticationService,
AddUsernameService,
UpdateUsernameService,
DeleteUsernameService,
UpdatePasswordService,
];
const queryHandlers: Provider[] = [ValidateAuthenticationQueryHandler];
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({ @Module({
imports: [ imports: [CqrsModule],
DatabaseModule, controllers: [...grpcControllers],
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],
providers: [ providers: [
AuthenticationProfile, ...messageHandlers,
UsernameProfile, ...commandHandlers,
AuthenticationRepository, ...queryHandlers,
Messager, ...mappers,
ValidateAuthenticationUseCase, ...repositories,
CreateAuthenticationUseCase, ...messageBrokers,
AddUsernameUseCase, ...orms,
UpdateUsernameUseCase, ],
UpdatePasswordUseCase, exports: [
DeleteUsernameUseCase, PrismaService,
DeleteAuthenticationUseCase, AuthenticationMapper,
UsernameMapper,
AUTHENTICATION_REPOSITORY,
USERNAME_REPOSITORY,
], ],
exports: [],
}) })
export class AuthenticationModule {} export class AuthenticationModule {}

View File

@ -1,9 +0,0 @@
import { AddUsernameRequest } from '../domain/dtos/add-username.request';
export class AddUsernameCommand {
readonly addUsernameRequest: AddUsernameRequest;
constructor(request: AddUsernameRequest) {
this.addUsernameRequest = request;
}
}

View File

@ -1,9 +0,0 @@
import { CreateAuthenticationRequest } from '../domain/dtos/create-authentication.request';
export class CreateAuthenticationCommand {
readonly createAuthenticationRequest: CreateAuthenticationRequest;
constructor(request: CreateAuthenticationRequest) {
this.createAuthenticationRequest = request;
}
}

View File

@ -1,9 +0,0 @@
import { DeleteAuthenticationRequest } from '../domain/dtos/delete-authentication.request';
export class DeleteAuthenticationCommand {
readonly deleteAuthenticationRequest: DeleteAuthenticationRequest;
constructor(request: DeleteAuthenticationRequest) {
this.deleteAuthenticationRequest = request;
}
}

View File

@ -1,9 +0,0 @@
import { DeleteUsernameRequest } from '../domain/dtos/delete-username.request';
export class DeleteUsernameCommand {
readonly deleteUsernameRequest: DeleteUsernameRequest;
constructor(request: DeleteUsernameRequest) {
this.deleteUsernameRequest = request;
}
}

View File

@ -1,9 +0,0 @@
import { UpdatePasswordRequest } from '../domain/dtos/update-password.request';
export class UpdatePasswordCommand {
readonly updatePasswordRequest: UpdatePasswordRequest;
constructor(request: UpdatePasswordRequest) {
this.updatePasswordRequest = request;
}
}

View File

@ -1,9 +0,0 @@
import { UpdateUsernameRequest } from '../domain/dtos/update-username.request';
export class UpdateUsernameCommand {
readonly updateUsernameRequest: UpdateUsernameRequest;
constructor(request: UpdateUsernameRequest) {
this.updateUsernameRequest = request;
}
}

View File

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

View File

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

View File

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

View File

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

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

View File

@ -0,0 +1,10 @@
import { Command, CommandProps } from '@mobicoop/ddd-library';
export class DeleteUsernameCommand extends Command {
readonly name: string;
constructor(props: CommandProps<DeleteUsernameCommand>) {
super(props);
this.name = props.name;
}
}

View File

@ -0,0 +1,32 @@
import { Inject, UnauthorizedException } from '@nestjs/common';
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { DeleteUsernameCommand } from './delete-username.command';
import { USERNAME_REPOSITORY } from '@modules/authentication/authentication.di-tokens';
import { UsernameRepositoryPort } from '../../ports/username.repository.port';
import { UsernameEntity } from '@modules/authentication/core/domain/username.entity';
@CommandHandler(DeleteUsernameCommand)
export class DeleteUsernameService implements ICommandHandler {
constructor(
@Inject(USERNAME_REPOSITORY)
private readonly usernameRepository: UsernameRepositoryPort,
) {}
async execute(command: DeleteUsernameCommand): Promise<boolean> {
const username: UsernameEntity = await this.usernameRepository.findByName(
command.name,
);
const usernamesCount: number = await this.usernameRepository.countUsernames(
username.getProps().userId,
);
if (usernamesCount <= 1)
throw new UnauthorizedException(
'Authentication must have at least one username',
);
username.delete();
const isDeleted: boolean = await this.usernameRepository.deleteUsername(
username,
);
return isDeleted;
}
}

View File

@ -0,0 +1,12 @@
import { Command, CommandProps } from '@mobicoop/ddd-library';
export class UpdatePasswordCommand extends Command {
readonly userId: string;
readonly password: string;
constructor(props: CommandProps<UpdatePasswordCommand>) {
super(props);
this.userId = props.userId;
this.password = props.password;
}
}

View File

@ -0,0 +1,23 @@
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { Inject } from '@nestjs/common';
import { AggregateID } from '@mobicoop/ddd-library';
import { AUTHENTICATION_REPOSITORY } from '@modules/authentication/authentication.di-tokens';
import { UpdatePasswordCommand } from './update-password.command';
import { AuthenticationRepositoryPort } from '../../ports/authentication.repository.port';
import { AuthenticationEntity } from '@modules/authentication/core/domain/authentication.entity';
@CommandHandler(UpdatePasswordCommand)
export class UpdatePasswordService implements ICommandHandler {
constructor(
@Inject(AUTHENTICATION_REPOSITORY)
private readonly authenticationRepository: AuthenticationRepositoryPort,
) {}
async execute(command: UpdatePasswordCommand): Promise<AggregateID> {
const authentication: AuthenticationEntity =
await this.authenticationRepository.findOneById(command.userId);
await authentication.updatePassword(command.password);
await this.authenticationRepository.update(command.userId, authentication);
return authentication.id;
}
}

View File

@ -0,0 +1,13 @@
import { Command, CommandProps } from '@mobicoop/ddd-library';
import { Username } from '../../types/username';
export class UpdateUsernameCommand extends Command {
readonly userId: string;
readonly username: Username;
constructor(props: CommandProps<UpdateUsernameCommand>) {
super(props);
this.userId = props.userId;
this.username = props.username;
}
}

View File

@ -0,0 +1,46 @@
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { Inject } from '@nestjs/common';
import {
AggregateID,
ConflictException,
UniqueConstraintException,
} from '@mobicoop/ddd-library';
import { USERNAME_REPOSITORY } from '@modules/authentication/authentication.di-tokens';
import { UsernameAlreadyExistsException } from '@modules/authentication/core/domain/authentication.errors';
import { UpdateUsernameCommand } from './update-username.command';
import { UsernameRepositoryPort } from '../../ports/username.repository.port';
import { UsernameEntity } from '@modules/authentication/core/domain/username.entity';
@CommandHandler(UpdateUsernameCommand)
export class UpdateUsernameService implements ICommandHandler {
constructor(
@Inject(USERNAME_REPOSITORY)
private readonly usernameRepository: UsernameRepositoryPort,
) {}
async execute(command: UpdateUsernameCommand): Promise<AggregateID> {
try {
const username: UsernameEntity = await this.usernameRepository.findByType(
command.userId,
command.username.type,
);
const oldName: string = username.id;
username.update({
name: command.username.name,
});
await this.usernameRepository.updateUsername(oldName, username);
return username.getProps().name;
} 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;
}
}
}

View File

@ -0,0 +1,4 @@
import { RepositoryPort } from '@mobicoop/ddd-library';
import { AuthenticationEntity } from '@modules/authentication/core/domain/authentication.entity';
export type AuthenticationRepositoryPort = RepositoryPort<AuthenticationEntity>;

View File

@ -0,0 +1,11 @@
import { RepositoryPort } from '@mobicoop/ddd-library';
import { UsernameEntity } from '../../domain/username.entity';
import { Type } from '../../domain/username.types';
export type UsernameRepositoryPort = RepositoryPort<UsernameEntity> & {
findByType(userId: string, type: Type): Promise<UsernameEntity>;
findByName(name: string): Promise<UsernameEntity>;
updateUsername(oldName: string, entity: UsernameEntity): Promise<void>;
deleteUsername(entity: UsernameEntity): Promise<boolean>;
countUsernames(userId: string): Promise<number>;
};

View File

@ -0,0 +1,50 @@
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
import { Inject, UnauthorizedException } from '@nestjs/common';
import { ValidateAuthenticationQuery } from './validate-authentication.query';
import {
AUTHENTICATION_REPOSITORY,
USERNAME_REPOSITORY,
} from '@modules/authentication/authentication.di-tokens';
import { AuthenticationRepositoryPort } from '../../ports/authentication.repository.port';
import { UsernameRepositoryPort } from '../../ports/username.repository.port';
import { AuthenticationEntity } from '@modules/authentication/core/domain/authentication.entity';
import { UsernameEntity } from '@modules/authentication/core/domain/username.entity';
import { AggregateID, NotFoundException } from '@mobicoop/ddd-library';
@QueryHandler(ValidateAuthenticationQuery)
export class ValidateAuthenticationQueryHandler implements IQueryHandler {
constructor(
@Inject(AUTHENTICATION_REPOSITORY)
private readonly authenticationRepository: AuthenticationRepositoryPort,
@Inject(USERNAME_REPOSITORY)
private readonly usernameRepository: UsernameRepositoryPort,
) {}
execute = async (
query: ValidateAuthenticationQuery,
): Promise<AggregateID> => {
let usernameEntity: UsernameEntity;
try {
usernameEntity = await this.usernameRepository.findByName(query.name);
} catch (e) {
throw new NotFoundException();
}
let authenticationEntity: AuthenticationEntity;
try {
authenticationEntity = await this.authenticationRepository.findOneById(
usernameEntity.getProps().userId,
);
} catch (e) {
throw new NotFoundException();
}
try {
const isAuthenticated = await authenticationEntity.authenticate(
query.password,
);
if (isAuthenticated) return authenticationEntity.id;
throw new UnauthorizedException();
} catch (e) {
throw new UnauthorizedException();
}
};
}

View File

@ -0,0 +1,12 @@
import { QueryBase } from '@mobicoop/ddd-library';
export class ValidateAuthenticationQuery extends QueryBase {
readonly name: string;
readonly password: string;
constructor(name: string, password: string) {
super();
this.name = name;
this.password = password;
}
}

View File

@ -0,0 +1,6 @@
import { Type } from '@modules/authentication/core/domain/username.types';
export type Username = {
name: string;
type: Type;
};

View File

@ -0,0 +1,59 @@
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';
import { PasswordUpdatedDomainEvent } from './events/password-updated.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 AuthenticationEntity.encryptPassword(props.password);
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;
};
updatePassword = async (password: string): Promise<void> => {
this.props.password = await AuthenticationEntity.encryptPassword(password);
this.addEvent(
new PasswordUpdatedDomainEvent({
aggregateId: this.id,
}),
);
};
delete(): void {
this.addEvent(
new AuthenticationDeletedDomainEvent({
aggregateId: this.id,
}),
);
}
authenticate = async (password: string): Promise<boolean> =>
await bcrypt.compare(password, this.props.password);
validate(): void {
// entity business rules validation to protect it's invariant before saving entity to a database
}
private static encryptPassword = async (password: string): Promise<string> =>
await bcrypt.hash(password, 10);
}

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,52 @@
import { AggregateID, AggregateRoot } from '@mobicoop/ddd-library';
import {
CreateUsernameProps,
UpdateUsernameProps,
UsernameProps,
} from './username.types';
import { UsernameAddedDomainEvent } from './events/username-added.domain-event';
import { UsernameDeletedDomainEvent } from './events/username-deleted.domain-event';
import { UsernameUpdatedDomainEvent } from './events/username-updated.domain-event';
export class UsernameEntity extends AggregateRoot<UsernameProps> {
protected readonly _id: AggregateID;
static create = async (
create: CreateUsernameProps,
): 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;
};
update(props: UpdateUsernameProps): void {
this.props.name = props.name;
this.addEvent(
new UsernameUpdatedDomainEvent({
aggregateId: props.name,
}),
);
}
delete(): void {
this.addEvent(
new UsernameDeletedDomainEvent({
aggregateId: this.id,
}),
);
}
validate(): void {
// entity business rules validation to protect it's invariant before saving entity to a database
}
}

View File

@ -0,0 +1,20 @@
export interface UsernameProps {
name: string;
userId?: string;
type: Type;
}
export interface CreateUsernameProps {
name: string;
userId: string;
type: Type;
}
export interface UpdateUsernameProps {
name: string;
}
export enum Type {
EMAIL = 'EMAIL',
PHONE = 'PHONE',
}

View File

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

View File

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

View File

@ -1,9 +0,0 @@
import { AutoMap } from '@automapper/classes';
import { IsNotEmpty, IsString } from 'class-validator';
export class DeleteAuthenticationRequest {
@IsString()
@IsNotEmpty()
@AutoMap()
uuid: string;
}

View File

@ -1,9 +0,0 @@
import { AutoMap } from '@automapper/classes';
import { IsNotEmpty, IsString } from 'class-validator';
export class DeleteUsernameRequest {
@IsString()
@IsNotEmpty()
@AutoMap()
username: string;
}

View File

@ -1,4 +0,0 @@
export enum Type {
EMAIL = 'EMAIL',
PHONE = 'PHONE',
}

View File

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

View File

@ -1,8 +0,0 @@
import { AutoMap } from '@automapper/classes';
export class Authentication {
@AutoMap()
uuid: string;
password: string;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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';
type AuthenticationBaseModel = {
uuid: string;
password: string;
};
export type AuthenticationReadModel = AuthenticationBaseModel & {
usernames: UsernameModel[];
createdAt: Date;
updatedAt: Date;
};
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,
}),
);
}
}

View File

@ -0,0 +1,83 @@
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';
import { Type } from '../core/domain/username.types';
type UsernameBaseModel = {
username: string;
authUuid: string;
type: string;
};
export type UsernameReadModel = UsernameBaseModel & {
createdAt?: Date;
updatedAt?: Date;
};
export type UsernameWriteModel = UsernameBaseModel;
/**
* Repository is used for retrieving/saving domain entities
* */
@Injectable()
export class UsernameRepository
extends PrismaRepositoryBase<
UsernameEntity,
UsernameReadModel,
UsernameWriteModel
>
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.username',
messagePublisher,
}),
);
}
findByType = async (userId: string, type: Type): Promise<UsernameEntity> =>
this.findOne({
authUuid: userId,
type,
});
findByName = async (name: string): Promise<UsernameEntity> =>
this.findOne({
username: name,
});
updateUsername = async (
oldName: string,
entity: UsernameEntity,
): Promise<void> => this.update(oldName, entity, 'username');
deleteUsername = async (entity: UsernameEntity): Promise<boolean> =>
this.delete(entity, 'username');
countUsernames = async (userId: string): Promise<number> =>
this.count({
authUuid: userId,
});
}

View File

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

View File

@ -0,0 +1,3 @@
import { ResponseBase } from '@mobicoop/ddd-library';
export class AuthenticationResponseDto extends ResponseBase {}

View File

@ -0,0 +1,3 @@
import { ResponseBase } from '@mobicoop/ddd-library';
export class UsernameResponseDto extends ResponseBase {}

View File

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

View File

@ -0,0 +1,45 @@
syntax = "proto3";
package authentication;
service AuthenticationService {
rpc Validate(AuthenticationByNamePassword) 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 AuthenticationByNamePassword {
string name = 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 {}

View File

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

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

View File

@ -0,0 +1,49 @@
import {
DatabaseErrorException,
NotFoundException,
RpcExceptionCode,
RpcValidationPipe,
} from '@mobicoop/ddd-library';
import { Controller, UnauthorizedException, UsePipes } from '@nestjs/common';
import { CommandBus } from '@nestjs/cqrs';
import { GrpcMethod, RpcException } from '@nestjs/microservices';
import { DeleteUsernameRequestDto } from './dtos/delete-username.request.dto';
import { DeleteUsernameCommand } from '@modules/authentication/core/application/commands/delete-username/delete-username.command';
@UsePipes(
new RpcValidationPipe({
whitelist: true,
forbidUnknownValues: false,
}),
)
@Controller()
export class DeleteUsernameGrpcController {
constructor(private readonly commandBus: CommandBus) {}
@GrpcMethod('AuthenticationService', 'DeleteUsername')
async delete(data: DeleteUsernameRequestDto): Promise<void> {
try {
await this.commandBus.execute(new DeleteUsernameCommand(data));
} catch (error: any) {
if (error instanceof UnauthorizedException)
throw new RpcException({
code: RpcExceptionCode.PERMISSION_DENIED,
message: error.message,
});
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

@ -0,0 +1,8 @@
import { IsNotEmpty, IsString } from 'class-validator';
import { UsernameDto } from './username.dto';
export class AddUsernameRequestDto extends UsernameDto {
@IsString()
@IsNotEmpty()
userId: string;
}

View File

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

View File

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

View File

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

View File

@ -1,9 +1,9 @@
import { IsNotEmpty, IsString } from 'class-validator'; import { IsNotEmpty, IsString } from 'class-validator';
export class ValidateAuthenticationRequest { export class UpdatePasswordRequestDto {
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
username: string; userId: string;
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()

View File

@ -0,0 +1,8 @@
import { IsNotEmpty, IsString } from 'class-validator';
import { UsernameDto } from './username.dto';
export class UpdateUsernameRequestDto extends UsernameDto {
@IsString()
@IsNotEmpty()
userId: string;
}

View File

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

View File

@ -1,14 +1,11 @@
import { AutoMap } from '@automapper/classes';
import { IsNotEmpty, IsString } from 'class-validator'; import { IsNotEmpty, IsString } from 'class-validator';
export class UpdatePasswordRequest { export class ValidateAuthenticationRequestDto {
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
@AutoMap() name: string;
uuid: string;
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
@AutoMap()
password: string; password: string;
} }

View File

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

View File

@ -0,0 +1,40 @@
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 { UpdatePasswordRequestDto } from './dtos/update-password.request.dto';
import { UpdatePasswordCommand } from '@modules/authentication/core/application/commands/update-password/update-password.command';
@UsePipes(
new RpcValidationPipe({
whitelist: true,
forbidUnknownValues: false,
}),
)
@Controller()
export class UpdatePasswordGrpcController {
constructor(private readonly commandBus: CommandBus) {}
@GrpcMethod('AuthenticationService', 'UpdatePassword')
async updatePassword(data: UpdatePasswordRequestDto): Promise<IdResponse> {
try {
const aggregateID: AggregateID = await this.commandBus.execute(
new UpdatePasswordCommand({
userId: data.userId,
password: data.password,
}),
);
return new IdResponse(aggregateID);
} catch (error: any) {
throw new RpcException({
code: RpcExceptionCode.UNKNOWN,
message: error.message,
});
}
}
}

View File

@ -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 { UpdateUsernameRequestDto } from './dtos/update-username.request.dto';
import { UpdateUsernameCommand } from '@modules/authentication/core/application/commands/update-username/update-username.command';
@UsePipes(
new RpcValidationPipe({
whitelist: true,
forbidUnknownValues: false,
}),
)
@Controller()
export class UpdateUsernameGrpcController {
constructor(private readonly commandBus: CommandBus) {}
@GrpcMethod('AuthenticationService', 'UpdateUsername')
async updateUsername(data: UpdateUsernameRequestDto): Promise<IdResponse> {
try {
const aggregateID: AggregateID = await this.commandBus.execute(
new UpdateUsernameCommand({
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,
});
}
}
}

View File

@ -0,0 +1,37 @@
import {
AggregateID,
IdResponse,
RpcExceptionCode,
RpcValidationPipe,
} from '@mobicoop/ddd-library';
import { Controller, UsePipes } from '@nestjs/common';
import { QueryBus } from '@nestjs/cqrs';
import { GrpcMethod, RpcException } from '@nestjs/microservices';
import { ValidateAuthenticationRequestDto } from './dtos/validate-authentication.request.dto';
import { ValidateAuthenticationQuery } from '@modules/authentication/core/application/queries/validate-authentication/validate-authentication.query';
@UsePipes(
new RpcValidationPipe({
whitelist: true,
forbidUnknownValues: false,
}),
)
@Controller()
export class ValidateAuthenticationGrpcController {
constructor(private readonly queryBus: QueryBus) {}
@GrpcMethod('AuthenticationService', 'Validate')
async validate(data: ValidateAuthenticationRequestDto): Promise<IdResponse> {
try {
const aggregateID: AggregateID = await this.queryBus.execute(
new ValidateAuthenticationQuery(data.name, data.password),
);
return new IdResponse(aggregateID);
} catch (error: any) {
throw new RpcException({
code: RpcExceptionCode.PERMISSION_DENIED,
message: 'Permission denied',
});
}
}
}

View File

@ -0,0 +1,24 @@
import { Injectable } from '@nestjs/common';
import { CommandBus } from '@nestjs/cqrs';
import { RabbitSubscribe } from '@mobicoop/message-broker-module';
import { DeleteAuthenticationCommand } from '@modules/authentication/core/application/commands/delete-authentication/delete-authentication.command';
@Injectable()
export class UserDeletedMessageHandler {
constructor(private readonly commandBus: CommandBus) {}
@RabbitSubscribe({
name: 'userDeleted',
})
public async userDeleted(message: string) {
const deletedUser = JSON.parse(message);
try {
if (!deletedUser.hasOwnProperty('userId')) throw new Error();
await this.commandBus.execute(
new DeleteAuthenticationCommand({
userId: deletedUser.userId,
}),
);
} catch (e: any) {}
}
}

View File

@ -0,0 +1,42 @@
import { Injectable } from '@nestjs/common';
import { CommandBus } from '@nestjs/cqrs';
import { RabbitSubscribe } from '@mobicoop/message-broker-module';
import { Type } from '@modules/authentication/core/domain/username.types';
import { UpdateUsernameCommand } from '@modules/authentication/core/application/commands/update-username/update-username.command';
@Injectable()
export class UserUpdatedMessageHandler {
constructor(private readonly commandBus: CommandBus) {}
@RabbitSubscribe({
name: 'userUpdated',
})
public async userUpdated(message: string) {
const updatedUser = JSON.parse(message);
try {
if (!updatedUser.hasOwnProperty('userId')) throw new Error();
if (updatedUser.hasOwnProperty('email') && updatedUser.email) {
await this.commandBus.execute(
new UpdateUsernameCommand({
userId: updatedUser.userId,
username: {
name: updatedUser.email,
type: Type.EMAIL,
},
}),
);
}
if (updatedUser.hasOwnProperty('phone') && updatedUser.phone) {
await this.commandBus.execute(
new UpdateUsernameCommand({
userId: updatedUser.userId,
username: {
name: updatedUser.phone,
type: Type.PHONE,
},
}),
);
}
} catch (e: any) {}
}
}

View File

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

Some files were not shown because too many files have changed in this diff Show More