diff --git a/.env.dist b/.env.dist index 058bb27..e55ee74 100644 --- a/.env.dist +++ b/.env.dist @@ -10,3 +10,7 @@ RMQ_URI=amqp://v3-broker:5672 # POSTGRES POSTGRES_IMAGE=postgres:15.0 + +# OPA +OPA_IMAGE=openpolicyagent/opa:0.48.0-rootless +OPA_URL=http://v3-opa:8181/v1/data/ diff --git a/.env.test b/.env.test index 215555a..00231b0 100644 --- a/.env.test +++ b/.env.test @@ -10,3 +10,7 @@ RMQ_URI=amqp://v3-broker:5672 # POSTGRES POSTGRES_IMAGE=postgres:15.0 + +# OPA +OPA_IMAGE=openpolicyagent/opa:0.48.0-rootless +OPA_URL=http://v3-opa:8181/v1/data/ diff --git a/Dockerfile b/Dockerfile index 147fe0f..85927e7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,6 +14,7 @@ COPY --chown=node:node package*.json ./ # Install app dependencies using the `npm ci` command instead of `npm install` RUN npm ci +RUN npx prisma generate # Bundle app source COPY --chown=node:node . . diff --git a/README.md b/README.md index 9d00ced..7e82dfb 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,9 @@ npm run migrate ## Usage -The app is used for authentication (aka AuthN) and authorization (aka AuthZ : _to be developped_). +The app is used for authentication (aka AuthN) and authorization (aka AuthZ). + +### AuthN AuthN consists in verifying a username / password couple. A user can have multiple usernames (representing multiple identifiers), all of them sharing the same password. In the app, all the authentication information about a user is called an _auth_. As of 2022/10/23, the possible identifiers are : @@ -122,6 +124,41 @@ For AuthN, the app exposes the following [gRPC](https://grpc.io/) services : } ``` +### AuthZ + +AuthZ consists in verifying if a given **user** has the right permission to execute a given **action** within a given **domain**. Some context-dependant information can be given as well. + +For AuthZ, the app exposes the following [gRPC](https://grpc.io/) services : + +- **Decide** : asks the authorization service if a user has the right permission + + ```json + { + "uuid": "96d99d44-e0a6-458e-a656-de2a400d60a9", + "domain": "user", + "action": "read", + "context": [ + { + "name": "owner", + "value": "96d99d44-e0a6-458e-a656-de2a400d60a8" + }, + { + "name": "role", + "value": "admin" + } + ] + } + ``` + + In return, the service gives an authorization response : + + ```json + { + "allow": true + } + ``` + + ## Messages Various RabbitMQ messages are sent for logging purpose. diff --git a/docker-compose.yml b/docker-compose.yml index 73217fe..eafe3e2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -51,6 +51,24 @@ services: aliases: - v3-auth-db-test + opa: + container_name: v3-opa + image: ${OPA_IMAGE} + ports: + - 8181:8181 + command: + - "run" + - "--server" + - "--log-format=json-pretty" + - "--set=decision_logs.console=true" + - "./policies/" + volumes: + - ./opa:/policies + networks: + v3-network: + aliases: + - v3-opa + networks: v3-network: name: v3-network diff --git a/opa/user/list.rego b/opa/user/list.rego new file mode 100644 index 0000000..4d92535 --- /dev/null +++ b/opa/user/list.rego @@ -0,0 +1,7 @@ +package user.list + +default allow := false + +allow := true { + input.role == "admin" +} diff --git a/opa/user/read.rego b/opa/user/read.rego new file mode 100644 index 0000000..16132e4 --- /dev/null +++ b/opa/user/read.rego @@ -0,0 +1,11 @@ +package user.read + +default allow := false + +allow := true { + input.uuid == input.owner +} + +allow := true { + input.role == "admin" +} diff --git a/package-lock.json b/package-lock.json index d242640..9130bf9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,13 @@ { - "name": "auth", + "name": "mobicoop-v3-auth", "version": "0.0.1", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "auth", + "name": "mobicoop-v3-auth", "version": "0.0.1", - "license": "UNLICENSED", + "license": "AGPL", "dependencies": { "@automapper/classes": "^8.7.7", "@automapper/core": "^8.7.7", @@ -15,6 +15,7 @@ "@golevelup/nestjs-rabbitmq": "^3.4.0", "@grpc/grpc-js": "^1.8.0", "@grpc/proto-loader": "^0.7.4", + "@nestjs/axios": "^1.0.1", "@nestjs/common": "^9.0.0", "@nestjs/config": "^2.2.0", "@nestjs/core": "^9.0.0", @@ -22,6 +23,7 @@ "@nestjs/microservices": "^9.2.1", "@nestjs/platform-express": "^9.0.0", "@prisma/client": "^4.7.1", + "axios": "^1.2.2", "bcrypt": "^5.1.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", @@ -1642,6 +1644,29 @@ "node-pre-gyp": "bin/node-pre-gyp" } }, + "node_modules/@nestjs/axios": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-1.0.1.tgz", + "integrity": "sha512-TpoZM/0ZJ9xiC04qkRDFod93LCZ12TQARRU3ejDvBK2E8emvzM4HThOs5ePklVxce4Q1ZsnrIWqnImvoDmJYnQ==", + "dependencies": { + "axios": "1.2.1" + }, + "peerDependencies": { + "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0", + "reflect-metadata": "^0.1.12", + "rxjs": "^6.0.0 || ^7.0.0" + } + }, + "node_modules/@nestjs/axios/node_modules/axios": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.2.1.tgz", + "integrity": "sha512-I88cFiGu9ryt/tfVEi4kX2SITsvDddTajXTOFmt2uK1ZVA8LytjtdeyefdQWEf5PU8w+4SSJDoYnggflB5tW4A==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/@nestjs/cli": { "version": "9.1.5", "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-9.1.5.tgz", @@ -3173,8 +3198,17 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.2.2.tgz", + "integrity": "sha512-bz/J4gS2S3I7mpN/YZfGFTqhXTYzRho8Ay38w2otuuDR322KzFIWm/4W2K6gIwvWaws5n+mnb7D1lN9uD+QH6Q==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } }, "node_modules/babel-jest": { "version": "28.1.3", @@ -3813,7 +3847,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -4015,7 +4048,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -4854,6 +4886,25 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/fork-ts-checker-webpack-plugin": { "version": "7.2.13", "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-7.2.13.tgz", @@ -4908,7 +4959,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -7548,6 +7598,11 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -10487,6 +10542,26 @@ "tar": "^6.1.11" } }, + "@nestjs/axios": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-1.0.1.tgz", + "integrity": "sha512-TpoZM/0ZJ9xiC04qkRDFod93LCZ12TQARRU3ejDvBK2E8emvzM4HThOs5ePklVxce4Q1ZsnrIWqnImvoDmJYnQ==", + "requires": { + "axios": "1.2.1" + }, + "dependencies": { + "axios": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.2.1.tgz", + "integrity": "sha512-I88cFiGu9ryt/tfVEi4kX2SITsvDddTajXTOFmt2uK1ZVA8LytjtdeyefdQWEf5PU8w+4SSJDoYnggflB5tW4A==", + "requires": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + } + } + }, "@nestjs/cli": { "version": "9.1.5", "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-9.1.5.tgz", @@ -11648,8 +11723,17 @@ "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "axios": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.2.2.tgz", + "integrity": "sha512-bz/J4gS2S3I7mpN/YZfGFTqhXTYzRho8Ay38w2otuuDR322KzFIWm/4W2K6gIwvWaws5n+mnb7D1lN9uD+QH6Q==", + "requires": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } }, "babel-jest": { "version": "28.1.3", @@ -12118,7 +12202,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "requires": { "delayed-stream": "~1.0.0" } @@ -12278,8 +12361,7 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, "delegates": { "version": "1.0.0", @@ -12926,6 +13008,11 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, + "follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" + }, "fork-ts-checker-webpack-plugin": { "version": "7.2.13", "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-7.2.13.tgz", @@ -12962,7 +13049,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -14917,6 +15003,11 @@ "ipaddr.js": "1.9.1" } }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", diff --git a/package.json b/package.json index bdcf790..5c02f7d 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@golevelup/nestjs-rabbitmq": "^3.4.0", "@grpc/grpc-js": "^1.8.0", "@grpc/proto-loader": "^0.7.4", + "@nestjs/axios": "^1.0.1", "@nestjs/common": "^9.0.0", "@nestjs/config": "^2.2.0", "@nestjs/core": "^9.0.0", @@ -39,6 +40,7 @@ "@nestjs/microservices": "^9.2.1", "@nestjs/platform-express": "^9.0.0", "@prisma/client": "^4.7.1", + "axios": "^1.2.2", "bcrypt": "^5.1.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", @@ -80,7 +82,11 @@ "json", "ts" ], - "modulePathIgnorePatterns": [".controller.ts",".module.ts","main.ts"], + "modulePathIgnorePatterns": [ + ".controller.ts", + ".module.ts", + "main.ts" + ], "rootDir": "src", "testRegex": ".*\\.spec\\.ts$", "transform": { diff --git a/src/app.module.ts b/src/app.module.ts index d7b2a9e..8afd292 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -2,13 +2,15 @@ import { classes } from '@automapper/classes'; import { AutomapperModule } from '@automapper/nestjs'; import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; -import { AuthModule } from './modules/auth/auth.module'; +import { AuthenticationModule } from './modules/authentication/authentication.module'; +import { AuthorizationModule } from './modules/authorization/authorization.module'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true }), AutomapperModule.forRoot({ strategyInitializer: classes() }), - AuthModule, + AuthenticationModule, + AuthorizationModule, ], controllers: [], providers: [], diff --git a/src/main.ts b/src/main.ts index 92a84a4..88dd40c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -9,11 +9,17 @@ async function bootstrap() { { transport: Transport.GRPC, options: { - package: 'auth', - protoPath: join( - __dirname, - 'modules/auth/adapters/primaries/auth.proto', - ), + package: ['authentication', 'authorization'], + protoPath: [ + join( + __dirname, + 'modules/authentication/adapters/primaries/authentication.proto', + ), + join( + __dirname, + 'modules/authorization/adapters/primaries/authorization.proto', + ), + ], url: process.env.SERVICE_URL + ':' + process.env.SERVICE_PORT, loader: { keepCase: true, enums: String }, }, diff --git a/src/modules/auth/adapters/secondaries/auth.repository.ts b/src/modules/auth/adapters/secondaries/auth.repository.ts deleted file mode 100644 index 251df9a..0000000 --- a/src/modules/auth/adapters/secondaries/auth.repository.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { AuthNZRepository } from '../../../database/src/domain/authnz-repository'; -import { Auth } from '../../domain/entities/auth'; - -@Injectable() -export class AuthRepository extends AuthNZRepository { - protected _model = 'auth'; -} diff --git a/src/modules/auth/commands/create-auth.command.ts b/src/modules/auth/commands/create-auth.command.ts deleted file mode 100644 index 1b54ebc..0000000 --- a/src/modules/auth/commands/create-auth.command.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { CreateAuthRequest } from '../domain/dtos/create-auth.request'; - -export class CreateAuthCommand { - readonly createAuthRequest: CreateAuthRequest; - - constructor(request: CreateAuthRequest) { - this.createAuthRequest = request; - } -} diff --git a/src/modules/auth/commands/delete-auth.command.ts b/src/modules/auth/commands/delete-auth.command.ts deleted file mode 100644 index fb3034f..0000000 --- a/src/modules/auth/commands/delete-auth.command.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { DeleteAuthRequest } from '../domain/dtos/delete-auth.request'; - -export class DeleteAuthCommand { - readonly deleteAuthRequest: DeleteAuthRequest; - - constructor(request: DeleteAuthRequest) { - this.deleteAuthRequest = request; - } -} diff --git a/src/modules/auth/tests/unit/create-auth.usecase.spec.ts b/src/modules/auth/tests/unit/create-auth.usecase.spec.ts deleted file mode 100644 index 6d521f3..0000000 --- a/src/modules/auth/tests/unit/create-auth.usecase.spec.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { classes } from '@automapper/classes'; -import { AutomapperModule } from '@automapper/nestjs'; -import { Test, TestingModule } from '@nestjs/testing'; -import { AuthRepository } from '../../adapters/secondaries/auth.repository'; -import { CreateAuthCommand } from '../../commands/create-auth.command'; -import { CreateAuthRequest } from '../../domain/dtos/create-auth.request'; -import { Auth } from '../../domain/entities/auth'; -import { CreateAuthUseCase } from '../../domain/usecases/create-auth.usecase'; -import * as bcrypt from 'bcrypt'; -import { UsernameRepository } from '../../adapters/secondaries/username.repository'; -import { Type } from '../../domain/dtos/type.enum'; -import { LoggingMessager } from '../../adapters/secondaries/logging.messager'; - -const newAuthRequest: CreateAuthRequest = new CreateAuthRequest(); -newAuthRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a91'; -newAuthRequest.username = 'john.doe@email.com'; -newAuthRequest.password = 'John123'; -newAuthRequest.type = Type.EMAIL; -const newAuthCommand: CreateAuthCommand = new CreateAuthCommand(newAuthRequest); - -const mockAuthRepository = { - create: jest - .fn() - .mockImplementationOnce(() => { - return Promise.resolve({ - uuid: newAuthRequest.uuid, - password: bcrypt.hashSync(newAuthRequest.password, 10), - }); - }) - .mockImplementation(() => { - throw new Error('Already exists'); - }), -}; - -const mockUsernameRepository = { - create: jest.fn().mockResolvedValue({ - uuid: newAuthRequest.uuid, - username: newAuthRequest.username, - type: newAuthRequest.type, - }), -}; - -const mockMessager = { - publish: jest.fn().mockImplementation(), -}; - -describe('CreateAuthUseCase', () => { - let createAuthUseCase: CreateAuthUseCase; - - beforeAll(async () => { - const module: TestingModule = await Test.createTestingModule({ - imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })], - providers: [ - { - provide: AuthRepository, - useValue: mockAuthRepository, - }, - { - provide: UsernameRepository, - useValue: mockUsernameRepository, - }, - { - provide: LoggingMessager, - useValue: mockMessager, - }, - CreateAuthUseCase, - ], - }).compile(); - - createAuthUseCase = module.get(CreateAuthUseCase); - }); - - it('should be defined', () => { - expect(createAuthUseCase).toBeDefined(); - }); - - describe('execute', () => { - it('should create an auth with an encrypted password', async () => { - const newAuth: Auth = await createAuthUseCase.execute(newAuthCommand); - - expect( - bcrypt.compareSync(newAuthRequest.password, newAuth.password), - ).toBeTruthy(); - }); - it('should throw an error if user already exists', async () => { - await expect( - createAuthUseCase.execute(newAuthCommand), - ).rejects.toBeInstanceOf(Error); - }); - }); -}); diff --git a/src/modules/auth/tests/unit/validate-auth.usecase.spec.ts b/src/modules/auth/tests/unit/validate-auth.usecase.spec.ts deleted file mode 100644 index f1bb4f6..0000000 --- a/src/modules/auth/tests/unit/validate-auth.usecase.spec.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { classes } from '@automapper/classes'; -import { AutomapperModule } from '@automapper/nestjs'; -import { Test, TestingModule } from '@nestjs/testing'; -import { AuthRepository } from '../../adapters/secondaries/auth.repository'; -import { Auth } from '../../domain/entities/auth'; -import * as bcrypt from 'bcrypt'; -import { ValidateAuthUseCase } from '../../domain/usecases/validate-auth.usecase'; -import { ValidateAuthQuery } from '../../queries/validate-auth.query'; -import { UsernameRepository } from '../../adapters/secondaries/username.repository'; -import { Type } from '../../domain/dtos/type.enum'; -import { NotFoundException, UnauthorizedException } from '@nestjs/common'; -import { DatabaseException } from '../../../database/src/exceptions/DatabaseException'; -import { ValidateAuthRequest } from '../../domain/dtos/validate-auth.request'; - -const mockAuthRepository = { - findOne: jest - .fn() - .mockImplementationOnce(() => ({ - uuid: 'bb281075-1b98-4456-89d6-c643d3044a91', - password: bcrypt.hashSync('John123', 10), - })) - .mockImplementationOnce(() => ({ - uuid: 'bb281075-1b98-4456-89d6-c643d3044a91', - password: bcrypt.hashSync('John123', 10), - })), -}; - -const mockUsernameRepository = { - findOne: jest - .fn() - .mockImplementationOnce(() => ({ - uuid: 'bb281075-1b98-4456-89d6-c643d3044a91', - username: 'john.doe@email.com', - type: Type.EMAIL, - })) - .mockImplementationOnce(() => { - throw new DatabaseException(); - }) - .mockImplementationOnce(() => ({ - uuid: 'bb281075-1b98-4456-89d6-c643d3044a91', - username: 'john.doe@email.com', - type: Type.EMAIL, - })), -}; - -describe('ValidateAuthUseCase', () => { - let validateAuthUseCase: ValidateAuthUseCase; - - beforeAll(async () => { - const module: TestingModule = await Test.createTestingModule({ - imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })], - providers: [ - { - provide: AuthRepository, - useValue: mockAuthRepository, - }, - { - provide: UsernameRepository, - useValue: mockUsernameRepository, - }, - ValidateAuthUseCase, - ], - }).compile(); - - validateAuthUseCase = module.get(ValidateAuthUseCase); - }); - - it('should be defined', () => { - expect(validateAuthUseCase).toBeDefined(); - }); - - describe('execute', () => { - it('should validate an auth and returns entity object', async () => { - const validateAuthRequest: ValidateAuthRequest = - new ValidateAuthRequest(); - validateAuthRequest.username = 'john.doe@email.com'; - validateAuthRequest.password = 'John123'; - const auth: Auth = await validateAuthUseCase.execute( - new ValidateAuthQuery( - validateAuthRequest.username, - validateAuthRequest.password, - ), - ); - - expect(auth.uuid).toBe('bb281075-1b98-4456-89d6-c643d3044a91'); - }); - - it('should not validate an auth with unknown username and returns not found exception', async () => { - await expect( - validateAuthUseCase.execute( - new ValidateAuthQuery('jane.doe@email.com', 'Jane123'), - ), - ).rejects.toBeInstanceOf(NotFoundException); - }); - - it('should not validate an auth with wrong password and returns unauthorized exception', async () => { - await expect( - validateAuthUseCase.execute( - new ValidateAuthQuery('john.doe@email.com', 'John1234'), - ), - ).rejects.toBeInstanceOf(UnauthorizedException); - }); - }); -}); diff --git a/src/modules/auth/adapters/primaries/auth-messager.controller.ts b/src/modules/authentication/adapters/primaries/authentication-messager.controller.ts similarity index 75% rename from src/modules/auth/adapters/primaries/auth-messager.controller.ts rename to src/modules/authentication/adapters/primaries/authentication-messager.controller.ts index 7331e7b..1f64072 100644 --- a/src/modules/auth/adapters/primaries/auth-messager.controller.ts +++ b/src/modules/authentication/adapters/primaries/authentication-messager.controller.ts @@ -4,17 +4,17 @@ 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 { DeleteAuthRequest } from '../../domain/dtos/delete-auth.request'; -import { DeleteAuthCommand } from '../../commands/delete-auth.command'; +import { DeleteAuthenticationRequest } from '../../domain/dtos/delete-authentication.request'; +import { DeleteAuthenticationCommand } from '../../commands/delete-authentication.command'; @Controller() -export class AuthMessagerController { +export class AuthenticationMessagerController { constructor(private readonly _commandBus: CommandBus) {} @RabbitSubscribe({ exchange: 'user', routingKey: 'update', - queue: 'auth-user-update', + queue: 'authentication-user-update', }) public async userUpdatedHandler(message: string) { const updatedUser = JSON.parse(message); @@ -42,13 +42,15 @@ export class AuthMessagerController { @RabbitSubscribe({ exchange: 'user', routingKey: 'delete', - queue: 'auth-user-delete', + queue: 'authentication-user-delete', }) public async userDeletedHandler(message: string) { const deletedUser = JSON.parse(message); if (!deletedUser.hasOwnProperty('uuid')) throw new Error(); - const deleteAuthRequest = new DeleteAuthRequest(); - deleteAuthRequest.uuid = deletedUser.uuid; - await this._commandBus.execute(new DeleteAuthCommand(deleteAuthRequest)); + const deleteAuthenticationRequest = new DeleteAuthenticationRequest(); + deleteAuthenticationRequest.uuid = deletedUser.uuid; + await this._commandBus.execute( + new DeleteAuthenticationCommand(deleteAuthenticationRequest), + ); } } diff --git a/src/modules/auth/adapters/primaries/auth.controller.ts b/src/modules/authentication/adapters/primaries/authentication.controller.ts similarity index 61% rename from src/modules/auth/adapters/primaries/auth.controller.ts rename to src/modules/authentication/adapters/primaries/authentication.controller.ts index 515b62c..5fc1901 100644 --- a/src/modules/auth/adapters/primaries/auth.controller.ts +++ b/src/modules/authentication/adapters/primaries/authentication.controller.ts @@ -5,23 +5,23 @@ import { CommandBus, QueryBus } from '@nestjs/cqrs'; import { GrpcMethod, RpcException } from '@nestjs/microservices'; import { DatabaseException } from 'src/modules/database/src/exceptions/DatabaseException'; import { AddUsernameCommand } from '../../commands/add-username.command'; -import { CreateAuthCommand } from '../../commands/create-auth.command'; -import { DeleteAuthCommand } from '../../commands/delete-auth.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 { CreateAuthRequest } from '../../domain/dtos/create-auth.request'; -import { DeleteAuthRequest } from '../../domain/dtos/delete-auth.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 { ValidateAuthRequest } from '../../domain/dtos/validate-auth.request'; -import { Auth } from '../../domain/entities/auth'; +import { ValidateAuthenticationRequest } from '../../domain/dtos/validate-authentication.request'; +import { Authentication } from '../../domain/entities/authentication'; import { Username } from '../../domain/entities/username'; -import { ValidateAuthQuery } from '../../queries/validate-auth.query'; -import { AuthPresenter } from './auth.presenter'; -import { RpcValidationPipe } from './rpc.validation-pipe'; +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( @@ -31,20 +31,26 @@ import { UsernamePresenter } from './username.presenter'; }), ) @Controller() -export class AuthController { +export class AuthenticationController { constructor( private readonly _commandBus: CommandBus, private readonly _queryBus: QueryBus, @InjectMapper() private readonly _mapper: Mapper, ) {} - @GrpcMethod('AuthService', 'Validate') - async validate(data: ValidateAuthRequest): Promise { + @GrpcMethod('AuthenticationService', 'Validate') + async validate( + data: ValidateAuthenticationRequest, + ): Promise { try { - const auth: Auth = await this._queryBus.execute( - new ValidateAuthQuery(data.username, data.password), + const authentication: Authentication = await this._queryBus.execute( + new ValidateAuthenticationQuery(data.username, data.password), + ); + return this._mapper.map( + authentication, + Authentication, + AuthenticationPresenter, ); - return this._mapper.map(auth, Auth, AuthPresenter); } catch (e) { throw new RpcException({ code: 7, @@ -53,13 +59,19 @@ export class AuthController { } } - @GrpcMethod('AuthService', 'Create') - async createUser(data: CreateAuthRequest): Promise { + @GrpcMethod('AuthenticationService', 'Create') + async createUser( + data: CreateAuthenticationRequest, + ): Promise { try { - const auth: Auth = await this._commandBus.execute( - new CreateAuthCommand(data), + const authentication: Authentication = await this._commandBus.execute( + new CreateAuthenticationCommand(data), + ); + return this._mapper.map( + authentication, + Authentication, + AuthenticationPresenter, ); - return this._mapper.map(auth, Auth, AuthPresenter); } catch (e) { if (e instanceof DatabaseException) { if (e.message.includes('Already exists')) { @@ -76,7 +88,7 @@ export class AuthController { } } - @GrpcMethod('AuthService', 'AddUsername') + @GrpcMethod('AuthenticationService', 'AddUsername') async addUsername(data: AddUsernameRequest): Promise { try { const username: Username = await this._commandBus.execute( @@ -100,7 +112,7 @@ export class AuthController { } } - @GrpcMethod('AuthService', 'UpdateUsername') + @GrpcMethod('AuthenticationService', 'UpdateUsername') async updateUsername( data: UpdateUsernameRequest, ): Promise { @@ -126,14 +138,20 @@ export class AuthController { } } - @GrpcMethod('AuthService', 'UpdatePassword') - async updatePassword(data: UpdatePasswordRequest): Promise { + @GrpcMethod('AuthenticationService', 'UpdatePassword') + async updatePassword( + data: UpdatePasswordRequest, + ): Promise { try { - const auth: Auth = await this._commandBus.execute( + const authentication: Authentication = await this._commandBus.execute( new UpdatePasswordCommand(data), ); - return this._mapper.map(auth, Auth, AuthPresenter); + return this._mapper.map( + authentication, + Authentication, + AuthenticationPresenter, + ); } catch (e) { throw new RpcException({ code: 7, @@ -142,7 +160,7 @@ export class AuthController { } } - @GrpcMethod('AuthService', 'DeleteUsername') + @GrpcMethod('AuthenticationService', 'DeleteUsername') async deleteUsername(data: DeleteUsernameRequest) { try { return await this._commandBus.execute(new DeleteUsernameCommand(data)); @@ -154,10 +172,12 @@ export class AuthController { } } - @GrpcMethod('AuthService', 'Delete') - async deleteAuth(data: DeleteAuthRequest) { + @GrpcMethod('AuthenticationService', 'Delete') + async deleteAuthentication(data: DeleteAuthenticationRequest) { try { - return await this._commandBus.execute(new DeleteAuthCommand(data)); + return await this._commandBus.execute( + new DeleteAuthenticationCommand(data), + ); } catch (e) { throw new RpcException({ code: 7, diff --git a/src/modules/auth/adapters/primaries/auth.presenter.ts b/src/modules/authentication/adapters/primaries/authentication.presenter.ts similarity index 66% rename from src/modules/auth/adapters/primaries/auth.presenter.ts rename to src/modules/authentication/adapters/primaries/authentication.presenter.ts index b780968..080c7eb 100644 --- a/src/modules/auth/adapters/primaries/auth.presenter.ts +++ b/src/modules/authentication/adapters/primaries/authentication.presenter.ts @@ -1,6 +1,6 @@ import { AutoMap } from '@automapper/classes'; -export class AuthPresenter { +export class AuthenticationPresenter { @AutoMap() uuid: string; } diff --git a/src/modules/auth/adapters/primaries/auth.proto b/src/modules/authentication/adapters/primaries/authentication.proto similarity index 72% rename from src/modules/auth/adapters/primaries/auth.proto rename to src/modules/authentication/adapters/primaries/authentication.proto index 587dd66..42c9afc 100644 --- a/src/modules/auth/adapters/primaries/auth.proto +++ b/src/modules/authentication/adapters/primaries/authentication.proto @@ -1,10 +1,10 @@ syntax = "proto3"; -package auth; +package authentication; -service AuthService { - rpc Validate(AuthByUsernamePassword) returns (Uuid); - rpc Create(Auth) returns (Uuid); +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); @@ -12,7 +12,7 @@ service AuthService { rpc Delete(Uuid) returns (Empty); } -message AuthByUsernamePassword { +message AuthenticationByUsernamePassword { string username = 1; string password = 2; } @@ -22,7 +22,7 @@ enum Type { PHONE = 1; } -message Auth { +message Authentication { string uuid = 1; string username = 2; string password = 3; diff --git a/src/modules/auth/adapters/primaries/username.presenter.ts b/src/modules/authentication/adapters/primaries/username.presenter.ts similarity index 100% rename from src/modules/auth/adapters/primaries/username.presenter.ts rename to src/modules/authentication/adapters/primaries/username.presenter.ts diff --git a/src/modules/auth/adapters/secondaries/auth.messager.ts b/src/modules/authentication/adapters/secondaries/authentication.messager.ts similarity index 87% rename from src/modules/auth/adapters/secondaries/auth.messager.ts rename to src/modules/authentication/adapters/secondaries/authentication.messager.ts index 7bf4e2e..7364946 100644 --- a/src/modules/auth/adapters/secondaries/auth.messager.ts +++ b/src/modules/authentication/adapters/secondaries/authentication.messager.ts @@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common'; import { IMessageBroker } from '../../domain/interfaces/message-broker'; @Injectable() -export class AuthMessager extends IMessageBroker { +export class AuthenticationMessager extends IMessageBroker { constructor(private readonly _amqpConnection: AmqpConnection) { super('auth'); } diff --git a/src/modules/authentication/adapters/secondaries/authentication.repository.ts b/src/modules/authentication/adapters/secondaries/authentication.repository.ts new file mode 100644 index 0000000..98d1e37 --- /dev/null +++ b/src/modules/authentication/adapters/secondaries/authentication.repository.ts @@ -0,0 +1,8 @@ +import { Injectable } from '@nestjs/common'; +import { AuthRepository } from '../../../database/src/domain/auth-repository'; +import { Authentication } from '../../domain/entities/authentication'; + +@Injectable() +export class AuthenticationRepository extends AuthRepository { + protected _model = 'auth'; +} diff --git a/src/modules/auth/adapters/secondaries/logging.messager.ts b/src/modules/authentication/adapters/secondaries/logging.messager.ts similarity index 100% rename from src/modules/auth/adapters/secondaries/logging.messager.ts rename to src/modules/authentication/adapters/secondaries/logging.messager.ts diff --git a/src/modules/auth/adapters/secondaries/username.repository.ts b/src/modules/authentication/adapters/secondaries/username.repository.ts similarity index 50% rename from src/modules/auth/adapters/secondaries/username.repository.ts rename to src/modules/authentication/adapters/secondaries/username.repository.ts index fe39acd..ec10d25 100644 --- a/src/modules/auth/adapters/secondaries/username.repository.ts +++ b/src/modules/authentication/adapters/secondaries/username.repository.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; -import { AuthNZRepository } from '../../../database/src/domain/authnz-repository'; +import { AuthRepository } from '../../../database/src/domain/auth-repository'; import { Username } from '../../domain/entities/username'; @Injectable() -export class UsernameRepository extends AuthNZRepository { +export class UsernameRepository extends AuthRepository { protected _model = 'username'; } diff --git a/src/modules/auth/auth.module.ts b/src/modules/authentication/authentication.module.ts similarity index 61% rename from src/modules/auth/auth.module.ts rename to src/modules/authentication/authentication.module.ts index cd9152a..2e5f3cb 100644 --- a/src/modules/auth/auth.module.ts +++ b/src/modules/authentication/authentication.module.ts @@ -1,20 +1,20 @@ import { Module } from '@nestjs/common'; import { CqrsModule } from '@nestjs/cqrs'; import { DatabaseModule } from '../database/database.module'; -import { AuthController } from './adapters/primaries/auth.controller'; -import { CreateAuthUseCase } from './domain/usecases/create-auth.usecase'; -import { ValidateAuthUseCase } from './domain/usecases/validate-auth.usecase'; -import { AuthProfile } from './mappers/auth.profile'; -import { AuthRepository } from './adapters/secondaries/auth.repository'; +import { AuthenticationController } from './adapters/primaries/authentication.controller'; +import { CreateAuthenticationUseCase } from './domain/usecases/create-authentication.usecase'; +import { ValidateAuthenticationUseCase } from './domain/usecases/validate-authentication.usecase'; +import { AuthenticationProfile } from './mappers/authentication.profile'; +import { AuthenticationRepository } from './adapters/secondaries/authentication.repository'; import { UpdateUsernameUseCase } from './domain/usecases/update-username.usecase'; import { UsernameProfile } from './mappers/username.profile'; import { AddUsernameUseCase } from './domain/usecases/add-username.usecase'; import { UpdatePasswordUseCase } from './domain/usecases/update-password.usecase'; import { DeleteUsernameUseCase } from './domain/usecases/delete-username.usecase'; -import { DeleteAuthUseCase } from './domain/usecases/delete-auth.usecase'; +import { DeleteAuthenticationUseCase } from './domain/usecases/delete-authentication.usecase'; import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq'; import { ConfigModule, ConfigService } from '@nestjs/config'; -import { AuthMessagerController } from './adapters/primaries/auth-messager.controller'; +import { AuthenticationMessagerController } from './adapters/primaries/authentication-messager.controller'; import { LoggingMessager } from './adapters/secondaries/logging.messager'; @Module({ @@ -41,20 +41,20 @@ import { LoggingMessager } from './adapters/secondaries/logging.messager'; inject: [ConfigService], }), ], - controllers: [AuthController, AuthMessagerController], + controllers: [AuthenticationController, AuthenticationMessagerController], providers: [ - AuthProfile, + AuthenticationProfile, UsernameProfile, - AuthRepository, + AuthenticationRepository, LoggingMessager, - ValidateAuthUseCase, - CreateAuthUseCase, + ValidateAuthenticationUseCase, + CreateAuthenticationUseCase, AddUsernameUseCase, UpdateUsernameUseCase, UpdatePasswordUseCase, DeleteUsernameUseCase, - DeleteAuthUseCase, + DeleteAuthenticationUseCase, ], exports: [], }) -export class AuthModule {} +export class AuthenticationModule {} diff --git a/src/modules/auth/commands/add-username.command.ts b/src/modules/authentication/commands/add-username.command.ts similarity index 100% rename from src/modules/auth/commands/add-username.command.ts rename to src/modules/authentication/commands/add-username.command.ts diff --git a/src/modules/authentication/commands/create-authentication.command.ts b/src/modules/authentication/commands/create-authentication.command.ts new file mode 100644 index 0000000..1d06deb --- /dev/null +++ b/src/modules/authentication/commands/create-authentication.command.ts @@ -0,0 +1,9 @@ +import { CreateAuthenticationRequest } from '../domain/dtos/create-authentication.request'; + +export class CreateAuthenticationCommand { + readonly createAuthenticationRequest: CreateAuthenticationRequest; + + constructor(request: CreateAuthenticationRequest) { + this.createAuthenticationRequest = request; + } +} diff --git a/src/modules/authentication/commands/delete-authentication.command.ts b/src/modules/authentication/commands/delete-authentication.command.ts new file mode 100644 index 0000000..bea3cd2 --- /dev/null +++ b/src/modules/authentication/commands/delete-authentication.command.ts @@ -0,0 +1,9 @@ +import { DeleteAuthenticationRequest } from '../domain/dtos/delete-authentication.request'; + +export class DeleteAuthenticationCommand { + readonly deleteAuthenticationRequest: DeleteAuthenticationRequest; + + constructor(request: DeleteAuthenticationRequest) { + this.deleteAuthenticationRequest = request; + } +} diff --git a/src/modules/auth/commands/delete-username.command.ts b/src/modules/authentication/commands/delete-username.command.ts similarity index 100% rename from src/modules/auth/commands/delete-username.command.ts rename to src/modules/authentication/commands/delete-username.command.ts diff --git a/src/modules/auth/commands/update-password.command.ts b/src/modules/authentication/commands/update-password.command.ts similarity index 100% rename from src/modules/auth/commands/update-password.command.ts rename to src/modules/authentication/commands/update-password.command.ts diff --git a/src/modules/auth/commands/update-username.command.ts b/src/modules/authentication/commands/update-username.command.ts similarity index 100% rename from src/modules/auth/commands/update-username.command.ts rename to src/modules/authentication/commands/update-username.command.ts diff --git a/src/modules/auth/domain/dtos/add-username.request.ts b/src/modules/authentication/domain/dtos/add-username.request.ts similarity index 100% rename from src/modules/auth/domain/dtos/add-username.request.ts rename to src/modules/authentication/domain/dtos/add-username.request.ts diff --git a/src/modules/auth/domain/dtos/create-auth.request.ts b/src/modules/authentication/domain/dtos/create-authentication.request.ts similarity index 90% rename from src/modules/auth/domain/dtos/create-auth.request.ts rename to src/modules/authentication/domain/dtos/create-authentication.request.ts index 91423cb..7ca6562 100644 --- a/src/modules/auth/domain/dtos/create-auth.request.ts +++ b/src/modules/authentication/domain/dtos/create-authentication.request.ts @@ -2,7 +2,7 @@ import { AutoMap } from '@automapper/classes'; import { IsEnum, IsNotEmpty, IsString } from 'class-validator'; import { Type } from './type.enum'; -export class CreateAuthRequest { +export class CreateAuthenticationRequest { @IsString() @IsNotEmpty() @AutoMap() diff --git a/src/modules/auth/domain/dtos/delete-auth.request.ts b/src/modules/authentication/domain/dtos/delete-authentication.request.ts similarity index 79% rename from src/modules/auth/domain/dtos/delete-auth.request.ts rename to src/modules/authentication/domain/dtos/delete-authentication.request.ts index ac92806..09ecb56 100644 --- a/src/modules/auth/domain/dtos/delete-auth.request.ts +++ b/src/modules/authentication/domain/dtos/delete-authentication.request.ts @@ -1,7 +1,7 @@ import { AutoMap } from '@automapper/classes'; import { IsNotEmpty, IsString } from 'class-validator'; -export class DeleteAuthRequest { +export class DeleteAuthenticationRequest { @IsString() @IsNotEmpty() @AutoMap() diff --git a/src/modules/auth/domain/dtos/delete-username.request.ts b/src/modules/authentication/domain/dtos/delete-username.request.ts similarity index 100% rename from src/modules/auth/domain/dtos/delete-username.request.ts rename to src/modules/authentication/domain/dtos/delete-username.request.ts diff --git a/src/modules/auth/domain/dtos/type.enum.ts b/src/modules/authentication/domain/dtos/type.enum.ts similarity index 100% rename from src/modules/auth/domain/dtos/type.enum.ts rename to src/modules/authentication/domain/dtos/type.enum.ts diff --git a/src/modules/auth/domain/dtos/update-password.request.ts b/src/modules/authentication/domain/dtos/update-password.request.ts similarity index 100% rename from src/modules/auth/domain/dtos/update-password.request.ts rename to src/modules/authentication/domain/dtos/update-password.request.ts diff --git a/src/modules/auth/domain/dtos/update-username.request.ts b/src/modules/authentication/domain/dtos/update-username.request.ts similarity index 100% rename from src/modules/auth/domain/dtos/update-username.request.ts rename to src/modules/authentication/domain/dtos/update-username.request.ts diff --git a/src/modules/auth/domain/dtos/validate-auth.request.ts b/src/modules/authentication/domain/dtos/validate-authentication.request.ts similarity index 78% rename from src/modules/auth/domain/dtos/validate-auth.request.ts rename to src/modules/authentication/domain/dtos/validate-authentication.request.ts index 4b80587..997d67e 100644 --- a/src/modules/auth/domain/dtos/validate-auth.request.ts +++ b/src/modules/authentication/domain/dtos/validate-authentication.request.ts @@ -1,6 +1,6 @@ import { IsNotEmpty, IsString } from 'class-validator'; -export class ValidateAuthRequest { +export class ValidateAuthenticationRequest { @IsString() @IsNotEmpty() username: string; diff --git a/src/modules/auth/domain/entities/auth.ts b/src/modules/authentication/domain/entities/authentication.ts similarity index 76% rename from src/modules/auth/domain/entities/auth.ts rename to src/modules/authentication/domain/entities/authentication.ts index 4bf327d..6f00964 100644 --- a/src/modules/auth/domain/entities/auth.ts +++ b/src/modules/authentication/domain/entities/authentication.ts @@ -1,6 +1,6 @@ import { AutoMap } from '@automapper/classes'; -export class Auth { +export class Authentication { @AutoMap() uuid: string; diff --git a/src/modules/auth/domain/entities/username.ts b/src/modules/authentication/domain/entities/username.ts similarity index 100% rename from src/modules/auth/domain/entities/username.ts rename to src/modules/authentication/domain/entities/username.ts diff --git a/src/modules/auth/domain/interfaces/message-broker.ts b/src/modules/authentication/domain/interfaces/message-broker.ts similarity index 100% rename from src/modules/auth/domain/interfaces/message-broker.ts rename to src/modules/authentication/domain/interfaces/message-broker.ts diff --git a/src/modules/auth/domain/usecases/add-username.usecase.ts b/src/modules/authentication/domain/usecases/add-username.usecase.ts similarity index 100% rename from src/modules/auth/domain/usecases/add-username.usecase.ts rename to src/modules/authentication/domain/usecases/add-username.usecase.ts diff --git a/src/modules/auth/domain/usecases/create-auth.usecase.ts b/src/modules/authentication/domain/usecases/create-authentication.usecase.ts similarity index 55% rename from src/modules/auth/domain/usecases/create-auth.usecase.ts rename to src/modules/authentication/domain/usecases/create-authentication.usecase.ts index a477ee4..7c83ff3 100644 --- a/src/modules/auth/domain/usecases/create-auth.usecase.ts +++ b/src/modules/authentication/domain/usecases/create-authentication.usecase.ts @@ -1,25 +1,25 @@ import { CommandHandler } from '@nestjs/cqrs'; -import { AuthRepository } from '../../adapters/secondaries/auth.repository'; -import { CreateAuthCommand } from '../../commands/create-auth.command'; -import { Auth } from '../entities/auth'; +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 { LoggingMessager } from '../../adapters/secondaries/logging.messager'; -@CommandHandler(CreateAuthCommand) -export class CreateAuthUseCase { +@CommandHandler(CreateAuthenticationCommand) +export class CreateAuthenticationUseCase { constructor( - private readonly _authRepository: AuthRepository, + private readonly _authenticationRepository: AuthenticationRepository, private readonly _usernameRepository: UsernameRepository, private readonly _loggingMessager: LoggingMessager, ) {} - async execute(command: CreateAuthCommand): Promise { - const { uuid, password, ...username } = command.createAuthRequest; + async execute(command: CreateAuthenticationCommand): Promise { + const { uuid, password, ...username } = command.createAuthenticationRequest; const hash = await bcrypt.hash(password, 10); try { - const auth = await this._authRepository.create({ + const auth = await this._authenticationRepository.create({ uuid, password: hash, }); diff --git a/src/modules/auth/domain/usecases/delete-auth.usecase.ts b/src/modules/authentication/domain/usecases/delete-authentication.usecase.ts similarity index 51% rename from src/modules/auth/domain/usecases/delete-auth.usecase.ts rename to src/modules/authentication/domain/usecases/delete-authentication.usecase.ts index 05f4a3c..791d05e 100644 --- a/src/modules/auth/domain/usecases/delete-auth.usecase.ts +++ b/src/modules/authentication/domain/usecases/delete-authentication.usecase.ts @@ -1,23 +1,25 @@ import { CommandHandler } from '@nestjs/cqrs'; -import { AuthRepository } from '../../adapters/secondaries/auth.repository'; +import { AuthenticationRepository } from '../../adapters/secondaries/authentication.repository'; import { LoggingMessager } from '../../adapters/secondaries/logging.messager'; import { UsernameRepository } from '../../adapters/secondaries/username.repository'; -import { DeleteAuthCommand } from '../../commands/delete-auth.command'; +import { DeleteAuthenticationCommand } from '../../commands/delete-authentication.command'; -@CommandHandler(DeleteAuthCommand) -export class DeleteAuthUseCase { +@CommandHandler(DeleteAuthenticationCommand) +export class DeleteAuthenticationUseCase { constructor( - private readonly _authRepository: AuthRepository, + private readonly _authenticationRepository: AuthenticationRepository, private readonly _usernameRepository: UsernameRepository, private readonly _loggingMessager: LoggingMessager, ) {} - async execute(command: DeleteAuthCommand) { + async execute(command: DeleteAuthenticationCommand) { try { await this._usernameRepository.deleteMany({ - uuid: command.deleteAuthRequest.uuid, + uuid: command.deleteAuthenticationRequest.uuid, }); - return await this._authRepository.delete(command.deleteAuthRequest.uuid); + return await this._authenticationRepository.delete( + command.deleteAuthenticationRequest.uuid, + ); } catch (error) { this._loggingMessager.publish( 'auth.delete.crit', diff --git a/src/modules/auth/domain/usecases/delete-username.usecase.ts b/src/modules/authentication/domain/usecases/delete-username.usecase.ts similarity index 100% rename from src/modules/auth/domain/usecases/delete-username.usecase.ts rename to src/modules/authentication/domain/usecases/delete-username.usecase.ts diff --git a/src/modules/auth/domain/usecases/update-password.usecase.ts b/src/modules/authentication/domain/usecases/update-password.usecase.ts similarity index 67% rename from src/modules/auth/domain/usecases/update-password.usecase.ts rename to src/modules/authentication/domain/usecases/update-password.usecase.ts index b8bf22d..0f6f8eb 100644 --- a/src/modules/auth/domain/usecases/update-password.usecase.ts +++ b/src/modules/authentication/domain/usecases/update-password.usecase.ts @@ -1,6 +1,6 @@ import { CommandHandler } from '@nestjs/cqrs'; -import { AuthRepository } from '../../adapters/secondaries/auth.repository'; -import { Auth } from '../entities/auth'; +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 { LoggingMessager } from '../../adapters/secondaries/logging.messager'; @@ -8,16 +8,16 @@ import { LoggingMessager } from '../../adapters/secondaries/logging.messager'; @CommandHandler(UpdatePasswordCommand) export class UpdatePasswordUseCase { constructor( - private readonly _authRepository: AuthRepository, + private readonly _authenticationRepository: AuthenticationRepository, private readonly _loggingMessager: LoggingMessager, ) {} - async execute(command: UpdatePasswordCommand): Promise { + async execute(command: UpdatePasswordCommand): Promise { const { uuid, password } = command.updatePasswordRequest; const hash = await bcrypt.hash(password, 10); try { - return await this._authRepository.update(uuid, { + return await this._authenticationRepository.update(uuid, { password: hash, }); } catch (error) { diff --git a/src/modules/auth/domain/usecases/update-username.usecase.ts b/src/modules/authentication/domain/usecases/update-username.usecase.ts similarity index 100% rename from src/modules/auth/domain/usecases/update-username.usecase.ts rename to src/modules/authentication/domain/usecases/update-username.usecase.ts diff --git a/src/modules/auth/domain/usecases/validate-auth.usecase.ts b/src/modules/authentication/domain/usecases/validate-authentication.usecase.ts similarity index 59% rename from src/modules/auth/domain/usecases/validate-auth.usecase.ts rename to src/modules/authentication/domain/usecases/validate-authentication.usecase.ts index fd70cbe..98bb3d5 100644 --- a/src/modules/auth/domain/usecases/validate-auth.usecase.ts +++ b/src/modules/authentication/domain/usecases/validate-authentication.usecase.ts @@ -1,20 +1,22 @@ import { QueryHandler } from '@nestjs/cqrs'; -import { AuthRepository } from '../../adapters/secondaries/auth.repository'; -import { ValidateAuthQuery } from '../../queries/validate-auth.query'; -import { Auth } from '../entities/auth'; +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(ValidateAuthQuery) -export class ValidateAuthUseCase { +@QueryHandler(ValidateAuthenticationQuery) +export class ValidateAuthenticationUseCase { constructor( - private readonly _authRepository: AuthRepository, + private readonly _authenticationRepository: AuthenticationRepository, private readonly _usernameRepository: UsernameRepository, ) {} - async execute(validate: ValidateAuthQuery): Promise { + async execute( + validate: ValidateAuthenticationQuery, + ): Promise { let username = new Username(); try { username = await this._usernameRepository.findOne({ @@ -24,7 +26,7 @@ export class ValidateAuthUseCase { throw new NotFoundException(); } try { - const auth = await this._authRepository.findOne({ + const auth = await this._authenticationRepository.findOne({ uuid: username.uuid, }); if (auth) { diff --git a/src/modules/authentication/mappers/authentication.profile.ts b/src/modules/authentication/mappers/authentication.profile.ts new file mode 100644 index 0000000..ab9156b --- /dev/null +++ b/src/modules/authentication/mappers/authentication.profile.ts @@ -0,0 +1,18 @@ +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); + }; + } +} diff --git a/src/modules/auth/mappers/username.profile.ts b/src/modules/authentication/mappers/username.profile.ts similarity index 100% rename from src/modules/auth/mappers/username.profile.ts rename to src/modules/authentication/mappers/username.profile.ts diff --git a/src/modules/auth/queries/validate-auth.query.ts b/src/modules/authentication/queries/validate-authentication.query.ts similarity index 80% rename from src/modules/auth/queries/validate-auth.query.ts rename to src/modules/authentication/queries/validate-authentication.query.ts index 2de952e..32bb41c 100644 --- a/src/modules/auth/queries/validate-auth.query.ts +++ b/src/modules/authentication/queries/validate-authentication.query.ts @@ -1,4 +1,4 @@ -export class ValidateAuthQuery { +export class ValidateAuthenticationQuery { readonly username: string; readonly password: string; diff --git a/src/modules/auth/tests/integration/auth.repository.spec.ts b/src/modules/authentication/tests/integration/authentication.repository.spec.ts similarity index 55% rename from src/modules/auth/tests/integration/auth.repository.spec.ts rename to src/modules/authentication/tests/integration/authentication.repository.spec.ts index 64d4f9f..ffd78ce 100644 --- a/src/modules/auth/tests/integration/auth.repository.spec.ts +++ b/src/modules/authentication/tests/integration/authentication.repository.spec.ts @@ -2,16 +2,16 @@ import { TestingModule, Test } from '@nestjs/testing'; import { DatabaseModule } from '../../../database/database.module'; import { PrismaService } from '../../../database/src/adapters/secondaries/prisma-service'; import { DatabaseException } from '../../../database/src/exceptions/DatabaseException'; -import { AuthRepository } from '../../adapters/secondaries/auth.repository'; +import { AuthenticationRepository } from '../../adapters/secondaries/authentication.repository'; import { v4 } from 'uuid'; import * as bcrypt from 'bcrypt'; -import { Auth } from '../../domain/entities/auth'; +import { Authentication } from '../../domain/entities/authentication'; -describe('AuthRepository', () => { +describe('AuthenticationRepository', () => { let prismaService: PrismaService; - let authRepository: AuthRepository; + let authenticationRepository: AuthenticationRepository; - const createAuths = async (nbToCreate = 10) => { + const createAuthentications = async (nbToCreate = 10) => { for (let i = 0; i < nbToCreate; i++) { await prismaService.auth.create({ data: { @@ -25,11 +25,13 @@ describe('AuthRepository', () => { beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ imports: [DatabaseModule], - providers: [AuthRepository, PrismaService], + providers: [AuthenticationRepository, PrismaService], }).compile(); prismaService = module.get(PrismaService); - authRepository = module.get(AuthRepository); + authenticationRepository = module.get( + AuthenticationRepository, + ); }); afterAll(async () => { @@ -42,7 +44,7 @@ describe('AuthRepository', () => { describe('findAll', () => { it('should return an empty data array', async () => { - const res = await authRepository.findAll(); + const res = await authenticationRepository.findAll(); expect(res).toEqual({ data: [], total: 0, @@ -50,22 +52,22 @@ describe('AuthRepository', () => { }); it('should return a data array with 8 auths', async () => { - await createAuths(8); - const auths = await authRepository.findAll(); + await createAuthentications(8); + const auths = await authenticationRepository.findAll(); expect(auths.data.length).toBe(8); expect(auths.total).toBe(8); }); - it('should return a data array limited to 10 auths', async () => { - await createAuths(20); - const auths = await authRepository.findAll(); + it('should return a data array limited to 10 authentications', async () => { + await createAuthentications(20); + const auths = await authenticationRepository.findAll(); expect(auths.data.length).toBe(10); expect(auths.total).toBe(20); }); }); describe('findOneByUuid', () => { - it('should return an auth', async () => { + it('should return an authentication', async () => { const authToFind = await prismaService.auth.create({ data: { uuid: v4(), @@ -73,12 +75,14 @@ describe('AuthRepository', () => { }, }); - const auth = await authRepository.findOneByUuid(authToFind.uuid); + const auth = await authenticationRepository.findOneByUuid( + authToFind.uuid, + ); expect(auth.uuid).toBe(authToFind.uuid); }); it('should return null', async () => { - const auth = await authRepository.findOneByUuid( + const auth = await authenticationRepository.findOneByUuid( '544572be-11fb-4244-8235-587221fc9104', ); expect(auth).toBeNull(); @@ -86,59 +90,64 @@ describe('AuthRepository', () => { }); describe('create', () => { - it('should create an auth', async () => { + it('should create an authentication', async () => { const beforeCount = await prismaService.auth.count(); - const authToCreate: Auth = new Auth(); - authToCreate.uuid = v4(); - authToCreate.password = bcrypt.hashSync(`password`, 10); - const auth = await authRepository.create(authToCreate); + const authenticationToCreate: Authentication = new Authentication(); + authenticationToCreate.uuid = v4(); + authenticationToCreate.password = bcrypt.hashSync(`password`, 10); + const authentication = await authenticationRepository.create( + authenticationToCreate, + ); const afterCount = await prismaService.auth.count(); expect(afterCount - beforeCount).toBe(1); - expect(auth.uuid).toBeDefined(); + expect(authentication.uuid).toBeDefined(); }); }); describe('update', () => { - it('should update auth password', async () => { - const authToUpdate = await prismaService.auth.create({ + it('should update authentication password', async () => { + const authenticationToUpdate = await prismaService.auth.create({ data: { uuid: v4(), password: bcrypt.hashSync(`password`, 10), }, }); - const toUpdate: Auth = new Auth(); + const toUpdate: Authentication = new Authentication(); toUpdate.password = bcrypt.hashSync(`newPassword`, 10); - const updatedAuth = await authRepository.update( - authToUpdate.uuid, + const updatedAuthentication = await authenticationRepository.update( + authenticationToUpdate.uuid, toUpdate, ); - expect(updatedAuth.uuid).toBe(authToUpdate.uuid); + expect(updatedAuthentication.uuid).toBe(authenticationToUpdate.uuid); }); it('should throw DatabaseException', async () => { - const toUpdate: Auth = new Auth(); + const toUpdate: Authentication = new Authentication(); toUpdate.password = bcrypt.hashSync(`newPassword`, 10); await expect( - authRepository.update('544572be-11fb-4244-8235-587221fc9104', toUpdate), + authenticationRepository.update( + '544572be-11fb-4244-8235-587221fc9104', + toUpdate, + ), ).rejects.toBeInstanceOf(DatabaseException); }); }); describe('delete', () => { - it('should delete an auth', async () => { - const authToRemove = await prismaService.auth.create({ + it('should delete an authentication', async () => { + const authenticationToRemove = await prismaService.auth.create({ data: { uuid: v4(), password: bcrypt.hashSync(`password`, 10), }, }); - await authRepository.delete(authToRemove.uuid); + await authenticationRepository.delete(authenticationToRemove.uuid); const count = await prismaService.auth.count(); expect(count).toBe(0); @@ -146,7 +155,7 @@ describe('AuthRepository', () => { it('should throw DatabaseException', async () => { await expect( - authRepository.delete('544572be-11fb-4244-8235-587221fc9104'), + authenticationRepository.delete('544572be-11fb-4244-8235-587221fc9104'), ).rejects.toBeInstanceOf(DatabaseException); }); }); diff --git a/src/modules/auth/tests/integration/username.repository.spec.ts b/src/modules/authentication/tests/integration/username.repository.spec.ts similarity index 100% rename from src/modules/auth/tests/integration/username.repository.spec.ts rename to src/modules/authentication/tests/integration/username.repository.spec.ts diff --git a/src/modules/auth/tests/unit/add-username.usecase.spec.ts b/src/modules/authentication/tests/unit/add-username.usecase.spec.ts similarity index 95% rename from src/modules/auth/tests/unit/add-username.usecase.spec.ts rename to src/modules/authentication/tests/unit/add-username.usecase.spec.ts index e6a3cba..a46f35e 100644 --- a/src/modules/auth/tests/unit/add-username.usecase.spec.ts +++ b/src/modules/authentication/tests/unit/add-username.usecase.spec.ts @@ -1,7 +1,7 @@ import { classes } from '@automapper/classes'; import { AutomapperModule } from '@automapper/nestjs'; import { Test, TestingModule } from '@nestjs/testing'; -import { AuthProfile } from '../../mappers/auth.profile'; +import { AuthenticationProfile } from '../../mappers/authentication.profile'; import { UsernameRepository } from '../../adapters/secondaries/username.repository'; import { Username } from '../../domain/entities/username'; import { Type } from '../../domain/dtos/type.enum'; @@ -50,7 +50,7 @@ describe('AddUsernameUseCase', () => { useValue: mockMessager, }, AddUsernameUseCase, - AuthProfile, + AuthenticationProfile, ], }).compile(); diff --git a/src/modules/authentication/tests/unit/create-authentication.usecase.spec.ts b/src/modules/authentication/tests/unit/create-authentication.usecase.spec.ts new file mode 100644 index 0000000..a223f69 --- /dev/null +++ b/src/modules/authentication/tests/unit/create-authentication.usecase.spec.ts @@ -0,0 +1,99 @@ +import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; +import { Test, TestingModule } from '@nestjs/testing'; +import { AuthenticationRepository } from '../../adapters/secondaries/authentication.repository'; +import { CreateAuthenticationCommand } from '../../commands/create-authentication.command'; +import { CreateAuthenticationRequest } from '../../domain/dtos/create-authentication.request'; +import { Authentication } from '../../domain/entities/authentication'; +import { CreateAuthenticationUseCase } from '../../domain/usecases/create-authentication.usecase'; +import * as bcrypt from 'bcrypt'; +import { UsernameRepository } from '../../adapters/secondaries/username.repository'; +import { Type } from '../../domain/dtos/type.enum'; +import { LoggingMessager } from '../../adapters/secondaries/logging.messager'; + +const newAuthenticationRequest: CreateAuthenticationRequest = + new CreateAuthenticationRequest(); +newAuthenticationRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a91'; +newAuthenticationRequest.username = 'john.doe@email.com'; +newAuthenticationRequest.password = 'John123'; +newAuthenticationRequest.type = Type.EMAIL; +const newAuthCommand: CreateAuthenticationCommand = + new CreateAuthenticationCommand(newAuthenticationRequest); + +const mockAuthenticationRepository = { + create: jest + .fn() + .mockImplementationOnce(() => { + return Promise.resolve({ + uuid: newAuthenticationRequest.uuid, + password: bcrypt.hashSync(newAuthenticationRequest.password, 10), + }); + }) + .mockImplementation(() => { + throw new Error('Already exists'); + }), +}; + +const mockUsernameRepository = { + create: jest.fn().mockResolvedValue({ + uuid: newAuthenticationRequest.uuid, + username: newAuthenticationRequest.username, + type: newAuthenticationRequest.type, + }), +}; + +const mockMessager = { + publish: jest.fn().mockImplementation(), +}; + +describe('CreateAuthenticationUseCase', () => { + let createAuthenticationUseCase: CreateAuthenticationUseCase; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })], + providers: [ + { + provide: AuthenticationRepository, + useValue: mockAuthenticationRepository, + }, + { + provide: UsernameRepository, + useValue: mockUsernameRepository, + }, + { + provide: LoggingMessager, + useValue: mockMessager, + }, + CreateAuthenticationUseCase, + ], + }).compile(); + + createAuthenticationUseCase = module.get( + CreateAuthenticationUseCase, + ); + }); + + it('should be defined', () => { + expect(createAuthenticationUseCase).toBeDefined(); + }); + + describe('execute', () => { + it('should create an authentication with an encrypted password', async () => { + const newAuthentication: Authentication = + await createAuthenticationUseCase.execute(newAuthCommand); + + expect( + bcrypt.compareSync( + newAuthenticationRequest.password, + newAuthentication.password, + ), + ).toBeTruthy(); + }); + it('should throw an error if user already exists', async () => { + await expect( + createAuthenticationUseCase.execute(newAuthCommand), + ).rejects.toBeInstanceOf(Error); + }); + }); +}); diff --git a/src/modules/auth/tests/unit/delete-auth.usecase.spec.ts b/src/modules/authentication/tests/unit/delete-authentication.usecase.spec.ts similarity index 53% rename from src/modules/auth/tests/unit/delete-auth.usecase.spec.ts rename to src/modules/authentication/tests/unit/delete-authentication.usecase.spec.ts index da0ce4b..dd81688 100644 --- a/src/modules/auth/tests/unit/delete-auth.usecase.spec.ts +++ b/src/modules/authentication/tests/unit/delete-authentication.usecase.spec.ts @@ -1,13 +1,13 @@ import { classes } from '@automapper/classes'; import { AutomapperModule } from '@automapper/nestjs'; import { Test, TestingModule } from '@nestjs/testing'; -import { AuthRepository } from '../../adapters/secondaries/auth.repository'; +import { AuthenticationRepository } from '../../adapters/secondaries/authentication.repository'; import { LoggingMessager } from '../../adapters/secondaries/logging.messager'; import { UsernameRepository } from '../../adapters/secondaries/username.repository'; -import { DeleteAuthCommand } from '../../commands/delete-auth.command'; -import { DeleteAuthRequest } from '../../domain/dtos/delete-auth.request'; +import { DeleteAuthenticationCommand } from '../../commands/delete-authentication.command'; +import { DeleteAuthenticationRequest } from '../../domain/dtos/delete-authentication.request'; import { Type } from '../../domain/dtos/type.enum'; -import { DeleteAuthUseCase } from '../../domain/usecases/delete-auth.usecase'; +import { DeleteAuthenticationUseCase } from '../../domain/usecases/delete-authentication.usecase'; const usernames = { data: [ @@ -25,13 +25,13 @@ const usernames = { total: 2, }; -const deleteAuthRequest: DeleteAuthRequest = new DeleteAuthRequest(); -deleteAuthRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a91'; -const deleteAuthCommand: DeleteAuthCommand = new DeleteAuthCommand( - deleteAuthRequest, -); +const deleteAuthenticationRequest: DeleteAuthenticationRequest = + new DeleteAuthenticationRequest(); +deleteAuthenticationRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a91'; +const deleteAuthenticationCommand: DeleteAuthenticationCommand = + new DeleteAuthenticationCommand(deleteAuthenticationRequest); -const mockAuthRepository = { +const mockAuthenticationRepository = { delete: jest .fn() .mockResolvedValueOnce(undefined) @@ -53,16 +53,16 @@ const mockMessager = { publish: jest.fn().mockImplementation(), }; -describe('DeleteAuthUseCase', () => { - let deleteAuthUseCase: DeleteAuthUseCase; +describe('DeleteAuthenticationUseCase', () => { + let deleteAuthenticationUseCase: DeleteAuthenticationUseCase; beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })], providers: [ { - provide: AuthRepository, - useValue: mockAuthRepository, + provide: AuthenticationRepository, + useValue: mockAuthenticationRepository, }, { provide: UsernameRepository, @@ -72,26 +72,30 @@ describe('DeleteAuthUseCase', () => { provide: LoggingMessager, useValue: mockMessager, }, - DeleteAuthUseCase, + DeleteAuthenticationUseCase, ], }).compile(); - deleteAuthUseCase = module.get(DeleteAuthUseCase); + deleteAuthenticationUseCase = module.get( + DeleteAuthenticationUseCase, + ); }); it('should be defined', () => { - expect(deleteAuthUseCase).toBeDefined(); + expect(deleteAuthenticationUseCase).toBeDefined(); }); describe('execute', () => { - it('should delete an auth and its usernames', async () => { - const deletedAuth = await deleteAuthUseCase.execute(deleteAuthCommand); + it('should delete an authentication and its usernames', async () => { + const deletedAuthentication = await deleteAuthenticationUseCase.execute( + deleteAuthenticationCommand, + ); - expect(deletedAuth).toBe(undefined); + expect(deletedAuthentication).toBe(undefined); }); - it('should throw an error if auth does not exist', async () => { + it('should throw an error if authentication does not exist', async () => { await expect( - deleteAuthUseCase.execute(deleteAuthCommand), + deleteAuthenticationUseCase.execute(deleteAuthenticationCommand), ).rejects.toBeInstanceOf(Error); }); }); diff --git a/src/modules/auth/tests/unit/delete-username.usecase.spec.ts b/src/modules/authentication/tests/unit/delete-username.usecase.spec.ts similarity index 100% rename from src/modules/auth/tests/unit/delete-username.usecase.spec.ts rename to src/modules/authentication/tests/unit/delete-username.usecase.spec.ts diff --git a/src/modules/auth/tests/unit/update-password.usecase.spec.ts b/src/modules/authentication/tests/unit/update-password.usecase.spec.ts similarity index 85% rename from src/modules/auth/tests/unit/update-password.usecase.spec.ts rename to src/modules/authentication/tests/unit/update-password.usecase.spec.ts index a7f4990..38ed6ac 100644 --- a/src/modules/auth/tests/unit/update-password.usecase.spec.ts +++ b/src/modules/authentication/tests/unit/update-password.usecase.spec.ts @@ -1,8 +1,8 @@ import { classes } from '@automapper/classes'; import { AutomapperModule } from '@automapper/nestjs'; import { Test, TestingModule } from '@nestjs/testing'; -import { AuthRepository } from '../../adapters/secondaries/auth.repository'; -import { Auth } from '../../domain/entities/auth'; +import { AuthenticationRepository } from '../../adapters/secondaries/authentication.repository'; +import { Authentication } from '../../domain/entities/authentication'; import * as bcrypt from 'bcrypt'; import { UpdatePasswordRequest } from '../../domain/dtos/update-password.request'; import { UpdatePasswordCommand } from '../../commands/update-password.command'; @@ -18,7 +18,7 @@ const updatePasswordCommand: UpdatePasswordCommand = new UpdatePasswordCommand( updatePasswordRequest, ); -const mockAuthRepository = { +const mockAuthenticationRepository = { update: jest .fn() .mockResolvedValueOnce({ @@ -42,8 +42,8 @@ describe('UpdatePasswordUseCase', () => { imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })], providers: [ { - provide: AuthRepository, - useValue: mockAuthRepository, + provide: AuthenticationRepository, + useValue: mockAuthenticationRepository, }, { provide: LoggingMessager, @@ -64,7 +64,7 @@ describe('UpdatePasswordUseCase', () => { describe('execute', () => { it('should update an auth with an new encrypted password', async () => { - const newAuth: Auth = await updatePasswordUseCase.execute( + const newAuth: Authentication = await updatePasswordUseCase.execute( updatePasswordCommand, ); diff --git a/src/modules/auth/tests/unit/update-username.usecase.spec.ts b/src/modules/authentication/tests/unit/update-username.usecase.spec.ts similarity index 87% rename from src/modules/auth/tests/unit/update-username.usecase.spec.ts rename to src/modules/authentication/tests/unit/update-username.usecase.spec.ts index 2fcc95b..084a6b6 100644 --- a/src/modules/auth/tests/unit/update-username.usecase.spec.ts +++ b/src/modules/authentication/tests/unit/update-username.usecase.spec.ts @@ -18,37 +18,37 @@ const existingUsername = { type: Type.EMAIL, }; +const newUsernameRequest: UpdateUsernameRequest = new UpdateUsernameRequest(); +newUsernameRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a90'; +newUsernameRequest.username = '+33611223344'; +newUsernameRequest.type = Type.PHONE; + const updateUsernameRequest: UpdateUsernameRequest = new UpdateUsernameRequest(); updateUsernameRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a91'; updateUsernameRequest.username = 'johnny.doe@email.com'; updateUsernameRequest.type = Type.EMAIL; -const newUsernameRequest: UpdateUsernameRequest = new UpdateUsernameRequest(); -newUsernameRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a91'; -newUsernameRequest.username = '+33611223344'; -newUsernameRequest.type = Type.PHONE; - -const invalidUpdateUsernameRequest: UpdateUsernameRequest = - new UpdateUsernameRequest(); -invalidUpdateUsernameRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a91'; -invalidUpdateUsernameRequest.username = ''; -invalidUpdateUsernameRequest.type = Type.EMAIL; - const unknownUsernameRequest: UpdateUsernameRequest = new UpdateUsernameRequest(); -unknownUsernameRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a91'; +unknownUsernameRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a92'; unknownUsernameRequest.username = 'unknown@email.com'; unknownUsernameRequest.type = Type.EMAIL; -const updateUsernameCommand: UpdateUsernameCommand = new UpdateUsernameCommand( - updateUsernameRequest, -); +const invalidUpdateUsernameRequest: UpdateUsernameRequest = + new UpdateUsernameRequest(); +invalidUpdateUsernameRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a93'; +invalidUpdateUsernameRequest.username = ''; +invalidUpdateUsernameRequest.type = Type.EMAIL; const newUsernameCommand: UpdateUsernameCommand = new UpdateUsernameCommand( newUsernameRequest, ); +const updateUsernameCommand: UpdateUsernameCommand = new UpdateUsernameCommand( + updateUsernameRequest, +); + const invalidUpdateUsernameCommand: UpdateUsernameCommand = new UpdateUsernameCommand(invalidUpdateUsernameRequest); @@ -56,21 +56,24 @@ const unknownUpdateUsernameCommand: UpdateUsernameCommand = new UpdateUsernameCommand(unknownUsernameRequest); const mockUsernameRepository = { - findOne: jest.fn().mockResolvedValue(existingUsername), - updateWhere: jest - .fn() - .mockImplementationOnce(() => { - return Promise.resolve(updateUsernameRequest); - }) - .mockImplementationOnce(() => { + findOne: jest.fn().mockImplementation((request) => { + if (request.uuid == 'bb281075-1b98-4456-89d6-c643d3044a90') { + return Promise.resolve(null); + } + return Promise.resolve(existingUsername); + }), + updateWhere: jest.fn().mockImplementation((request) => { + if (request.uuid_type.uuid == 'bb281075-1b98-4456-89d6-c643d3044a90') { return Promise.resolve(newUsernameRequest); - }) - .mockImplementationOnce(() => { + } + if (request.uuid_type.uuid == 'bb281075-1b98-4456-89d6-c643d3044a91') { + return Promise.resolve(updateUsernameRequest); + } + if (request.uuid_type.uuid == 'bb281075-1b98-4456-89d6-c643d3044a92') { throw new Error('Error'); - }) - .mockImplementationOnce(() => { - return Promise.resolve(invalidUpdateUsernameRequest); - }), + } + return Promise.resolve(invalidUpdateUsernameRequest); + }), }; const mockAddUsernameCommand = { @@ -115,15 +118,6 @@ describe('UpdateUsernameUseCase', () => { }); describe('execute', () => { - it('should update a username for email type', async () => { - const updatedUsername: Username = await updateUsernameUseCase.execute( - updateUsernameCommand, - ); - - expect(updatedUsername.username).toBe(updateUsernameRequest.username); - expect(updatedUsername.type).toBe(updateUsernameRequest.type); - }); - it('should create a new username', async () => { const newUsername: Username = await updateUsernameUseCase.execute( newUsernameCommand, @@ -133,6 +127,15 @@ describe('UpdateUsernameUseCase', () => { expect(newUsername.type).toBe(newUsernameRequest.type); }); + it('should update a username for email type', async () => { + const updatedUsername: Username = await updateUsernameUseCase.execute( + updateUsernameCommand, + ); + + expect(updatedUsername.username).toBe(updateUsernameRequest.username); + expect(updatedUsername.type).toBe(updateUsernameRequest.type); + }); + it('should throw an error if username does not exist', async () => { await expect( updateUsernameUseCase.execute(unknownUpdateUsernameCommand), diff --git a/src/modules/authentication/tests/unit/validate-authentication.usecase.spec.ts b/src/modules/authentication/tests/unit/validate-authentication.usecase.spec.ts new file mode 100644 index 0000000..ebe8f92 --- /dev/null +++ b/src/modules/authentication/tests/unit/validate-authentication.usecase.spec.ts @@ -0,0 +1,107 @@ +import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; +import { Test, TestingModule } from '@nestjs/testing'; +import { AuthenticationRepository } from '../../adapters/secondaries/authentication.repository'; +import { Authentication } from '../../domain/entities/authentication'; +import * as bcrypt from 'bcrypt'; +import { ValidateAuthenticationUseCase } from '../../domain/usecases/validate-authentication.usecase'; +import { ValidateAuthenticationQuery } from '../../queries/validate-authentication.query'; +import { UsernameRepository } from '../../adapters/secondaries/username.repository'; +import { Type } from '../../domain/dtos/type.enum'; +import { NotFoundException, UnauthorizedException } from '@nestjs/common'; +import { DatabaseException } from '../../../database/src/exceptions/DatabaseException'; +import { ValidateAuthenticationRequest } from '../../domain/dtos/validate-authentication.request'; + +const mockAuthenticationRepository = { + findOne: jest + .fn() + .mockImplementationOnce(() => ({ + uuid: 'bb281075-1b98-4456-89d6-c643d3044a91', + password: bcrypt.hashSync('John123', 10), + })) + .mockImplementationOnce(() => ({ + uuid: 'bb281075-1b98-4456-89d6-c643d3044a91', + password: bcrypt.hashSync('John123', 10), + })), +}; + +const mockUsernameRepository = { + findOne: jest + .fn() + .mockImplementationOnce(() => ({ + uuid: 'bb281075-1b98-4456-89d6-c643d3044a91', + username: 'john.doe@email.com', + type: Type.EMAIL, + })) + .mockImplementationOnce(() => { + throw new DatabaseException(); + }) + .mockImplementationOnce(() => ({ + uuid: 'bb281075-1b98-4456-89d6-c643d3044a91', + username: 'john.doe@email.com', + type: Type.EMAIL, + })), +}; + +describe('ValidateAuthenticationUseCase', () => { + let validateAuthenticationUseCase: ValidateAuthenticationUseCase; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })], + providers: [ + { + provide: AuthenticationRepository, + useValue: mockAuthenticationRepository, + }, + { + provide: UsernameRepository, + useValue: mockUsernameRepository, + }, + ValidateAuthenticationUseCase, + ], + }).compile(); + + validateAuthenticationUseCase = module.get( + ValidateAuthenticationUseCase, + ); + }); + + it('should be defined', () => { + expect(validateAuthenticationUseCase).toBeDefined(); + }); + + describe('execute', () => { + it('should validate an authentication and returns entity object', async () => { + const validateAuthenticationRequest: ValidateAuthenticationRequest = + new ValidateAuthenticationRequest(); + validateAuthenticationRequest.username = 'john.doe@email.com'; + validateAuthenticationRequest.password = 'John123'; + const authentication: Authentication = + await validateAuthenticationUseCase.execute( + new ValidateAuthenticationQuery( + validateAuthenticationRequest.username, + validateAuthenticationRequest.password, + ), + ); + + expect(authentication.uuid).toBe('bb281075-1b98-4456-89d6-c643d3044a91'); + }); + + it('should not validate an authentication with unknown username and returns not found exception', async () => { + await expect( + validateAuthenticationUseCase.execute( + new ValidateAuthenticationQuery('jane.doe@email.com', 'Jane123'), + ), + ).rejects.toBeInstanceOf(NotFoundException); + }); + + it('should not validate an authentication with wrong password and returns unauthorized exception', async () => { + await expect( + validateAuthenticationUseCase.execute( + new ValidateAuthenticationQuery('john.doe@email.com', 'John1234'), + ), + ).rejects.toBeInstanceOf(UnauthorizedException); + }); + }); +}); diff --git a/src/modules/authorization/adapters/primaries/authorization.controller.ts b/src/modules/authorization/adapters/primaries/authorization.controller.ts new file mode 100644 index 0000000..b975371 --- /dev/null +++ b/src/modules/authorization/adapters/primaries/authorization.controller.ts @@ -0,0 +1,43 @@ +import { Mapper } from '@automapper/core'; +import { InjectMapper } from '@automapper/nestjs'; +import { Controller, UsePipes } from '@nestjs/common'; +import { QueryBus } from '@nestjs/cqrs'; +import { GrpcMethod, RpcException } from '@nestjs/microservices'; +import { RpcValidationPipe } from 'src/utils/pipes/rpc.validation-pipe'; +import { DecisionRequest } from '../../domain/dtos/decision.request'; +import { Authorization } from '../../domain/entities/authorization'; +import { DecisionQuery } from '../../queries/decision.query'; +import { AuthorizationPresenter } from './authorization.presenter'; + +@UsePipes( + new RpcValidationPipe({ + whitelist: true, + forbidUnknownValues: false, + }), +) +@Controller() +export class AuthorizationController { + constructor( + private readonly _queryBus: QueryBus, + @InjectMapper() private readonly _mapper: Mapper, + ) {} + + @GrpcMethod('AuthorizationService', 'Decide') + async decide(data: DecisionRequest): Promise { + try { + const authorization: Authorization = await this._queryBus.execute( + new DecisionQuery(data.uuid, data.domain, data.action, data.context), + ); + return this._mapper.map( + authorization, + Authorization, + AuthorizationPresenter, + ); + } catch (e) { + throw new RpcException({ + code: 7, + message: 'Permission denied', + }); + } + } +} diff --git a/src/modules/authorization/adapters/primaries/authorization.presenter.ts b/src/modules/authorization/adapters/primaries/authorization.presenter.ts new file mode 100644 index 0000000..c6f3733 --- /dev/null +++ b/src/modules/authorization/adapters/primaries/authorization.presenter.ts @@ -0,0 +1,6 @@ +import { AutoMap } from '@automapper/classes'; + +export class AuthorizationPresenter { + @AutoMap() + allow: boolean; +} diff --git a/src/modules/authorization/adapters/primaries/authorization.proto b/src/modules/authorization/adapters/primaries/authorization.proto new file mode 100644 index 0000000..6e9fc61 --- /dev/null +++ b/src/modules/authorization/adapters/primaries/authorization.proto @@ -0,0 +1,35 @@ +syntax = "proto3"; + +package authorization; + +service AuthorizationService { + rpc Decide(AuthorizationRequest) returns (Decision); +} + +message AuthorizationRequest { + string uuid = 1; + Domain domain = 2; + Action action = 3; + repeated Item context = 4; +} + +enum Domain { + user = 0; +} + +enum Action { + create = 0; + read = 1; + update = 2; + delete = 3; + list = 4; +} + +message Item { + string name = 1; + string value = 2; +} + +message Decision { + bool allow = 1; +} diff --git a/src/modules/authorization/adapters/secondaries/decision-result.ts b/src/modules/authorization/adapters/secondaries/decision-result.ts new file mode 100644 index 0000000..547205b --- /dev/null +++ b/src/modules/authorization/adapters/secondaries/decision-result.ts @@ -0,0 +1,3 @@ +export class DecisionResult { + allow: boolean; +} diff --git a/src/modules/authorization/adapters/secondaries/decision.ts b/src/modules/authorization/adapters/secondaries/decision.ts new file mode 100644 index 0000000..ccc5e92 --- /dev/null +++ b/src/modules/authorization/adapters/secondaries/decision.ts @@ -0,0 +1,6 @@ +import { DecisionResult } from './decision-result'; + +export class Decision { + decision_id: string; + result: DecisionResult; +} diff --git a/src/modules/authorization/adapters/secondaries/opa.decision-maker.ts b/src/modules/authorization/adapters/secondaries/opa.decision-maker.ts new file mode 100644 index 0000000..0e9d5e8 --- /dev/null +++ b/src/modules/authorization/adapters/secondaries/opa.decision-maker.ts @@ -0,0 +1,48 @@ +import { HttpService } from '@nestjs/axios'; +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { lastValueFrom } from 'rxjs'; +import { Action } from '../../domain/dtos/action.enum'; +import { Domain } from '../../domain/dtos/domain.enum'; +import { IMakeDecision } from '../../domain/interfaces/decision-maker'; +import { ContextItem } from '../../domain/dtos/context-item'; +import { Decision } from './decision'; +import { Authorization } from '../../domain/entities/authorization'; + +@Injectable() +export class OpaDecisionMaker extends IMakeDecision { + constructor( + private readonly _configService: ConfigService, + private readonly _httpService: HttpService, + ) { + super(); + } + + async decide( + uuid: string, + domain: Domain, + action: Action, + context: Array, + ): Promise { + const reducedContext = context.reduce( + (obj, item) => Object.assign(obj, { [item.name]: item.value }), + {}, + ); + try { + const { data } = await lastValueFrom( + this._httpService.post( + this._configService.get('OPA_URL') + domain + '/' + action, + { + input: { + uuid, + ...reducedContext, + }, + }, + ), + ); + return new Authorization(data.result.allow); + } catch (e) { + return new Authorization(false); + } + } +} diff --git a/src/modules/authorization/authorization.module.ts b/src/modules/authorization/authorization.module.ts new file mode 100644 index 0000000..c66d76d --- /dev/null +++ b/src/modules/authorization/authorization.module.ts @@ -0,0 +1,16 @@ +import { HttpModule } from '@nestjs/axios'; +import { Module } from '@nestjs/common'; +import { CqrsModule } from '@nestjs/cqrs'; +import { DatabaseModule } from '../database/database.module'; +import { AuthorizationController } from './adapters/primaries/authorization.controller'; +import { OpaDecisionMaker } from './adapters/secondaries/opa.decision-maker'; +import { DecisionUseCase } from './domain/usecases/decision.usecase'; +import { AuthorizationProfile } from './mappers/authorization.profile'; + +@Module({ + imports: [DatabaseModule, CqrsModule, HttpModule], + exports: [], + controllers: [AuthorizationController], + providers: [OpaDecisionMaker, DecisionUseCase, AuthorizationProfile], +}) +export class AuthorizationModule {} diff --git a/src/modules/authorization/domain/dtos/action.enum.ts b/src/modules/authorization/domain/dtos/action.enum.ts new file mode 100644 index 0000000..4a3955b --- /dev/null +++ b/src/modules/authorization/domain/dtos/action.enum.ts @@ -0,0 +1,7 @@ +export enum Action { + create = 'create', + read = 'read', + update = 'update', + delete = 'delete', + list = 'list', +} diff --git a/src/modules/authorization/domain/dtos/context-item.ts b/src/modules/authorization/domain/dtos/context-item.ts new file mode 100644 index 0000000..b9b95dc --- /dev/null +++ b/src/modules/authorization/domain/dtos/context-item.ts @@ -0,0 +1,9 @@ +export class ContextItem { + name: string; + value: any; + + constructor(name: string, value: any) { + this.name = name; + this.value = value; + } +} diff --git a/src/modules/authorization/domain/dtos/decision.request.ts b/src/modules/authorization/domain/dtos/decision.request.ts new file mode 100644 index 0000000..2dfa6ea --- /dev/null +++ b/src/modules/authorization/domain/dtos/decision.request.ts @@ -0,0 +1,21 @@ +import { IsArray, IsNotEmpty, IsString } from 'class-validator'; +import { ContextItem } from './context-item'; +import { Action } from './action.enum'; +import { Domain } from './domain.enum'; + +export class DecisionRequest { + @IsString() + @IsNotEmpty() + uuid: string; + + @IsString() + @IsNotEmpty() + domain: Domain; + + @IsString() + @IsNotEmpty() + action: Action; + + @IsArray() + context?: Array; +} diff --git a/src/modules/authorization/domain/dtos/domain.enum.ts b/src/modules/authorization/domain/dtos/domain.enum.ts new file mode 100644 index 0000000..6cb9b92 --- /dev/null +++ b/src/modules/authorization/domain/dtos/domain.enum.ts @@ -0,0 +1,3 @@ +export enum Domain { + user = 'user', +} diff --git a/src/modules/authorization/domain/entities/authorization.ts b/src/modules/authorization/domain/entities/authorization.ts new file mode 100644 index 0000000..3869a3b --- /dev/null +++ b/src/modules/authorization/domain/entities/authorization.ts @@ -0,0 +1,10 @@ +import { AutoMap } from '@automapper/classes'; + +export class Authorization { + @AutoMap() + allow: boolean; + + constructor(allow: boolean) { + this.allow = allow; + } +} diff --git a/src/modules/authorization/domain/interfaces/decision-maker.ts b/src/modules/authorization/domain/interfaces/decision-maker.ts new file mode 100644 index 0000000..7a6139c --- /dev/null +++ b/src/modules/authorization/domain/interfaces/decision-maker.ts @@ -0,0 +1,14 @@ +import { Injectable } from '@nestjs/common'; +import { Action } from '../dtos/action.enum'; +import { Domain } from '../dtos/domain.enum'; +import { Authorization } from '../entities/authorization'; + +@Injectable() +export abstract class IMakeDecision { + abstract decide( + uuid: string, + domain: Domain, + action: Action, + context: Array<{ name: string; value: string }>, + ): Promise; +} diff --git a/src/modules/authorization/domain/usecases/decision.usecase.ts b/src/modules/authorization/domain/usecases/decision.usecase.ts new file mode 100644 index 0000000..55e5830 --- /dev/null +++ b/src/modules/authorization/domain/usecases/decision.usecase.ts @@ -0,0 +1,18 @@ +import { QueryHandler } from '@nestjs/cqrs'; +import { OpaDecisionMaker } from '../../adapters/secondaries/opa.decision-maker'; +import { DecisionQuery } from '../../queries/decision.query'; +import { Authorization } from '../entities/authorization'; + +@QueryHandler(DecisionQuery) +export class DecisionUseCase { + constructor(private readonly _decisionMaker: OpaDecisionMaker) {} + + async execute(decisionQuery: DecisionQuery): Promise { + return this._decisionMaker.decide( + decisionQuery.uuid, + decisionQuery.domain, + decisionQuery.action, + decisionQuery.context, + ); + } +} diff --git a/src/modules/auth/mappers/auth.profile.ts b/src/modules/authorization/mappers/authorization.profile.ts similarity index 53% rename from src/modules/auth/mappers/auth.profile.ts rename to src/modules/authorization/mappers/authorization.profile.ts index 2ceaf03..db4419d 100644 --- a/src/modules/auth/mappers/auth.profile.ts +++ b/src/modules/authorization/mappers/authorization.profile.ts @@ -1,18 +1,18 @@ import { createMap, Mapper } from '@automapper/core'; import { AutomapperProfile, InjectMapper } from '@automapper/nestjs'; import { Injectable } from '@nestjs/common'; -import { AuthPresenter } from '../adapters/primaries/auth.presenter'; -import { Auth } from '../domain/entities/auth'; +import { AuthorizationPresenter } from '../adapters/primaries/authorization.presenter'; +import { Authorization } from '../domain/entities/authorization'; @Injectable() -export class AuthProfile extends AutomapperProfile { +export class AuthorizationProfile extends AutomapperProfile { constructor(@InjectMapper() mapper: Mapper) { super(mapper); } override get profile() { return (mapper: any) => { - createMap(mapper, Auth, AuthPresenter); + createMap(mapper, Authorization, AuthorizationPresenter); }; } } diff --git a/src/modules/authorization/queries/decision.query.ts b/src/modules/authorization/queries/decision.query.ts new file mode 100644 index 0000000..7110acf --- /dev/null +++ b/src/modules/authorization/queries/decision.query.ts @@ -0,0 +1,22 @@ +import { ContextItem } from '../domain/dtos/context-item'; +import { Action } from '../domain/dtos/action.enum'; +import { Domain } from '../domain/dtos/domain.enum'; + +export class DecisionQuery { + readonly uuid: string; + readonly domain: Domain; + readonly action: Action; + readonly context: Array; + + constructor( + uuid: string, + domain: Domain, + action: Action, + context?: Array, + ) { + this.uuid = uuid; + this.domain = domain; + this.action = action; + this.context = context; + } +} diff --git a/src/modules/authorization/tests/unit/decision.usecase.spec.ts b/src/modules/authorization/tests/unit/decision.usecase.spec.ts new file mode 100644 index 0000000..ce6cc23 --- /dev/null +++ b/src/modules/authorization/tests/unit/decision.usecase.spec.ts @@ -0,0 +1,59 @@ +import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; +import { Test, TestingModule } from '@nestjs/testing'; +import { OpaDecisionMaker } from '../../adapters/secondaries/opa.decision-maker'; +import { Action } from '../../domain/dtos/action.enum'; +import { ContextItem } from '../../domain/dtos/context-item'; +import { DecisionRequest } from '../../domain/dtos/decision.request'; +import { Domain } from '../../domain/dtos/domain.enum'; +import { DecisionUseCase } from '../../domain/usecases/decision.usecase'; +import { AuthorizationProfile } from '../../mappers/authorization.profile'; +import { DecisionQuery } from '../../queries/decision.query'; + +const mockOpaDecisionMaker = { + decide: jest.fn().mockResolvedValue(Promise.resolve(true)), +}; + +describe('DecisionUseCase', () => { + let decisionUseCase: DecisionUseCase; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })], + providers: [ + { + provide: OpaDecisionMaker, + useValue: mockOpaDecisionMaker, + }, + DecisionUseCase, + AuthorizationProfile, + ], + }).compile(); + + decisionUseCase = module.get(DecisionUseCase); + }); + + it('should be defined', () => { + expect(decisionUseCase).toBeDefined(); + }); + + describe('execute', () => { + it('should validate an authorization', async () => { + const decisionRequest: DecisionRequest = new DecisionRequest(); + decisionRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a91'; + decisionRequest.domain = Domain.user; + decisionRequest.action = Action.create; + decisionRequest.context = [new ContextItem('context1', 'value1')]; + expect( + decisionUseCase.execute( + new DecisionQuery( + decisionRequest.uuid, + decisionRequest.domain, + decisionRequest.action, + decisionRequest.context, + ), + ), + ).toBeTruthy(); + }); + }); +}); diff --git a/src/modules/authorization/tests/unit/opa.decision-maker.spec.ts b/src/modules/authorization/tests/unit/opa.decision-maker.spec.ts new file mode 100644 index 0000000..52c85e2 --- /dev/null +++ b/src/modules/authorization/tests/unit/opa.decision-maker.spec.ts @@ -0,0 +1,100 @@ +import { HttpService } from '@nestjs/axios'; +import { ConfigService } from '@nestjs/config'; +import { Test, TestingModule } from '@nestjs/testing'; +import { of } from 'rxjs'; +import { OpaDecisionMaker } from '../../adapters/secondaries/opa.decision-maker'; +import { Action } from '../../domain/dtos/action.enum'; +import { Domain } from '../../domain/dtos/domain.enum'; + +const mockHttpService = { + post: jest + .fn() + .mockImplementationOnce(() => { + return of({ + status: 200, + data: { + decision_id: '96d99d44-e0a6-458e-a656-de2a400d60a8', + result: { + allow: true, + }, + }, + }); + }) + .mockImplementationOnce(() => { + return of({ + status: 200, + data: { + decision_id: '96d99d44-e0a6-458e-a656-de2a400d60a9', + result: { + allow: false, + }, + }, + }); + }) + .mockImplementationOnce(() => { + throw new Error(); + }), +}; + +const mockConfigService = { + get: jest.fn().mockResolvedValue({ + OPA_URL: 'http://url/', + }), +}; + +describe('OpaDecisionMaker', () => { + let opaDecisionMaker: OpaDecisionMaker; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [], + providers: [ + { + provide: HttpService, + useValue: mockHttpService, + }, + { + provide: ConfigService, + useValue: mockConfigService, + }, + OpaDecisionMaker, + ], + }).compile(); + + opaDecisionMaker = module.get(OpaDecisionMaker); + }); + + it('should be defined', () => { + expect(opaDecisionMaker).toBeDefined(); + }); + + describe('execute', () => { + it('should return a truthy authorization', async () => { + const authorization = await opaDecisionMaker.decide( + 'bb281075-1b98-4456-89d6-c643d3044a91', + Domain.user, + Action.read, + [], + ); + expect(authorization.allow).toBeTruthy(); + }); + it('should return a falsy authorization', async () => { + const authorization = await opaDecisionMaker.decide( + 'bb281075-1b98-4456-89d6-c643d3044a91', + Domain.user, + Action.read, + [], + ); + expect(authorization.allow).toBeFalsy(); + }); + it('should return a falsy authorization when an error happens', async () => { + const authorization = await opaDecisionMaker.decide( + 'bb281075-1b98-4456-89d6-c643d3044a91', + Domain.user, + Action.read, + [], + ); + expect(authorization.allow).toBeFalsy(); + }); + }); +}); diff --git a/src/modules/database/database.module.ts b/src/modules/database/database.module.ts index ab31158..12fcc32 100644 --- a/src/modules/database/database.module.ts +++ b/src/modules/database/database.module.ts @@ -1,10 +1,10 @@ import { Module } from '@nestjs/common'; -import { AuthRepository } from '../auth/adapters/secondaries/auth.repository'; -import { UsernameRepository } from '../auth/adapters/secondaries/username.repository'; +import { AuthenticationRepository } from '../authentication/adapters/secondaries/authentication.repository'; +import { UsernameRepository } from '../authentication/adapters/secondaries/username.repository'; import { PrismaService } from './src/adapters/secondaries/prisma-service'; @Module({ - providers: [PrismaService, AuthRepository, UsernameRepository], - exports: [PrismaService, AuthRepository, UsernameRepository], + providers: [PrismaService, AuthenticationRepository, UsernameRepository], + exports: [PrismaService, AuthenticationRepository, UsernameRepository], }) export class DatabaseModule {} diff --git a/src/modules/database/src/domain/authnz-repository.ts b/src/modules/database/src/domain/auth-repository.ts similarity index 57% rename from src/modules/database/src/domain/authnz-repository.ts rename to src/modules/database/src/domain/auth-repository.ts index fbb71f7..e20e282 100644 --- a/src/modules/database/src/domain/authnz-repository.ts +++ b/src/modules/database/src/domain/auth-repository.ts @@ -1,3 +1,3 @@ import { PrismaRepository } from '../adapters/secondaries/prisma-repository.abstract'; -export class AuthNZRepository extends PrismaRepository {} +export class AuthRepository extends PrismaRepository {} diff --git a/src/modules/auth/adapters/primaries/rpc.validation-pipe.ts b/src/utils/pipes/rpc.validation-pipe.ts similarity index 100% rename from src/modules/auth/adapters/primaries/rpc.validation-pipe.ts rename to src/utils/pipes/rpc.validation-pipe.ts