Merge branch 'dddhexagon' into 'main'
Massive upgrade to follow DDD principles See merge request v3/service/auth!41
This commit is contained in:
commit
d099a4e42a
|
@ -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/
|
||||||
|
|
|
@ -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/
|
||||||
|
|
34
README.md
34
README.md
|
@ -59,14 +59,18 @@ 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",
|
{
|
||||||
"type": "EMAIL"
|
"name": "john.doe@email.com",
|
||||||
|
"type": "EMAIL"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"password": "John123"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -74,8 +78,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": "+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": [
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
35
package.json
35
package.json
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
@ -2,7 +2,8 @@
|
||||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||||
|
|
||||||
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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
export const MESSAGE_PUBLISHER = Symbol('MESSAGE_PUBLISHER');
|
|
@ -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,
|
||||||
|
|
|
@ -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 },
|
||||||
|
|
|
@ -1,52 +0,0 @@
|
||||||
import { RabbitSubscribe } from '@golevelup/nestjs-rabbitmq';
|
|
||||||
import { Controller } from '@nestjs/common';
|
|
||||||
import { CommandBus } from '@nestjs/cqrs';
|
|
||||||
import { UpdateUsernameCommand } from '../../commands/update-username.command';
|
|
||||||
import { Type } from '../../domain/dtos/type.enum';
|
|
||||||
import { UpdateUsernameRequest } from '../../domain/dtos/update-username.request';
|
|
||||||
import { DeleteAuthenticationRequest } from '../../domain/dtos/delete-authentication.request';
|
|
||||||
import { DeleteAuthenticationCommand } from '../../commands/delete-authentication.command';
|
|
||||||
|
|
||||||
@Controller()
|
|
||||||
export class AuthenticationMessagerController {
|
|
||||||
constructor(private readonly commandBus: CommandBus) {}
|
|
||||||
|
|
||||||
@RabbitSubscribe({
|
|
||||||
name: 'userUpdate',
|
|
||||||
})
|
|
||||||
public async userUpdatedHandler(message: string) {
|
|
||||||
const updatedUser = JSON.parse(message);
|
|
||||||
if (!updatedUser.hasOwnProperty('uuid')) throw new Error();
|
|
||||||
if (updatedUser.hasOwnProperty('email') && updatedUser.email) {
|
|
||||||
const updateUsernameRequest = new UpdateUsernameRequest();
|
|
||||||
updateUsernameRequest.uuid = updatedUser.uuid;
|
|
||||||
updateUsernameRequest.username = updatedUser.email;
|
|
||||||
updateUsernameRequest.type = Type.EMAIL;
|
|
||||||
await this.commandBus.execute(
|
|
||||||
new UpdateUsernameCommand(updateUsernameRequest),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (updatedUser.hasOwnProperty('phone') && updatedUser.phone) {
|
|
||||||
const updateUsernameRequest = new UpdateUsernameRequest();
|
|
||||||
updateUsernameRequest.uuid = updatedUser.uuid;
|
|
||||||
updateUsernameRequest.username = updatedUser.phone;
|
|
||||||
updateUsernameRequest.type = Type.PHONE;
|
|
||||||
await this.commandBus.execute(
|
|
||||||
new UpdateUsernameCommand(updateUsernameRequest),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@RabbitSubscribe({
|
|
||||||
name: 'userDelete',
|
|
||||||
})
|
|
||||||
public async userDeletedHandler(message: string) {
|
|
||||||
const deletedUser = JSON.parse(message);
|
|
||||||
if (!deletedUser.hasOwnProperty('uuid')) throw new Error();
|
|
||||||
const deleteAuthenticationRequest = new DeleteAuthenticationRequest();
|
|
||||||
deleteAuthenticationRequest.uuid = deletedUser.uuid;
|
|
||||||
await this.commandBus.execute(
|
|
||||||
new DeleteAuthenticationCommand(deleteAuthenticationRequest),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,188 +0,0 @@
|
||||||
import { Mapper } from '@automapper/core';
|
|
||||||
import { InjectMapper } from '@automapper/nestjs';
|
|
||||||
import { Controller, UsePipes } from '@nestjs/common';
|
|
||||||
import { CommandBus, QueryBus } from '@nestjs/cqrs';
|
|
||||||
import { GrpcMethod, RpcException } from '@nestjs/microservices';
|
|
||||||
import { DatabaseException } from 'src/modules/database/exceptions/database.exception';
|
|
||||||
import { AddUsernameCommand } from '../../commands/add-username.command';
|
|
||||||
import { CreateAuthenticationCommand } from '../../commands/create-authentication.command';
|
|
||||||
import { DeleteAuthenticationCommand } from '../../commands/delete-authentication.command';
|
|
||||||
import { DeleteUsernameCommand } from '../../commands/delete-username.command';
|
|
||||||
import { UpdatePasswordCommand } from '../../commands/update-password.command';
|
|
||||||
import { UpdateUsernameCommand } from '../../commands/update-username.command';
|
|
||||||
import { AddUsernameRequest } from '../../domain/dtos/add-username.request';
|
|
||||||
import { CreateAuthenticationRequest } from '../../domain/dtos/create-authentication.request';
|
|
||||||
import { DeleteAuthenticationRequest } from '../../domain/dtos/delete-authentication.request';
|
|
||||||
import { DeleteUsernameRequest } from '../../domain/dtos/delete-username.request';
|
|
||||||
import { UpdatePasswordRequest } from '../../domain/dtos/update-password.request';
|
|
||||||
import { UpdateUsernameRequest } from '../../domain/dtos/update-username.request';
|
|
||||||
import { ValidateAuthenticationRequest } from '../../domain/dtos/validate-authentication.request';
|
|
||||||
import { Authentication } from '../../domain/entities/authentication';
|
|
||||||
import { Username } from '../../domain/entities/username';
|
|
||||||
import { ValidateAuthenticationQuery } from '../../queries/validate-authentication.query';
|
|
||||||
import { AuthenticationPresenter } from './authentication.presenter';
|
|
||||||
import { RpcValidationPipe } from '../../../../utils/pipes/rpc.validation-pipe';
|
|
||||||
import { UsernamePresenter } from './username.presenter';
|
|
||||||
|
|
||||||
@UsePipes(
|
|
||||||
new RpcValidationPipe({
|
|
||||||
whitelist: true,
|
|
||||||
forbidUnknownValues: false,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
@Controller()
|
|
||||||
export class AuthenticationController {
|
|
||||||
constructor(
|
|
||||||
private readonly commandBus: CommandBus,
|
|
||||||
private readonly queryBus: QueryBus,
|
|
||||||
@InjectMapper() private readonly mapper: Mapper,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
@GrpcMethod('AuthenticationService', 'Validate')
|
|
||||||
async validate(
|
|
||||||
data: ValidateAuthenticationRequest,
|
|
||||||
): Promise<AuthenticationPresenter> {
|
|
||||||
try {
|
|
||||||
const authentication: Authentication = await this.queryBus.execute(
|
|
||||||
new ValidateAuthenticationQuery(data.username, data.password),
|
|
||||||
);
|
|
||||||
return this.mapper.map(
|
|
||||||
authentication,
|
|
||||||
Authentication,
|
|
||||||
AuthenticationPresenter,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
throw new RpcException({
|
|
||||||
code: 7,
|
|
||||||
message: 'Permission denied',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@GrpcMethod('AuthenticationService', 'Create')
|
|
||||||
async createUser(
|
|
||||||
data: CreateAuthenticationRequest,
|
|
||||||
): Promise<AuthenticationPresenter> {
|
|
||||||
try {
|
|
||||||
const authentication: Authentication = await this.commandBus.execute(
|
|
||||||
new CreateAuthenticationCommand(data),
|
|
||||||
);
|
|
||||||
return this.mapper.map(
|
|
||||||
authentication,
|
|
||||||
Authentication,
|
|
||||||
AuthenticationPresenter,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof DatabaseException) {
|
|
||||||
if (e.message.includes('Already exists')) {
|
|
||||||
throw new RpcException({
|
|
||||||
code: 6,
|
|
||||||
message: 'Auth already exists',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new RpcException({
|
|
||||||
code: 7,
|
|
||||||
message: 'Permission denied',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@GrpcMethod('AuthenticationService', 'AddUsername')
|
|
||||||
async addUsername(data: AddUsernameRequest): Promise<UsernamePresenter> {
|
|
||||||
try {
|
|
||||||
const username: Username = await this.commandBus.execute(
|
|
||||||
new AddUsernameCommand(data),
|
|
||||||
);
|
|
||||||
|
|
||||||
return this.mapper.map(username, Username, UsernamePresenter);
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof DatabaseException) {
|
|
||||||
if (e.message.includes('Already exists')) {
|
|
||||||
throw new RpcException({
|
|
||||||
code: 6,
|
|
||||||
message: 'Username already exists',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new RpcException({
|
|
||||||
code: 7,
|
|
||||||
message: 'Permission denied',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@GrpcMethod('AuthenticationService', 'UpdateUsername')
|
|
||||||
async updateUsername(
|
|
||||||
data: UpdateUsernameRequest,
|
|
||||||
): Promise<UsernamePresenter> {
|
|
||||||
try {
|
|
||||||
const username: Username = await this.commandBus.execute(
|
|
||||||
new UpdateUsernameCommand(data),
|
|
||||||
);
|
|
||||||
|
|
||||||
return this.mapper.map(username, Username, UsernamePresenter);
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof DatabaseException) {
|
|
||||||
if (e.message.includes('Already exists')) {
|
|
||||||
throw new RpcException({
|
|
||||||
code: 6,
|
|
||||||
message: 'Username already exists',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new RpcException({
|
|
||||||
code: 7,
|
|
||||||
message: 'Permission denied',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@GrpcMethod('AuthenticationService', 'UpdatePassword')
|
|
||||||
async updatePassword(
|
|
||||||
data: UpdatePasswordRequest,
|
|
||||||
): Promise<AuthenticationPresenter> {
|
|
||||||
try {
|
|
||||||
const authentication: Authentication = await this.commandBus.execute(
|
|
||||||
new UpdatePasswordCommand(data),
|
|
||||||
);
|
|
||||||
|
|
||||||
return this.mapper.map(
|
|
||||||
authentication,
|
|
||||||
Authentication,
|
|
||||||
AuthenticationPresenter,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
throw new RpcException({
|
|
||||||
code: 7,
|
|
||||||
message: 'Permission denied',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@GrpcMethod('AuthenticationService', 'DeleteUsername')
|
|
||||||
async deleteUsername(data: DeleteUsernameRequest) {
|
|
||||||
try {
|
|
||||||
return await this.commandBus.execute(new DeleteUsernameCommand(data));
|
|
||||||
} catch (e) {
|
|
||||||
throw new RpcException({
|
|
||||||
code: 7,
|
|
||||||
message: 'Permission denied',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@GrpcMethod('AuthenticationService', 'Delete')
|
|
||||||
async deleteAuthentication(data: DeleteAuthenticationRequest) {
|
|
||||||
try {
|
|
||||||
return await this.commandBus.execute(
|
|
||||||
new DeleteAuthenticationCommand(data),
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
throw new RpcException({
|
|
||||||
code: 7,
|
|
||||||
message: 'Permission denied',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
import { AutoMap } from '@automapper/classes';
|
|
||||||
|
|
||||||
export class AuthenticationPresenter {
|
|
||||||
@AutoMap()
|
|
||||||
uuid: string;
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
syntax = "proto3";
|
|
||||||
|
|
||||||
package authentication;
|
|
||||||
|
|
||||||
service AuthenticationService {
|
|
||||||
rpc Validate(AuthenticationByUsernamePassword) returns (Uuid);
|
|
||||||
rpc Create(Authentication) returns (Uuid);
|
|
||||||
rpc AddUsername(Username) returns (Uuid);
|
|
||||||
rpc UpdatePassword(Password) returns (Uuid);
|
|
||||||
rpc UpdateUsername(Username) returns (Uuid);
|
|
||||||
rpc DeleteUsername(Username) returns (Uuid);
|
|
||||||
rpc Delete(Uuid) returns (Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
message AuthenticationByUsernamePassword {
|
|
||||||
string username = 1;
|
|
||||||
string password = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message Authentication {
|
|
||||||
string uuid = 1;
|
|
||||||
string username = 2;
|
|
||||||
string password = 3;
|
|
||||||
string type = 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
message Password {
|
|
||||||
string uuid = 1;
|
|
||||||
string password = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message Username {
|
|
||||||
string uuid = 1;
|
|
||||||
string username = 2;
|
|
||||||
string type = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
message Uuid {
|
|
||||||
string uuid = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message Empty {}
|
|
|
@ -1,9 +0,0 @@
|
||||||
import { AutoMap } from '@automapper/classes';
|
|
||||||
|
|
||||||
export class UsernamePresenter {
|
|
||||||
@AutoMap()
|
|
||||||
uuid: string;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
username: string;
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { AuthRepository } from '../../../database/domain/auth-repository';
|
|
||||||
import { Authentication } from '../../domain/entities/authentication';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class AuthenticationRepository extends AuthRepository<Authentication> {
|
|
||||||
protected model = 'auth';
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
import { AmqpConnection } from '@golevelup/nestjs-rabbitmq';
|
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
import { IMessageBroker } from '../../domain/interfaces/message-broker';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class Messager extends IMessageBroker {
|
|
||||||
constructor(
|
|
||||||
private readonly amqpConnection: AmqpConnection,
|
|
||||||
configService: ConfigService,
|
|
||||||
) {
|
|
||||||
super(configService.get<string>('RMQ_EXCHANGE'));
|
|
||||||
}
|
|
||||||
|
|
||||||
publish = (routingKey: string, message: string): void => {
|
|
||||||
this.amqpConnection.publish(this.exchange, routingKey, message);
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { AuthRepository } from '../../../database/domain/auth-repository';
|
|
||||||
import { Username } from '../../domain/entities/username';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class UsernameRepository extends AuthRepository<Username> {
|
|
||||||
protected model = 'username';
|
|
||||||
}
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
export const AUTHENTICATION_REPOSITORY = Symbol('AUTHENTICATION_REPOSITORY');
|
||||||
|
export const USERNAME_REPOSITORY = Symbol('USERNAME_REPOSITORY');
|
|
@ -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;
|
||||||
|
};
|
||||||
|
}
|
|
@ -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 {}
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
import { AddUsernameRequest } from '../domain/dtos/add-username.request';
|
|
||||||
|
|
||||||
export class AddUsernameCommand {
|
|
||||||
readonly addUsernameRequest: AddUsernameRequest;
|
|
||||||
|
|
||||||
constructor(request: AddUsernameRequest) {
|
|
||||||
this.addUsernameRequest = request;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
import { CreateAuthenticationRequest } from '../domain/dtos/create-authentication.request';
|
|
||||||
|
|
||||||
export class CreateAuthenticationCommand {
|
|
||||||
readonly createAuthenticationRequest: CreateAuthenticationRequest;
|
|
||||||
|
|
||||||
constructor(request: CreateAuthenticationRequest) {
|
|
||||||
this.createAuthenticationRequest = request;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
import { DeleteAuthenticationRequest } from '../domain/dtos/delete-authentication.request';
|
|
||||||
|
|
||||||
export class DeleteAuthenticationCommand {
|
|
||||||
readonly deleteAuthenticationRequest: DeleteAuthenticationRequest;
|
|
||||||
|
|
||||||
constructor(request: DeleteAuthenticationRequest) {
|
|
||||||
this.deleteAuthenticationRequest = request;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
import { DeleteUsernameRequest } from '../domain/dtos/delete-username.request';
|
|
||||||
|
|
||||||
export class DeleteUsernameCommand {
|
|
||||||
readonly deleteUsernameRequest: DeleteUsernameRequest;
|
|
||||||
|
|
||||||
constructor(request: DeleteUsernameRequest) {
|
|
||||||
this.deleteUsernameRequest = request;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
import { UpdatePasswordRequest } from '../domain/dtos/update-password.request';
|
|
||||||
|
|
||||||
export class UpdatePasswordCommand {
|
|
||||||
readonly updatePasswordRequest: UpdatePasswordRequest;
|
|
||||||
|
|
||||||
constructor(request: UpdatePasswordRequest) {
|
|
||||||
this.updatePasswordRequest = request;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
import { UpdateUsernameRequest } from '../domain/dtos/update-username.request';
|
|
||||||
|
|
||||||
export class UpdateUsernameCommand {
|
|
||||||
readonly updateUsernameRequest: UpdateUsernameRequest;
|
|
||||||
|
|
||||||
constructor(request: UpdateUsernameRequest) {
|
|
||||||
this.updateUsernameRequest = request;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { Command, CommandProps } from '@mobicoop/ddd-library';
|
||||||
|
import { Username } from '../../types/username';
|
||||||
|
|
||||||
|
export class AddUsernameCommand extends Command {
|
||||||
|
readonly userId: string;
|
||||||
|
readonly username: Username;
|
||||||
|
|
||||||
|
constructor(props: CommandProps<AddUsernameCommand>) {
|
||||||
|
super(props);
|
||||||
|
this.userId = props.userId;
|
||||||
|
this.username = props.username;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||||
|
import { Inject } from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
AggregateID,
|
||||||
|
ConflictException,
|
||||||
|
UniqueConstraintException,
|
||||||
|
} from '@mobicoop/ddd-library';
|
||||||
|
import {
|
||||||
|
AUTHENTICATION_REPOSITORY,
|
||||||
|
USERNAME_REPOSITORY,
|
||||||
|
} from '@modules/authentication/authentication.di-tokens';
|
||||||
|
import { AuthenticationRepositoryPort } from '../../ports/authentication.repository.port';
|
||||||
|
import { UsernameAlreadyExistsException } from '@modules/authentication/core/domain/authentication.errors';
|
||||||
|
import { AddUsernameCommand } from './add-username.command';
|
||||||
|
import { UsernameRepositoryPort } from '../../ports/username.repository.port';
|
||||||
|
import { UsernameEntity } from '@modules/authentication/core/domain/username.entity';
|
||||||
|
|
||||||
|
@CommandHandler(AddUsernameCommand)
|
||||||
|
export class AddUsernameService implements ICommandHandler {
|
||||||
|
constructor(
|
||||||
|
@Inject(AUTHENTICATION_REPOSITORY)
|
||||||
|
private readonly authenticationRepository: AuthenticationRepositoryPort,
|
||||||
|
@Inject(USERNAME_REPOSITORY)
|
||||||
|
private readonly usernameRepository: UsernameRepositoryPort,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async execute(command: AddUsernameCommand): Promise<AggregateID> {
|
||||||
|
await this.authenticationRepository.findOneById(command.userId, {
|
||||||
|
usernames: true,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const newUsername = await UsernameEntity.create({
|
||||||
|
name: command.username.name,
|
||||||
|
userId: command.userId,
|
||||||
|
type: command.username.type,
|
||||||
|
});
|
||||||
|
await this.usernameRepository.insert(newUsername);
|
||||||
|
return newUsername.id;
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error instanceof ConflictException) {
|
||||||
|
throw new UsernameAlreadyExistsException(error);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
error instanceof UniqueConstraintException &&
|
||||||
|
error.message.includes('username')
|
||||||
|
) {
|
||||||
|
throw new UsernameAlreadyExistsException(error);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { Command, CommandProps } from '@mobicoop/ddd-library';
|
||||||
|
import { Username } from '../../types/username';
|
||||||
|
|
||||||
|
export class CreateAuthenticationCommand extends Command {
|
||||||
|
readonly userId: string;
|
||||||
|
readonly password: string;
|
||||||
|
readonly usernames: Username[];
|
||||||
|
|
||||||
|
constructor(props: CommandProps<CreateAuthenticationCommand>) {
|
||||||
|
super(props);
|
||||||
|
this.userId = props.userId;
|
||||||
|
this.password = props.password;
|
||||||
|
this.usernames = props.usernames;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||||
|
import { Inject } from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
AggregateID,
|
||||||
|
ConflictException,
|
||||||
|
UniqueConstraintException,
|
||||||
|
} from '@mobicoop/ddd-library';
|
||||||
|
import { CreateAuthenticationCommand } from './create-authentication.command';
|
||||||
|
import { AUTHENTICATION_REPOSITORY } from '@modules/authentication/authentication.di-tokens';
|
||||||
|
import { AuthenticationRepositoryPort } from '../../ports/authentication.repository.port';
|
||||||
|
import { AuthenticationEntity } from '@modules/authentication/core/domain/authentication.entity';
|
||||||
|
import {
|
||||||
|
AuthenticationAlreadyExistsException,
|
||||||
|
UsernameAlreadyExistsException,
|
||||||
|
} from '@modules/authentication/core/domain/authentication.errors';
|
||||||
|
|
||||||
|
@CommandHandler(CreateAuthenticationCommand)
|
||||||
|
export class CreateAuthenticationService implements ICommandHandler {
|
||||||
|
constructor(
|
||||||
|
@Inject(AUTHENTICATION_REPOSITORY)
|
||||||
|
private readonly authenticationRepository: AuthenticationRepositoryPort,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async execute(command: CreateAuthenticationCommand): Promise<AggregateID> {
|
||||||
|
const authentication: AuthenticationEntity =
|
||||||
|
await AuthenticationEntity.create({
|
||||||
|
userId: command.userId,
|
||||||
|
password: command.password,
|
||||||
|
usernames: command.usernames,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await this.authenticationRepository.insert(authentication);
|
||||||
|
return authentication.getProps().userId;
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error instanceof ConflictException) {
|
||||||
|
throw new AuthenticationAlreadyExistsException(error);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
error instanceof UniqueConstraintException &&
|
||||||
|
error.message.includes('uuid')
|
||||||
|
) {
|
||||||
|
throw new AuthenticationAlreadyExistsException(error);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
error instanceof UniqueConstraintException &&
|
||||||
|
error.message.includes('username')
|
||||||
|
) {
|
||||||
|
throw new UsernameAlreadyExistsException(error);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { Command, CommandProps } from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
|
export class DeleteAuthenticationCommand extends Command {
|
||||||
|
readonly userId: string;
|
||||||
|
|
||||||
|
constructor(props: CommandProps<DeleteAuthenticationCommand>) {
|
||||||
|
super(props);
|
||||||
|
this.userId = props.userId;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { Inject } from '@nestjs/common';
|
||||||
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||||
|
import { DeleteAuthenticationCommand } from './delete-authentication.command';
|
||||||
|
import { AUTHENTICATION_REPOSITORY } from '@modules/authentication/authentication.di-tokens';
|
||||||
|
import { AuthenticationRepositoryPort } from '../../ports/authentication.repository.port';
|
||||||
|
import { AuthenticationEntity } from '@modules/authentication/core/domain/authentication.entity';
|
||||||
|
|
||||||
|
@CommandHandler(DeleteAuthenticationCommand)
|
||||||
|
export class DeleteAuthenticationService implements ICommandHandler {
|
||||||
|
constructor(
|
||||||
|
@Inject(AUTHENTICATION_REPOSITORY)
|
||||||
|
private readonly authenticationRepository: AuthenticationRepositoryPort,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async execute(command: DeleteAuthenticationCommand): Promise<boolean> {
|
||||||
|
const authentication: AuthenticationEntity =
|
||||||
|
await this.authenticationRepository.findOneById(command.userId, {
|
||||||
|
usernames: true,
|
||||||
|
});
|
||||||
|
authentication.delete();
|
||||||
|
const isDeleted: boolean = await this.authenticationRepository.delete(
|
||||||
|
authentication,
|
||||||
|
);
|
||||||
|
return isDeleted;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
import { RepositoryPort } from '@mobicoop/ddd-library';
|
||||||
|
import { AuthenticationEntity } from '@modules/authentication/core/domain/authentication.entity';
|
||||||
|
|
||||||
|
export type AuthenticationRepositoryPort = RepositoryPort<AuthenticationEntity>;
|
|
@ -0,0 +1,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>;
|
||||||
|
};
|
|
@ -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();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { Type } from '@modules/authentication/core/domain/username.types';
|
||||||
|
|
||||||
|
export type Username = {
|
||||||
|
name: string;
|
||||||
|
type: Type;
|
||||||
|
};
|
|
@ -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);
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { ExceptionBase } from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
|
export class AuthenticationAlreadyExistsException extends ExceptionBase {
|
||||||
|
static readonly message = 'Authentication already exists';
|
||||||
|
|
||||||
|
public readonly code = 'AUTHENTICATION.ALREADY_EXISTS';
|
||||||
|
|
||||||
|
constructor(cause?: Error, metadata?: unknown) {
|
||||||
|
super(AuthenticationAlreadyExistsException.message, cause, metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UsernameAlreadyExistsException extends ExceptionBase {
|
||||||
|
static readonly message = 'Username already exists';
|
||||||
|
|
||||||
|
public readonly code = 'USERNAME.ALREADY_EXISTS';
|
||||||
|
|
||||||
|
constructor(cause?: Error, metadata?: unknown) {
|
||||||
|
super(UsernameAlreadyExistsException.message, cause, metadata);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { UsernameProps } from './username.types';
|
||||||
|
|
||||||
|
// All properties that an Authentication has
|
||||||
|
export interface AuthenticationProps {
|
||||||
|
userId: string;
|
||||||
|
password: string;
|
||||||
|
usernames: UsernameProps[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Properties that are needed for an Authentication creation
|
||||||
|
export interface CreateAuthenticationProps {
|
||||||
|
userId: string;
|
||||||
|
password: string;
|
||||||
|
usernames: UsernameProps[];
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
|
export class AuthenticationCreatedDomainEvent extends DomainEvent {
|
||||||
|
constructor(props: DomainEventProps<AuthenticationCreatedDomainEvent>) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
|
export class AuthenticationDeletedDomainEvent extends DomainEvent {
|
||||||
|
constructor(props: DomainEventProps<AuthenticationDeletedDomainEvent>) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
|
export class PasswordUpdatedDomainEvent extends DomainEvent {
|
||||||
|
constructor(props: DomainEventProps<PasswordUpdatedDomainEvent>) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
|
export class UsernameAddedDomainEvent extends DomainEvent {
|
||||||
|
constructor(props: DomainEventProps<UsernameAddedDomainEvent>) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
|
export class UsernameDeletedDomainEvent extends DomainEvent {
|
||||||
|
constructor(props: DomainEventProps<UsernameDeletedDomainEvent>) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
|
export class UsernameUpdatedDomainEvent extends DomainEvent {
|
||||||
|
constructor(props: DomainEventProps<UsernameUpdatedDomainEvent>) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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',
|
||||||
|
}
|
|
@ -1,20 +0,0 @@
|
||||||
import { AutoMap } from '@automapper/classes';
|
|
||||||
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
|
|
||||||
import { Type } from './type.enum';
|
|
||||||
|
|
||||||
export class AddUsernameRequest {
|
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
@AutoMap()
|
|
||||||
uuid: string;
|
|
||||||
|
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
@AutoMap()
|
|
||||||
username: string;
|
|
||||||
|
|
||||||
@IsEnum(Type)
|
|
||||||
@IsNotEmpty()
|
|
||||||
@AutoMap()
|
|
||||||
type: Type;
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
import { AutoMap } from '@automapper/classes';
|
|
||||||
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
|
|
||||||
import { Type } from './type.enum';
|
|
||||||
|
|
||||||
export class CreateAuthenticationRequest {
|
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
@AutoMap()
|
|
||||||
uuid: string;
|
|
||||||
|
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
@AutoMap()
|
|
||||||
username: string;
|
|
||||||
|
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
@AutoMap()
|
|
||||||
password: string;
|
|
||||||
|
|
||||||
@IsEnum(Type)
|
|
||||||
@IsNotEmpty()
|
|
||||||
@AutoMap()
|
|
||||||
type: Type;
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
import { AutoMap } from '@automapper/classes';
|
|
||||||
import { IsNotEmpty, IsString } from 'class-validator';
|
|
||||||
|
|
||||||
export class DeleteAuthenticationRequest {
|
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
@AutoMap()
|
|
||||||
uuid: string;
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
import { AutoMap } from '@automapper/classes';
|
|
||||||
import { IsNotEmpty, IsString } from 'class-validator';
|
|
||||||
|
|
||||||
export class DeleteUsernameRequest {
|
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
@AutoMap()
|
|
||||||
username: string;
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
export enum Type {
|
|
||||||
EMAIL = 'EMAIL',
|
|
||||||
PHONE = 'PHONE',
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
import { AutoMap } from '@automapper/classes';
|
|
||||||
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
|
|
||||||
import { Type } from './type.enum';
|
|
||||||
|
|
||||||
export class UpdateUsernameRequest {
|
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
@AutoMap()
|
|
||||||
uuid: string;
|
|
||||||
|
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
@AutoMap()
|
|
||||||
username: string;
|
|
||||||
|
|
||||||
@IsEnum(Type)
|
|
||||||
@IsNotEmpty()
|
|
||||||
@AutoMap()
|
|
||||||
type: Type;
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
import { AutoMap } from '@automapper/classes';
|
|
||||||
|
|
||||||
export class Authentication {
|
|
||||||
@AutoMap()
|
|
||||||
uuid: string;
|
|
||||||
|
|
||||||
password: string;
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
import { AutoMap } from '@automapper/classes';
|
|
||||||
import { Type } from '../dtos/type.enum';
|
|
||||||
|
|
||||||
export class Username {
|
|
||||||
@AutoMap()
|
|
||||||
uuid: string;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
username: string;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
type: Type;
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export abstract class IMessageBroker {
|
|
||||||
exchange: string;
|
|
||||||
|
|
||||||
constructor(exchange: string) {
|
|
||||||
this.exchange = exchange;
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract publish(routingKey: string, message: string): void;
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
import { CommandHandler } from '@nestjs/cqrs';
|
|
||||||
import { Messager } from '../../adapters/secondaries/messager';
|
|
||||||
import { UsernameRepository } from '../../adapters/secondaries/username.repository';
|
|
||||||
import { AddUsernameCommand } from '../../commands/add-username.command';
|
|
||||||
import { Username } from '../entities/username';
|
|
||||||
|
|
||||||
@CommandHandler(AddUsernameCommand)
|
|
||||||
export class AddUsernameUseCase {
|
|
||||||
constructor(
|
|
||||||
private readonly usernameRepository: UsernameRepository,
|
|
||||||
private readonly messager: Messager,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
execute = async (command: AddUsernameCommand): Promise<Username> => {
|
|
||||||
const { uuid, username, type } = command.addUsernameRequest;
|
|
||||||
try {
|
|
||||||
return await this.usernameRepository.create({
|
|
||||||
uuid,
|
|
||||||
type,
|
|
||||||
username,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
this.messager.publish(
|
|
||||||
'logging.auth.username.add.warning',
|
|
||||||
JSON.stringify({
|
|
||||||
command,
|
|
||||||
error,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
import { CommandHandler } from '@nestjs/cqrs';
|
|
||||||
import { AuthenticationRepository } from '../../adapters/secondaries/authentication.repository';
|
|
||||||
import { CreateAuthenticationCommand } from '../../commands/create-authentication.command';
|
|
||||||
import { Authentication } from '../entities/authentication';
|
|
||||||
import * as bcrypt from 'bcrypt';
|
|
||||||
import { UsernameRepository } from '../../adapters/secondaries/username.repository';
|
|
||||||
import { Messager } from '../../adapters/secondaries/messager';
|
|
||||||
|
|
||||||
@CommandHandler(CreateAuthenticationCommand)
|
|
||||||
export class CreateAuthenticationUseCase {
|
|
||||||
constructor(
|
|
||||||
private readonly authenticationRepository: AuthenticationRepository,
|
|
||||||
private readonly usernameRepository: UsernameRepository,
|
|
||||||
private readonly messager: Messager,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
execute = async (
|
|
||||||
command: CreateAuthenticationCommand,
|
|
||||||
): Promise<Authentication> => {
|
|
||||||
const { uuid, password, ...username } = command.createAuthenticationRequest;
|
|
||||||
const hash = await bcrypt.hash(password, 10);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const auth = await this.authenticationRepository.create({
|
|
||||||
uuid,
|
|
||||||
password: hash,
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.usernameRepository.create({
|
|
||||||
uuid,
|
|
||||||
...username,
|
|
||||||
});
|
|
||||||
|
|
||||||
return auth;
|
|
||||||
} catch (error) {
|
|
||||||
this.messager.publish(
|
|
||||||
'logging.auth.create.crit',
|
|
||||||
JSON.stringify({
|
|
||||||
command,
|
|
||||||
error,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
import { CommandHandler } from '@nestjs/cqrs';
|
|
||||||
import { AuthenticationRepository } from '../../adapters/secondaries/authentication.repository';
|
|
||||||
import { Messager } from '../../adapters/secondaries/messager';
|
|
||||||
import { UsernameRepository } from '../../adapters/secondaries/username.repository';
|
|
||||||
import { DeleteAuthenticationCommand } from '../../commands/delete-authentication.command';
|
|
||||||
import { Authentication } from '../entities/authentication';
|
|
||||||
|
|
||||||
@CommandHandler(DeleteAuthenticationCommand)
|
|
||||||
export class DeleteAuthenticationUseCase {
|
|
||||||
constructor(
|
|
||||||
private readonly authenticationRepository: AuthenticationRepository,
|
|
||||||
private readonly usernameRepository: UsernameRepository,
|
|
||||||
private readonly messager: Messager,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
execute = async (
|
|
||||||
command: DeleteAuthenticationCommand,
|
|
||||||
): Promise<Authentication> => {
|
|
||||||
try {
|
|
||||||
await this.usernameRepository.deleteMany({
|
|
||||||
uuid: command.deleteAuthenticationRequest.uuid,
|
|
||||||
});
|
|
||||||
return await this.authenticationRepository.delete(
|
|
||||||
command.deleteAuthenticationRequest.uuid,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
this.messager.publish(
|
|
||||||
'logging.auth.delete.crit',
|
|
||||||
JSON.stringify({
|
|
||||||
command,
|
|
||||||
error,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,38 +0,0 @@
|
||||||
import { UnauthorizedException } from '@nestjs/common';
|
|
||||||
import { CommandHandler } from '@nestjs/cqrs';
|
|
||||||
import { Messager } from '../../adapters/secondaries/messager';
|
|
||||||
import { UsernameRepository } from '../../adapters/secondaries/username.repository';
|
|
||||||
import { DeleteUsernameCommand } from '../../commands/delete-username.command';
|
|
||||||
|
|
||||||
@CommandHandler(DeleteUsernameCommand)
|
|
||||||
export class DeleteUsernameUseCase {
|
|
||||||
constructor(
|
|
||||||
private readonly usernameRepository: UsernameRepository,
|
|
||||||
private readonly messager: Messager,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
execute = async (command: DeleteUsernameCommand): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const { username } = command.deleteUsernameRequest;
|
|
||||||
const usernameFound = await this.usernameRepository.findOne({
|
|
||||||
username,
|
|
||||||
});
|
|
||||||
const usernames = await this.usernameRepository.findAll(1, 1, {
|
|
||||||
uuid: usernameFound.uuid,
|
|
||||||
});
|
|
||||||
if (usernames.total > 1) {
|
|
||||||
return await this.usernameRepository.deleteMany({ username });
|
|
||||||
}
|
|
||||||
throw new UnauthorizedException();
|
|
||||||
} catch (error) {
|
|
||||||
this.messager.publish(
|
|
||||||
'logging.auth.username.delete.warning',
|
|
||||||
JSON.stringify({
|
|
||||||
command,
|
|
||||||
error,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,34 +0,0 @@
|
||||||
import { CommandHandler } from '@nestjs/cqrs';
|
|
||||||
import { AuthenticationRepository } from '../../adapters/secondaries/authentication.repository';
|
|
||||||
import { Authentication } from '../entities/authentication';
|
|
||||||
import * as bcrypt from 'bcrypt';
|
|
||||||
import { UpdatePasswordCommand } from '../../commands/update-password.command';
|
|
||||||
import { Messager } from '../../adapters/secondaries/messager';
|
|
||||||
|
|
||||||
@CommandHandler(UpdatePasswordCommand)
|
|
||||||
export class UpdatePasswordUseCase {
|
|
||||||
constructor(
|
|
||||||
private readonly authenticationRepository: AuthenticationRepository,
|
|
||||||
private readonly messager: Messager,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
execute = async (command: UpdatePasswordCommand): Promise<Authentication> => {
|
|
||||||
const { uuid, password } = command.updatePasswordRequest;
|
|
||||||
const hash = await bcrypt.hash(password, 10);
|
|
||||||
|
|
||||||
try {
|
|
||||||
return await this.authenticationRepository.update(uuid, {
|
|
||||||
password: hash,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
this.messager.publish(
|
|
||||||
'logging.auth.password.update.warning',
|
|
||||||
JSON.stringify({
|
|
||||||
command,
|
|
||||||
error,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,67 +0,0 @@
|
||||||
import { Mapper } from '@automapper/core';
|
|
||||||
import { InjectMapper } from '@automapper/nestjs';
|
|
||||||
import { BadRequestException } from '@nestjs/common';
|
|
||||||
import { CommandBus, CommandHandler } from '@nestjs/cqrs';
|
|
||||||
import { Messager } from '../../adapters/secondaries/messager';
|
|
||||||
import { UsernameRepository } from '../../adapters/secondaries/username.repository';
|
|
||||||
import { AddUsernameCommand } from '../../commands/add-username.command';
|
|
||||||
import { UpdateUsernameCommand } from '../../commands/update-username.command';
|
|
||||||
import { AddUsernameRequest } from '../dtos/add-username.request';
|
|
||||||
import { UpdateUsernameRequest } from '../dtos/update-username.request';
|
|
||||||
import { Username } from '../entities/username';
|
|
||||||
|
|
||||||
@CommandHandler(UpdateUsernameCommand)
|
|
||||||
export class UpdateUsernameUseCase {
|
|
||||||
constructor(
|
|
||||||
private readonly usernameRepository: UsernameRepository,
|
|
||||||
private readonly commandBus: CommandBus,
|
|
||||||
@InjectMapper() private readonly mapper: Mapper,
|
|
||||||
private readonly messager: Messager,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
execute = async (command: UpdateUsernameCommand): Promise<Username> => {
|
|
||||||
const { uuid, username, type } = command.updateUsernameRequest;
|
|
||||||
if (!username) throw new BadRequestException();
|
|
||||||
// update username if it exists, otherwise create it
|
|
||||||
const existingUsername = await this.usernameRepository.findOne({
|
|
||||||
uuid,
|
|
||||||
type,
|
|
||||||
});
|
|
||||||
if (existingUsername) {
|
|
||||||
try {
|
|
||||||
return await this.usernameRepository.updateWhere(
|
|
||||||
{
|
|
||||||
uuid_type: {
|
|
||||||
uuid,
|
|
||||||
type,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
username,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
this.messager.publish(
|
|
||||||
'logging.auth.username.update.warning',
|
|
||||||
JSON.stringify({
|
|
||||||
command,
|
|
||||||
error,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const addUsernameRequest = this.mapper.map(
|
|
||||||
command.updateUsernameRequest,
|
|
||||||
UpdateUsernameRequest,
|
|
||||||
AddUsernameRequest,
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
return await this.commandBus.execute(
|
|
||||||
new AddUsernameCommand(addUsernameRequest),
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,41 +0,0 @@
|
||||||
import { QueryHandler } from '@nestjs/cqrs';
|
|
||||||
import { AuthenticationRepository } from '../../adapters/secondaries/authentication.repository';
|
|
||||||
import { ValidateAuthenticationQuery as ValidateAuthenticationQuery } from '../../queries/validate-authentication.query';
|
|
||||||
import { Authentication } from '../entities/authentication';
|
|
||||||
import * as bcrypt from 'bcrypt';
|
|
||||||
import { NotFoundException, UnauthorizedException } from '@nestjs/common';
|
|
||||||
import { UsernameRepository } from '../../adapters/secondaries/username.repository';
|
|
||||||
import { Username } from '../entities/username';
|
|
||||||
|
|
||||||
@QueryHandler(ValidateAuthenticationQuery)
|
|
||||||
export class ValidateAuthenticationUseCase {
|
|
||||||
constructor(
|
|
||||||
private readonly authenticationRepository: AuthenticationRepository,
|
|
||||||
private readonly usernameRepository: UsernameRepository,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
execute = async (
|
|
||||||
validate: ValidateAuthenticationQuery,
|
|
||||||
): Promise<Authentication> => {
|
|
||||||
let username = new Username();
|
|
||||||
try {
|
|
||||||
username = await this.usernameRepository.findOne({
|
|
||||||
username: validate.username,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const auth = await this.authenticationRepository.findOne({
|
|
||||||
uuid: username.uuid,
|
|
||||||
});
|
|
||||||
if (auth) {
|
|
||||||
const isMatch = await bcrypt.compare(validate.password, auth.password);
|
|
||||||
if (isMatch) return auth;
|
|
||||||
}
|
|
||||||
throw new UnauthorizedException();
|
|
||||||
} catch (e) {
|
|
||||||
throw new UnauthorizedException();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import {
|
||||||
|
LoggerBase,
|
||||||
|
MessagePublisherPort,
|
||||||
|
PrismaRepositoryBase,
|
||||||
|
} from '@mobicoop/ddd-library';
|
||||||
|
import { AuthenticationEntity } from '../core/domain/authentication.entity';
|
||||||
|
import { AuthenticationRepositoryPort } from '../core/application/ports/authentication.repository.port';
|
||||||
|
import { PrismaService } from './prisma.service';
|
||||||
|
import { AuthenticationMapper } from '../authentication.mapper';
|
||||||
|
import { MESSAGE_PUBLISHER } from '@src/app.di-tokens';
|
||||||
|
|
||||||
|
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,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { PaginatedResponseDto } from '@mobicoop/ddd-library';
|
||||||
|
import { AuthenticationResponseDto } from './authentication.response.dto';
|
||||||
|
|
||||||
|
export class AauthenticationPaginatedResponseDto extends PaginatedResponseDto<AuthenticationResponseDto> {
|
||||||
|
readonly data: readonly AuthenticationResponseDto[];
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { ResponseBase } from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
|
export class AuthenticationResponseDto extends ResponseBase {}
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { ResponseBase } from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
|
export class UsernameResponseDto extends ResponseBase {}
|
|
@ -0,0 +1,49 @@
|
||||||
|
import {
|
||||||
|
AggregateID,
|
||||||
|
IdResponse,
|
||||||
|
RpcExceptionCode,
|
||||||
|
RpcValidationPipe,
|
||||||
|
} from '@mobicoop/ddd-library';
|
||||||
|
import { Controller, UsePipes } from '@nestjs/common';
|
||||||
|
import { CommandBus } from '@nestjs/cqrs';
|
||||||
|
import { GrpcMethod, RpcException } from '@nestjs/microservices';
|
||||||
|
import { UsernameAlreadyExistsException } from '@modules/authentication/core/domain/authentication.errors';
|
||||||
|
import { AddUsernameRequestDto } from './dtos/add-username.request.dto';
|
||||||
|
import { AddUsernameCommand } from '@modules/authentication/core/application/commands/add-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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {}
|
|
@ -0,0 +1,43 @@
|
||||||
|
import {
|
||||||
|
AggregateID,
|
||||||
|
IdResponse,
|
||||||
|
RpcExceptionCode,
|
||||||
|
RpcValidationPipe,
|
||||||
|
} from '@mobicoop/ddd-library';
|
||||||
|
import { Controller, UsePipes } from '@nestjs/common';
|
||||||
|
import { CommandBus } from '@nestjs/cqrs';
|
||||||
|
import { GrpcMethod, RpcException } from '@nestjs/microservices';
|
||||||
|
import { CreateAuthenticationRequestDto } from './dtos/create-authentication.request.dto';
|
||||||
|
import { CreateAuthenticationCommand } from '@modules/authentication/core/application/commands/create-authentication/create-authentication.command';
|
||||||
|
import { AuthenticationAlreadyExistsException } from '@modules/authentication/core/domain/authentication.errors';
|
||||||
|
|
||||||
|
@UsePipes(
|
||||||
|
new RpcValidationPipe({
|
||||||
|
whitelist: true,
|
||||||
|
forbidUnknownValues: false,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
@Controller()
|
||||||
|
export class CreateAuthenticationGrpcController {
|
||||||
|
constructor(private readonly commandBus: CommandBus) {}
|
||||||
|
|
||||||
|
@GrpcMethod('AuthenticationService', 'Create')
|
||||||
|
async create(data: CreateAuthenticationRequestDto): Promise<IdResponse> {
|
||||||
|
try {
|
||||||
|
const aggregateID: AggregateID = await this.commandBus.execute(
|
||||||
|
new CreateAuthenticationCommand(data),
|
||||||
|
);
|
||||||
|
return new IdResponse(aggregateID);
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error instanceof AuthenticationAlreadyExistsException)
|
||||||
|
throw new RpcException({
|
||||||
|
code: RpcExceptionCode.ALREADY_EXISTS,
|
||||||
|
message: error.message,
|
||||||
|
});
|
||||||
|
throw new RpcException({
|
||||||
|
code: RpcExceptionCode.UNKNOWN,
|
||||||
|
message: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
import {
|
||||||
|
DatabaseErrorException,
|
||||||
|
NotFoundException,
|
||||||
|
RpcExceptionCode,
|
||||||
|
RpcValidationPipe,
|
||||||
|
} from '@mobicoop/ddd-library';
|
||||||
|
import { Controller, UsePipes } from '@nestjs/common';
|
||||||
|
import { CommandBus } from '@nestjs/cqrs';
|
||||||
|
import { GrpcMethod, RpcException } from '@nestjs/microservices';
|
||||||
|
import { DeleteAuthenticationCommand } from '@modules/authentication/core/application/commands/delete-authentication/delete-authentication.command';
|
||||||
|
import { DeleteAuthenticationRequestDto } from './dtos/delete-authentication.request.dto';
|
||||||
|
|
||||||
|
@UsePipes(
|
||||||
|
new RpcValidationPipe({
|
||||||
|
whitelist: true,
|
||||||
|
forbidUnknownValues: false,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
@Controller()
|
||||||
|
export class DeleteAuthenticationGrpcController {
|
||||||
|
constructor(private readonly commandBus: CommandBus) {}
|
||||||
|
|
||||||
|
@GrpcMethod('AuthenticationService', 'Delete')
|
||||||
|
async delete(data: DeleteAuthenticationRequestDto): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.commandBus.execute(new DeleteAuthenticationCommand(data));
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error instanceof NotFoundException)
|
||||||
|
throw new RpcException({
|
||||||
|
code: RpcExceptionCode.NOT_FOUND,
|
||||||
|
message: error.message,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error instanceof DatabaseErrorException)
|
||||||
|
throw new RpcException({
|
||||||
|
code: RpcExceptionCode.INTERNAL,
|
||||||
|
message: error.message,
|
||||||
|
});
|
||||||
|
throw new RpcException({
|
||||||
|
code: RpcExceptionCode.UNKNOWN,
|
||||||
|
message: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { IsNotEmpty, IsString } from 'class-validator';
|
||||||
|
import { UsernameDto } from './username.dto';
|
||||||
|
|
||||||
|
export class AddUsernameRequestDto extends UsernameDto {
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
userId: string;
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
import {
|
||||||
|
ArrayMinSize,
|
||||||
|
IsArray,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsString,
|
||||||
|
ValidateNested,
|
||||||
|
} from 'class-validator';
|
||||||
|
import { UsernameDto } from './username.dto';
|
||||||
|
|
||||||
|
export class CreateAuthenticationRequestDto {
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
userId: string;
|
||||||
|
|
||||||
|
@Type(() => UsernameDto)
|
||||||
|
@IsArray()
|
||||||
|
@ArrayMinSize(1)
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
usernames: UsernameDto[];
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
password: string;
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { IsNotEmpty, IsString } from 'class-validator';
|
||||||
|
|
||||||
|
export class DeleteAuthenticationRequestDto {
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
userId: string;
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { IsNotEmpty, IsString } from 'class-validator';
|
||||||
|
|
||||||
|
export class DeleteUsernameRequestDto {
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
name: string;
|
||||||
|
}
|
|
@ -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()
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { IsNotEmpty, IsString } from 'class-validator';
|
||||||
|
import { UsernameDto } from './username.dto';
|
||||||
|
|
||||||
|
export class UpdateUsernameRequestDto extends UsernameDto {
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
userId: string;
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { Type } from '@modules/authentication/core/domain/username.types';
|
||||||
|
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
|
||||||
|
import { IsValidUsername } from './validators/decorators/is-valid-username.decorator';
|
||||||
|
|
||||||
|
export class UsernameDto {
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsValidUsername({
|
||||||
|
message: 'Invalid username',
|
||||||
|
})
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@IsEnum(Type)
|
||||||
|
@IsNotEmpty()
|
||||||
|
type: Type;
|
||||||
|
}
|
|
@ -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;
|
||||||
}
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
|
@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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) {}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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) {}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
Loading…
Reference in New Issue