Merge branch 'opa' into 'main'
Opa See merge request v3/services/auth!6
This commit is contained in:
commit
d75ab7f82a
|
@ -10,3 +10,7 @@ RMQ_URI=amqp://v3-broker:5672
|
||||||
|
|
||||||
# POSTGRES
|
# POSTGRES
|
||||||
POSTGRES_IMAGE=postgres:15.0
|
POSTGRES_IMAGE=postgres:15.0
|
||||||
|
|
||||||
|
# OPA
|
||||||
|
OPA_IMAGE=openpolicyagent/opa:0.48.0-rootless
|
||||||
|
OPA_URL=http://v3-opa:8181/v1/data/
|
||||||
|
|
|
@ -10,3 +10,7 @@ RMQ_URI=amqp://v3-broker:5672
|
||||||
|
|
||||||
# POSTGRES
|
# POSTGRES
|
||||||
POSTGRES_IMAGE=postgres:15.0
|
POSTGRES_IMAGE=postgres:15.0
|
||||||
|
|
||||||
|
# OPA
|
||||||
|
OPA_IMAGE=openpolicyagent/opa:0.48.0-rootless
|
||||||
|
OPA_URL=http://v3-opa:8181/v1/data/
|
||||||
|
|
|
@ -14,6 +14,7 @@ COPY --chown=node:node package*.json ./
|
||||||
|
|
||||||
# Install app dependencies using the `npm ci` command instead of `npm install`
|
# Install app dependencies using the `npm ci` command instead of `npm install`
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
|
RUN npx prisma generate
|
||||||
|
|
||||||
# Bundle app source
|
# Bundle app source
|
||||||
COPY --chown=node:node . .
|
COPY --chown=node:node . .
|
||||||
|
|
39
README.md
39
README.md
|
@ -46,7 +46,9 @@ npm run migrate
|
||||||
|
|
||||||
## Usage
|
## 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 :
|
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
|
## Messages
|
||||||
|
|
||||||
Various RabbitMQ messages are sent for logging purpose.
|
Various RabbitMQ messages are sent for logging purpose.
|
||||||
|
|
|
@ -51,6 +51,24 @@ services:
|
||||||
aliases:
|
aliases:
|
||||||
- v3-auth-db-test
|
- 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:
|
networks:
|
||||||
v3-network:
|
v3-network:
|
||||||
name: v3-network
|
name: v3-network
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
package user.list
|
||||||
|
|
||||||
|
default allow := false
|
||||||
|
|
||||||
|
allow := true {
|
||||||
|
input.role == "admin"
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package user.read
|
||||||
|
|
||||||
|
default allow := false
|
||||||
|
|
||||||
|
allow := true {
|
||||||
|
input.uuid == input.owner
|
||||||
|
}
|
||||||
|
|
||||||
|
allow := true {
|
||||||
|
input.role == "admin"
|
||||||
|
}
|
|
@ -1,13 +1,13 @@
|
||||||
{
|
{
|
||||||
"name": "auth",
|
"name": "mobicoop-v3-auth",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "auth",
|
"name": "mobicoop-v3-auth",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"license": "UNLICENSED",
|
"license": "AGPL",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@automapper/classes": "^8.7.7",
|
"@automapper/classes": "^8.7.7",
|
||||||
"@automapper/core": "^8.7.7",
|
"@automapper/core": "^8.7.7",
|
||||||
|
@ -15,6 +15,7 @@
|
||||||
"@golevelup/nestjs-rabbitmq": "^3.4.0",
|
"@golevelup/nestjs-rabbitmq": "^3.4.0",
|
||||||
"@grpc/grpc-js": "^1.8.0",
|
"@grpc/grpc-js": "^1.8.0",
|
||||||
"@grpc/proto-loader": "^0.7.4",
|
"@grpc/proto-loader": "^0.7.4",
|
||||||
|
"@nestjs/axios": "^1.0.1",
|
||||||
"@nestjs/common": "^9.0.0",
|
"@nestjs/common": "^9.0.0",
|
||||||
"@nestjs/config": "^2.2.0",
|
"@nestjs/config": "^2.2.0",
|
||||||
"@nestjs/core": "^9.0.0",
|
"@nestjs/core": "^9.0.0",
|
||||||
|
@ -22,6 +23,7 @@
|
||||||
"@nestjs/microservices": "^9.2.1",
|
"@nestjs/microservices": "^9.2.1",
|
||||||
"@nestjs/platform-express": "^9.0.0",
|
"@nestjs/platform-express": "^9.0.0",
|
||||||
"@prisma/client": "^4.7.1",
|
"@prisma/client": "^4.7.1",
|
||||||
|
"axios": "^1.2.2",
|
||||||
"bcrypt": "^5.1.0",
|
"bcrypt": "^5.1.0",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.0",
|
"class-validator": "^0.14.0",
|
||||||
|
@ -1642,6 +1644,29 @@
|
||||||
"node-pre-gyp": "bin/node-pre-gyp"
|
"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": {
|
"node_modules/@nestjs/cli": {
|
||||||
"version": "9.1.5",
|
"version": "9.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-9.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-9.1.5.tgz",
|
||||||
|
@ -3173,8 +3198,17 @@
|
||||||
"node_modules/asynckit": {
|
"node_modules/asynckit": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||||
"dev": true
|
},
|
||||||
|
"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": {
|
"node_modules/babel-jest": {
|
||||||
"version": "28.1.3",
|
"version": "28.1.3",
|
||||||
|
@ -3813,7 +3847,6 @@
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"delayed-stream": "~1.0.0"
|
"delayed-stream": "~1.0.0"
|
||||||
},
|
},
|
||||||
|
@ -4015,7 +4048,6 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
|
@ -4854,6 +4886,25 @@
|
||||||
"integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
|
"integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/fork-ts-checker-webpack-plugin": {
|
||||||
"version": "7.2.13",
|
"version": "7.2.13",
|
||||||
"resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-7.2.13.tgz",
|
"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",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"asynckit": "^0.4.0",
|
"asynckit": "^0.4.0",
|
||||||
"combined-stream": "^1.0.8",
|
"combined-stream": "^1.0.8",
|
||||||
|
@ -7548,6 +7598,11 @@
|
||||||
"node": ">= 0.10"
|
"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": {
|
"node_modules/pump": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
||||||
|
@ -10487,6 +10542,26 @@
|
||||||
"tar": "^6.1.11"
|
"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": {
|
"@nestjs/cli": {
|
||||||
"version": "9.1.5",
|
"version": "9.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-9.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-9.1.5.tgz",
|
||||||
|
@ -11648,8 +11723,17 @@
|
||||||
"asynckit": {
|
"asynckit": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||||
"dev": true
|
},
|
||||||
|
"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": {
|
"babel-jest": {
|
||||||
"version": "28.1.3",
|
"version": "28.1.3",
|
||||||
|
@ -12118,7 +12202,6 @@
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"delayed-stream": "~1.0.0"
|
"delayed-stream": "~1.0.0"
|
||||||
}
|
}
|
||||||
|
@ -12278,8 +12361,7 @@
|
||||||
"delayed-stream": {
|
"delayed-stream": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"delegates": {
|
"delegates": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
@ -12926,6 +13008,11 @@
|
||||||
"integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
|
"integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
|
||||||
"dev": true
|
"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": {
|
"fork-ts-checker-webpack-plugin": {
|
||||||
"version": "7.2.13",
|
"version": "7.2.13",
|
||||||
"resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-7.2.13.tgz",
|
"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",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"asynckit": "^0.4.0",
|
"asynckit": "^0.4.0",
|
||||||
"combined-stream": "^1.0.8",
|
"combined-stream": "^1.0.8",
|
||||||
|
@ -14917,6 +15003,11 @@
|
||||||
"ipaddr.js": "1.9.1"
|
"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": {
|
"pump": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
||||||
|
|
|
@ -32,6 +32,7 @@
|
||||||
"@golevelup/nestjs-rabbitmq": "^3.4.0",
|
"@golevelup/nestjs-rabbitmq": "^3.4.0",
|
||||||
"@grpc/grpc-js": "^1.8.0",
|
"@grpc/grpc-js": "^1.8.0",
|
||||||
"@grpc/proto-loader": "^0.7.4",
|
"@grpc/proto-loader": "^0.7.4",
|
||||||
|
"@nestjs/axios": "^1.0.1",
|
||||||
"@nestjs/common": "^9.0.0",
|
"@nestjs/common": "^9.0.0",
|
||||||
"@nestjs/config": "^2.2.0",
|
"@nestjs/config": "^2.2.0",
|
||||||
"@nestjs/core": "^9.0.0",
|
"@nestjs/core": "^9.0.0",
|
||||||
|
@ -39,6 +40,7 @@
|
||||||
"@nestjs/microservices": "^9.2.1",
|
"@nestjs/microservices": "^9.2.1",
|
||||||
"@nestjs/platform-express": "^9.0.0",
|
"@nestjs/platform-express": "^9.0.0",
|
||||||
"@prisma/client": "^4.7.1",
|
"@prisma/client": "^4.7.1",
|
||||||
|
"axios": "^1.2.2",
|
||||||
"bcrypt": "^5.1.0",
|
"bcrypt": "^5.1.0",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.0",
|
"class-validator": "^0.14.0",
|
||||||
|
@ -80,7 +82,11 @@
|
||||||
"json",
|
"json",
|
||||||
"ts"
|
"ts"
|
||||||
],
|
],
|
||||||
"modulePathIgnorePatterns": [".controller.ts",".module.ts","main.ts"],
|
"modulePathIgnorePatterns": [
|
||||||
|
".controller.ts",
|
||||||
|
".module.ts",
|
||||||
|
"main.ts"
|
||||||
|
],
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"testRegex": ".*\\.spec\\.ts$",
|
"testRegex": ".*\\.spec\\.ts$",
|
||||||
"transform": {
|
"transform": {
|
||||||
|
|
|
@ -2,13 +2,15 @@ import { classes } from '@automapper/classes';
|
||||||
import { AutomapperModule } from '@automapper/nestjs';
|
import { AutomapperModule } from '@automapper/nestjs';
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
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({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule.forRoot({ isGlobal: true }),
|
ConfigModule.forRoot({ isGlobal: true }),
|
||||||
AutomapperModule.forRoot({ strategyInitializer: classes() }),
|
AutomapperModule.forRoot({ strategyInitializer: classes() }),
|
||||||
AuthModule,
|
AuthenticationModule,
|
||||||
|
AuthorizationModule,
|
||||||
],
|
],
|
||||||
controllers: [],
|
controllers: [],
|
||||||
providers: [],
|
providers: [],
|
||||||
|
|
12
src/main.ts
12
src/main.ts
|
@ -9,11 +9,17 @@ async function bootstrap() {
|
||||||
{
|
{
|
||||||
transport: Transport.GRPC,
|
transport: Transport.GRPC,
|
||||||
options: {
|
options: {
|
||||||
package: 'auth',
|
package: ['authentication', 'authorization'],
|
||||||
protoPath: join(
|
protoPath: [
|
||||||
|
join(
|
||||||
__dirname,
|
__dirname,
|
||||||
'modules/auth/adapters/primaries/auth.proto',
|
'modules/authentication/adapters/primaries/authentication.proto',
|
||||||
),
|
),
|
||||||
|
join(
|
||||||
|
__dirname,
|
||||||
|
'modules/authorization/adapters/primaries/authorization.proto',
|
||||||
|
),
|
||||||
|
],
|
||||||
url: process.env.SERVICE_URL + ':' + process.env.SERVICE_PORT,
|
url: process.env.SERVICE_URL + ':' + process.env.SERVICE_PORT,
|
||||||
loader: { keepCase: true, enums: String },
|
loader: { keepCase: true, enums: String },
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,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<Auth> {
|
|
||||||
protected _model = 'auth';
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
import { CreateAuthRequest } from '../domain/dtos/create-auth.request';
|
|
||||||
|
|
||||||
export class CreateAuthCommand {
|
|
||||||
readonly createAuthRequest: CreateAuthRequest;
|
|
||||||
|
|
||||||
constructor(request: CreateAuthRequest) {
|
|
||||||
this.createAuthRequest = request;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
import { DeleteAuthRequest } from '../domain/dtos/delete-auth.request';
|
|
||||||
|
|
||||||
export class DeleteAuthCommand {
|
|
||||||
readonly deleteAuthRequest: DeleteAuthRequest;
|
|
||||||
|
|
||||||
constructor(request: DeleteAuthRequest) {
|
|
||||||
this.deleteAuthRequest = request;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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>(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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -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>(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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -4,17 +4,17 @@ import { CommandBus } from '@nestjs/cqrs';
|
||||||
import { UpdateUsernameCommand } from '../../commands/update-username.command';
|
import { UpdateUsernameCommand } from '../../commands/update-username.command';
|
||||||
import { Type } from '../../domain/dtos/type.enum';
|
import { Type } from '../../domain/dtos/type.enum';
|
||||||
import { UpdateUsernameRequest } from '../../domain/dtos/update-username.request';
|
import { UpdateUsernameRequest } from '../../domain/dtos/update-username.request';
|
||||||
import { DeleteAuthRequest } from '../../domain/dtos/delete-auth.request';
|
import { DeleteAuthenticationRequest } from '../../domain/dtos/delete-authentication.request';
|
||||||
import { DeleteAuthCommand } from '../../commands/delete-auth.command';
|
import { DeleteAuthenticationCommand } from '../../commands/delete-authentication.command';
|
||||||
|
|
||||||
@Controller()
|
@Controller()
|
||||||
export class AuthMessagerController {
|
export class AuthenticationMessagerController {
|
||||||
constructor(private readonly _commandBus: CommandBus) {}
|
constructor(private readonly _commandBus: CommandBus) {}
|
||||||
|
|
||||||
@RabbitSubscribe({
|
@RabbitSubscribe({
|
||||||
exchange: 'user',
|
exchange: 'user',
|
||||||
routingKey: 'update',
|
routingKey: 'update',
|
||||||
queue: 'auth-user-update',
|
queue: 'authentication-user-update',
|
||||||
})
|
})
|
||||||
public async userUpdatedHandler(message: string) {
|
public async userUpdatedHandler(message: string) {
|
||||||
const updatedUser = JSON.parse(message);
|
const updatedUser = JSON.parse(message);
|
||||||
|
@ -42,13 +42,15 @@ export class AuthMessagerController {
|
||||||
@RabbitSubscribe({
|
@RabbitSubscribe({
|
||||||
exchange: 'user',
|
exchange: 'user',
|
||||||
routingKey: 'delete',
|
routingKey: 'delete',
|
||||||
queue: 'auth-user-delete',
|
queue: 'authentication-user-delete',
|
||||||
})
|
})
|
||||||
public async userDeletedHandler(message: string) {
|
public async userDeletedHandler(message: string) {
|
||||||
const deletedUser = JSON.parse(message);
|
const deletedUser = JSON.parse(message);
|
||||||
if (!deletedUser.hasOwnProperty('uuid')) throw new Error();
|
if (!deletedUser.hasOwnProperty('uuid')) throw new Error();
|
||||||
const deleteAuthRequest = new DeleteAuthRequest();
|
const deleteAuthenticationRequest = new DeleteAuthenticationRequest();
|
||||||
deleteAuthRequest.uuid = deletedUser.uuid;
|
deleteAuthenticationRequest.uuid = deletedUser.uuid;
|
||||||
await this._commandBus.execute(new DeleteAuthCommand(deleteAuthRequest));
|
await this._commandBus.execute(
|
||||||
|
new DeleteAuthenticationCommand(deleteAuthenticationRequest),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -5,23 +5,23 @@ import { CommandBus, QueryBus } from '@nestjs/cqrs';
|
||||||
import { GrpcMethod, RpcException } from '@nestjs/microservices';
|
import { GrpcMethod, RpcException } from '@nestjs/microservices';
|
||||||
import { DatabaseException } from 'src/modules/database/src/exceptions/DatabaseException';
|
import { DatabaseException } from 'src/modules/database/src/exceptions/DatabaseException';
|
||||||
import { AddUsernameCommand } from '../../commands/add-username.command';
|
import { AddUsernameCommand } from '../../commands/add-username.command';
|
||||||
import { CreateAuthCommand } from '../../commands/create-auth.command';
|
import { CreateAuthenticationCommand } from '../../commands/create-authentication.command';
|
||||||
import { DeleteAuthCommand } from '../../commands/delete-auth.command';
|
import { DeleteAuthenticationCommand } from '../../commands/delete-authentication.command';
|
||||||
import { DeleteUsernameCommand } from '../../commands/delete-username.command';
|
import { DeleteUsernameCommand } from '../../commands/delete-username.command';
|
||||||
import { UpdatePasswordCommand } from '../../commands/update-password.command';
|
import { UpdatePasswordCommand } from '../../commands/update-password.command';
|
||||||
import { UpdateUsernameCommand } from '../../commands/update-username.command';
|
import { UpdateUsernameCommand } from '../../commands/update-username.command';
|
||||||
import { AddUsernameRequest } from '../../domain/dtos/add-username.request';
|
import { AddUsernameRequest } from '../../domain/dtos/add-username.request';
|
||||||
import { CreateAuthRequest } from '../../domain/dtos/create-auth.request';
|
import { CreateAuthenticationRequest } from '../../domain/dtos/create-authentication.request';
|
||||||
import { DeleteAuthRequest } from '../../domain/dtos/delete-auth.request';
|
import { DeleteAuthenticationRequest } from '../../domain/dtos/delete-authentication.request';
|
||||||
import { DeleteUsernameRequest } from '../../domain/dtos/delete-username.request';
|
import { DeleteUsernameRequest } from '../../domain/dtos/delete-username.request';
|
||||||
import { UpdatePasswordRequest } from '../../domain/dtos/update-password.request';
|
import { UpdatePasswordRequest } from '../../domain/dtos/update-password.request';
|
||||||
import { UpdateUsernameRequest } from '../../domain/dtos/update-username.request';
|
import { UpdateUsernameRequest } from '../../domain/dtos/update-username.request';
|
||||||
import { ValidateAuthRequest } from '../../domain/dtos/validate-auth.request';
|
import { ValidateAuthenticationRequest } from '../../domain/dtos/validate-authentication.request';
|
||||||
import { Auth } from '../../domain/entities/auth';
|
import { Authentication } from '../../domain/entities/authentication';
|
||||||
import { Username } from '../../domain/entities/username';
|
import { Username } from '../../domain/entities/username';
|
||||||
import { ValidateAuthQuery } from '../../queries/validate-auth.query';
|
import { ValidateAuthenticationQuery } from '../../queries/validate-authentication.query';
|
||||||
import { AuthPresenter } from './auth.presenter';
|
import { AuthenticationPresenter } from './authentication.presenter';
|
||||||
import { RpcValidationPipe } from './rpc.validation-pipe';
|
import { RpcValidationPipe } from '../../../../utils/pipes/rpc.validation-pipe';
|
||||||
import { UsernamePresenter } from './username.presenter';
|
import { UsernamePresenter } from './username.presenter';
|
||||||
|
|
||||||
@UsePipes(
|
@UsePipes(
|
||||||
|
@ -31,20 +31,26 @@ import { UsernamePresenter } from './username.presenter';
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@Controller()
|
@Controller()
|
||||||
export class AuthController {
|
export class AuthenticationController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly _commandBus: CommandBus,
|
private readonly _commandBus: CommandBus,
|
||||||
private readonly _queryBus: QueryBus,
|
private readonly _queryBus: QueryBus,
|
||||||
@InjectMapper() private readonly _mapper: Mapper,
|
@InjectMapper() private readonly _mapper: Mapper,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@GrpcMethod('AuthService', 'Validate')
|
@GrpcMethod('AuthenticationService', 'Validate')
|
||||||
async validate(data: ValidateAuthRequest): Promise<AuthPresenter> {
|
async validate(
|
||||||
|
data: ValidateAuthenticationRequest,
|
||||||
|
): Promise<AuthenticationPresenter> {
|
||||||
try {
|
try {
|
||||||
const auth: Auth = await this._queryBus.execute(
|
const authentication: Authentication = await this._queryBus.execute(
|
||||||
new ValidateAuthQuery(data.username, data.password),
|
new ValidateAuthenticationQuery(data.username, data.password),
|
||||||
|
);
|
||||||
|
return this._mapper.map(
|
||||||
|
authentication,
|
||||||
|
Authentication,
|
||||||
|
AuthenticationPresenter,
|
||||||
);
|
);
|
||||||
return this._mapper.map(auth, Auth, AuthPresenter);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new RpcException({
|
throw new RpcException({
|
||||||
code: 7,
|
code: 7,
|
||||||
|
@ -53,13 +59,19 @@ export class AuthController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@GrpcMethod('AuthService', 'Create')
|
@GrpcMethod('AuthenticationService', 'Create')
|
||||||
async createUser(data: CreateAuthRequest): Promise<AuthPresenter> {
|
async createUser(
|
||||||
|
data: CreateAuthenticationRequest,
|
||||||
|
): Promise<AuthenticationPresenter> {
|
||||||
try {
|
try {
|
||||||
const auth: Auth = await this._commandBus.execute(
|
const authentication: Authentication = await this._commandBus.execute(
|
||||||
new CreateAuthCommand(data),
|
new CreateAuthenticationCommand(data),
|
||||||
|
);
|
||||||
|
return this._mapper.map(
|
||||||
|
authentication,
|
||||||
|
Authentication,
|
||||||
|
AuthenticationPresenter,
|
||||||
);
|
);
|
||||||
return this._mapper.map(auth, Auth, AuthPresenter);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof DatabaseException) {
|
if (e instanceof DatabaseException) {
|
||||||
if (e.message.includes('Already exists')) {
|
if (e.message.includes('Already exists')) {
|
||||||
|
@ -76,7 +88,7 @@ export class AuthController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@GrpcMethod('AuthService', 'AddUsername')
|
@GrpcMethod('AuthenticationService', 'AddUsername')
|
||||||
async addUsername(data: AddUsernameRequest): Promise<UsernamePresenter> {
|
async addUsername(data: AddUsernameRequest): Promise<UsernamePresenter> {
|
||||||
try {
|
try {
|
||||||
const username: Username = await this._commandBus.execute(
|
const username: Username = await this._commandBus.execute(
|
||||||
|
@ -100,7 +112,7 @@ export class AuthController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@GrpcMethod('AuthService', 'UpdateUsername')
|
@GrpcMethod('AuthenticationService', 'UpdateUsername')
|
||||||
async updateUsername(
|
async updateUsername(
|
||||||
data: UpdateUsernameRequest,
|
data: UpdateUsernameRequest,
|
||||||
): Promise<UsernamePresenter> {
|
): Promise<UsernamePresenter> {
|
||||||
|
@ -126,14 +138,20 @@ export class AuthController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@GrpcMethod('AuthService', 'UpdatePassword')
|
@GrpcMethod('AuthenticationService', 'UpdatePassword')
|
||||||
async updatePassword(data: UpdatePasswordRequest): Promise<AuthPresenter> {
|
async updatePassword(
|
||||||
|
data: UpdatePasswordRequest,
|
||||||
|
): Promise<AuthenticationPresenter> {
|
||||||
try {
|
try {
|
||||||
const auth: Auth = await this._commandBus.execute(
|
const authentication: Authentication = await this._commandBus.execute(
|
||||||
new UpdatePasswordCommand(data),
|
new UpdatePasswordCommand(data),
|
||||||
);
|
);
|
||||||
|
|
||||||
return this._mapper.map(auth, Auth, AuthPresenter);
|
return this._mapper.map(
|
||||||
|
authentication,
|
||||||
|
Authentication,
|
||||||
|
AuthenticationPresenter,
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new RpcException({
|
throw new RpcException({
|
||||||
code: 7,
|
code: 7,
|
||||||
|
@ -142,7 +160,7 @@ export class AuthController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@GrpcMethod('AuthService', 'DeleteUsername')
|
@GrpcMethod('AuthenticationService', 'DeleteUsername')
|
||||||
async deleteUsername(data: DeleteUsernameRequest) {
|
async deleteUsername(data: DeleteUsernameRequest) {
|
||||||
try {
|
try {
|
||||||
return await this._commandBus.execute(new DeleteUsernameCommand(data));
|
return await this._commandBus.execute(new DeleteUsernameCommand(data));
|
||||||
|
@ -154,10 +172,12 @@ export class AuthController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@GrpcMethod('AuthService', 'Delete')
|
@GrpcMethod('AuthenticationService', 'Delete')
|
||||||
async deleteAuth(data: DeleteAuthRequest) {
|
async deleteAuthentication(data: DeleteAuthenticationRequest) {
|
||||||
try {
|
try {
|
||||||
return await this._commandBus.execute(new DeleteAuthCommand(data));
|
return await this._commandBus.execute(
|
||||||
|
new DeleteAuthenticationCommand(data),
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new RpcException({
|
throw new RpcException({
|
||||||
code: 7,
|
code: 7,
|
|
@ -1,6 +1,6 @@
|
||||||
import { AutoMap } from '@automapper/classes';
|
import { AutoMap } from '@automapper/classes';
|
||||||
|
|
||||||
export class AuthPresenter {
|
export class AuthenticationPresenter {
|
||||||
@AutoMap()
|
@AutoMap()
|
||||||
uuid: string;
|
uuid: string;
|
||||||
}
|
}
|
|
@ -1,10 +1,10 @@
|
||||||
syntax = "proto3";
|
syntax = "proto3";
|
||||||
|
|
||||||
package auth;
|
package authentication;
|
||||||
|
|
||||||
service AuthService {
|
service AuthenticationService {
|
||||||
rpc Validate(AuthByUsernamePassword) returns (Uuid);
|
rpc Validate(AuthenticationByUsernamePassword) returns (Uuid);
|
||||||
rpc Create(Auth) returns (Uuid);
|
rpc Create(Authentication) returns (Uuid);
|
||||||
rpc AddUsername(Username) returns (Uuid);
|
rpc AddUsername(Username) returns (Uuid);
|
||||||
rpc UpdatePassword(Password) returns (Uuid);
|
rpc UpdatePassword(Password) returns (Uuid);
|
||||||
rpc UpdateUsername(Username) returns (Uuid);
|
rpc UpdateUsername(Username) returns (Uuid);
|
||||||
|
@ -12,7 +12,7 @@ service AuthService {
|
||||||
rpc Delete(Uuid) returns (Empty);
|
rpc Delete(Uuid) returns (Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
message AuthByUsernamePassword {
|
message AuthenticationByUsernamePassword {
|
||||||
string username = 1;
|
string username = 1;
|
||||||
string password = 2;
|
string password = 2;
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@ enum Type {
|
||||||
PHONE = 1;
|
PHONE = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message Auth {
|
message Authentication {
|
||||||
string uuid = 1;
|
string uuid = 1;
|
||||||
string username = 2;
|
string username = 2;
|
||||||
string password = 3;
|
string password = 3;
|
|
@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common';
|
||||||
import { IMessageBroker } from '../../domain/interfaces/message-broker';
|
import { IMessageBroker } from '../../domain/interfaces/message-broker';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthMessager extends IMessageBroker {
|
export class AuthenticationMessager extends IMessageBroker {
|
||||||
constructor(private readonly _amqpConnection: AmqpConnection) {
|
constructor(private readonly _amqpConnection: AmqpConnection) {
|
||||||
super('auth');
|
super('auth');
|
||||||
}
|
}
|
|
@ -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<Authentication> {
|
||||||
|
protected _model = 'auth';
|
||||||
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
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';
|
import { Username } from '../../domain/entities/username';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UsernameRepository extends AuthNZRepository<Username> {
|
export class UsernameRepository extends AuthRepository<Username> {
|
||||||
protected _model = 'username';
|
protected _model = 'username';
|
||||||
}
|
}
|
|
@ -1,20 +1,20 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { CqrsModule } from '@nestjs/cqrs';
|
import { CqrsModule } from '@nestjs/cqrs';
|
||||||
import { DatabaseModule } from '../database/database.module';
|
import { DatabaseModule } from '../database/database.module';
|
||||||
import { AuthController } from './adapters/primaries/auth.controller';
|
import { AuthenticationController } from './adapters/primaries/authentication.controller';
|
||||||
import { CreateAuthUseCase } from './domain/usecases/create-auth.usecase';
|
import { CreateAuthenticationUseCase } from './domain/usecases/create-authentication.usecase';
|
||||||
import { ValidateAuthUseCase } from './domain/usecases/validate-auth.usecase';
|
import { ValidateAuthenticationUseCase } from './domain/usecases/validate-authentication.usecase';
|
||||||
import { AuthProfile } from './mappers/auth.profile';
|
import { AuthenticationProfile } from './mappers/authentication.profile';
|
||||||
import { AuthRepository } from './adapters/secondaries/auth.repository';
|
import { AuthenticationRepository } from './adapters/secondaries/authentication.repository';
|
||||||
import { UpdateUsernameUseCase } from './domain/usecases/update-username.usecase';
|
import { UpdateUsernameUseCase } from './domain/usecases/update-username.usecase';
|
||||||
import { UsernameProfile } from './mappers/username.profile';
|
import { UsernameProfile } from './mappers/username.profile';
|
||||||
import { AddUsernameUseCase } from './domain/usecases/add-username.usecase';
|
import { AddUsernameUseCase } from './domain/usecases/add-username.usecase';
|
||||||
import { UpdatePasswordUseCase } from './domain/usecases/update-password.usecase';
|
import { UpdatePasswordUseCase } from './domain/usecases/update-password.usecase';
|
||||||
import { DeleteUsernameUseCase } from './domain/usecases/delete-username.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 { RabbitMQModule } from '@golevelup/nestjs-rabbitmq';
|
||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
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';
|
import { LoggingMessager } from './adapters/secondaries/logging.messager';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
@ -41,20 +41,20 @@ import { LoggingMessager } from './adapters/secondaries/logging.messager';
|
||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
controllers: [AuthController, AuthMessagerController],
|
controllers: [AuthenticationController, AuthenticationMessagerController],
|
||||||
providers: [
|
providers: [
|
||||||
AuthProfile,
|
AuthenticationProfile,
|
||||||
UsernameProfile,
|
UsernameProfile,
|
||||||
AuthRepository,
|
AuthenticationRepository,
|
||||||
LoggingMessager,
|
LoggingMessager,
|
||||||
ValidateAuthUseCase,
|
ValidateAuthenticationUseCase,
|
||||||
CreateAuthUseCase,
|
CreateAuthenticationUseCase,
|
||||||
AddUsernameUseCase,
|
AddUsernameUseCase,
|
||||||
UpdateUsernameUseCase,
|
UpdateUsernameUseCase,
|
||||||
UpdatePasswordUseCase,
|
UpdatePasswordUseCase,
|
||||||
DeleteUsernameUseCase,
|
DeleteUsernameUseCase,
|
||||||
DeleteAuthUseCase,
|
DeleteAuthenticationUseCase,
|
||||||
],
|
],
|
||||||
exports: [],
|
exports: [],
|
||||||
})
|
})
|
||||||
export class AuthModule {}
|
export class AuthenticationModule {}
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { CreateAuthenticationRequest } from '../domain/dtos/create-authentication.request';
|
||||||
|
|
||||||
|
export class CreateAuthenticationCommand {
|
||||||
|
readonly createAuthenticationRequest: CreateAuthenticationRequest;
|
||||||
|
|
||||||
|
constructor(request: CreateAuthenticationRequest) {
|
||||||
|
this.createAuthenticationRequest = request;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { DeleteAuthenticationRequest } from '../domain/dtos/delete-authentication.request';
|
||||||
|
|
||||||
|
export class DeleteAuthenticationCommand {
|
||||||
|
readonly deleteAuthenticationRequest: DeleteAuthenticationRequest;
|
||||||
|
|
||||||
|
constructor(request: DeleteAuthenticationRequest) {
|
||||||
|
this.deleteAuthenticationRequest = request;
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,7 +2,7 @@ import { AutoMap } from '@automapper/classes';
|
||||||
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
|
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
|
||||||
import { Type } from './type.enum';
|
import { Type } from './type.enum';
|
||||||
|
|
||||||
export class CreateAuthRequest {
|
export class CreateAuthenticationRequest {
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@AutoMap()
|
@AutoMap()
|
|
@ -1,7 +1,7 @@
|
||||||
import { AutoMap } from '@automapper/classes';
|
import { AutoMap } from '@automapper/classes';
|
||||||
import { IsNotEmpty, IsString } from 'class-validator';
|
import { IsNotEmpty, IsString } from 'class-validator';
|
||||||
|
|
||||||
export class DeleteAuthRequest {
|
export class DeleteAuthenticationRequest {
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@AutoMap()
|
@AutoMap()
|
|
@ -1,6 +1,6 @@
|
||||||
import { IsNotEmpty, IsString } from 'class-validator';
|
import { IsNotEmpty, IsString } from 'class-validator';
|
||||||
|
|
||||||
export class ValidateAuthRequest {
|
export class ValidateAuthenticationRequest {
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
username: string;
|
username: string;
|
|
@ -1,6 +1,6 @@
|
||||||
import { AutoMap } from '@automapper/classes';
|
import { AutoMap } from '@automapper/classes';
|
||||||
|
|
||||||
export class Auth {
|
export class Authentication {
|
||||||
@AutoMap()
|
@AutoMap()
|
||||||
uuid: string;
|
uuid: string;
|
||||||
|
|
|
@ -1,25 +1,25 @@
|
||||||
import { CommandHandler } from '@nestjs/cqrs';
|
import { CommandHandler } from '@nestjs/cqrs';
|
||||||
import { AuthRepository } from '../../adapters/secondaries/auth.repository';
|
import { AuthenticationRepository } from '../../adapters/secondaries/authentication.repository';
|
||||||
import { CreateAuthCommand } from '../../commands/create-auth.command';
|
import { CreateAuthenticationCommand } from '../../commands/create-authentication.command';
|
||||||
import { Auth } from '../entities/auth';
|
import { Authentication } from '../entities/authentication';
|
||||||
import * as bcrypt from 'bcrypt';
|
import * as bcrypt from 'bcrypt';
|
||||||
import { UsernameRepository } from '../../adapters/secondaries/username.repository';
|
import { UsernameRepository } from '../../adapters/secondaries/username.repository';
|
||||||
import { LoggingMessager } from '../../adapters/secondaries/logging.messager';
|
import { LoggingMessager } from '../../adapters/secondaries/logging.messager';
|
||||||
|
|
||||||
@CommandHandler(CreateAuthCommand)
|
@CommandHandler(CreateAuthenticationCommand)
|
||||||
export class CreateAuthUseCase {
|
export class CreateAuthenticationUseCase {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly _authRepository: AuthRepository,
|
private readonly _authenticationRepository: AuthenticationRepository,
|
||||||
private readonly _usernameRepository: UsernameRepository,
|
private readonly _usernameRepository: UsernameRepository,
|
||||||
private readonly _loggingMessager: LoggingMessager,
|
private readonly _loggingMessager: LoggingMessager,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async execute(command: CreateAuthCommand): Promise<Auth> {
|
async execute(command: CreateAuthenticationCommand): Promise<Authentication> {
|
||||||
const { uuid, password, ...username } = command.createAuthRequest;
|
const { uuid, password, ...username } = command.createAuthenticationRequest;
|
||||||
const hash = await bcrypt.hash(password, 10);
|
const hash = await bcrypt.hash(password, 10);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const auth = await this._authRepository.create({
|
const auth = await this._authenticationRepository.create({
|
||||||
uuid,
|
uuid,
|
||||||
password: hash,
|
password: hash,
|
||||||
});
|
});
|
|
@ -1,23 +1,25 @@
|
||||||
import { CommandHandler } from '@nestjs/cqrs';
|
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 { LoggingMessager } from '../../adapters/secondaries/logging.messager';
|
||||||
import { UsernameRepository } from '../../adapters/secondaries/username.repository';
|
import { UsernameRepository } from '../../adapters/secondaries/username.repository';
|
||||||
import { DeleteAuthCommand } from '../../commands/delete-auth.command';
|
import { DeleteAuthenticationCommand } from '../../commands/delete-authentication.command';
|
||||||
|
|
||||||
@CommandHandler(DeleteAuthCommand)
|
@CommandHandler(DeleteAuthenticationCommand)
|
||||||
export class DeleteAuthUseCase {
|
export class DeleteAuthenticationUseCase {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly _authRepository: AuthRepository,
|
private readonly _authenticationRepository: AuthenticationRepository,
|
||||||
private readonly _usernameRepository: UsernameRepository,
|
private readonly _usernameRepository: UsernameRepository,
|
||||||
private readonly _loggingMessager: LoggingMessager,
|
private readonly _loggingMessager: LoggingMessager,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async execute(command: DeleteAuthCommand) {
|
async execute(command: DeleteAuthenticationCommand) {
|
||||||
try {
|
try {
|
||||||
await this._usernameRepository.deleteMany({
|
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) {
|
} catch (error) {
|
||||||
this._loggingMessager.publish(
|
this._loggingMessager.publish(
|
||||||
'auth.delete.crit',
|
'auth.delete.crit',
|
|
@ -1,6 +1,6 @@
|
||||||
import { CommandHandler } from '@nestjs/cqrs';
|
import { CommandHandler } from '@nestjs/cqrs';
|
||||||
import { AuthRepository } from '../../adapters/secondaries/auth.repository';
|
import { AuthenticationRepository } from '../../adapters/secondaries/authentication.repository';
|
||||||
import { Auth } from '../entities/auth';
|
import { Authentication } from '../entities/authentication';
|
||||||
import * as bcrypt from 'bcrypt';
|
import * as bcrypt from 'bcrypt';
|
||||||
import { UpdatePasswordCommand } from '../../commands/update-password.command';
|
import { UpdatePasswordCommand } from '../../commands/update-password.command';
|
||||||
import { LoggingMessager } from '../../adapters/secondaries/logging.messager';
|
import { LoggingMessager } from '../../adapters/secondaries/logging.messager';
|
||||||
|
@ -8,16 +8,16 @@ import { LoggingMessager } from '../../adapters/secondaries/logging.messager';
|
||||||
@CommandHandler(UpdatePasswordCommand)
|
@CommandHandler(UpdatePasswordCommand)
|
||||||
export class UpdatePasswordUseCase {
|
export class UpdatePasswordUseCase {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly _authRepository: AuthRepository,
|
private readonly _authenticationRepository: AuthenticationRepository,
|
||||||
private readonly _loggingMessager: LoggingMessager,
|
private readonly _loggingMessager: LoggingMessager,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async execute(command: UpdatePasswordCommand): Promise<Auth> {
|
async execute(command: UpdatePasswordCommand): Promise<Authentication> {
|
||||||
const { uuid, password } = command.updatePasswordRequest;
|
const { uuid, password } = command.updatePasswordRequest;
|
||||||
const hash = await bcrypt.hash(password, 10);
|
const hash = await bcrypt.hash(password, 10);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await this._authRepository.update(uuid, {
|
return await this._authenticationRepository.update(uuid, {
|
||||||
password: hash,
|
password: hash,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
|
@ -1,20 +1,22 @@
|
||||||
import { QueryHandler } from '@nestjs/cqrs';
|
import { QueryHandler } from '@nestjs/cqrs';
|
||||||
import { AuthRepository } from '../../adapters/secondaries/auth.repository';
|
import { AuthenticationRepository } from '../../adapters/secondaries/authentication.repository';
|
||||||
import { ValidateAuthQuery } from '../../queries/validate-auth.query';
|
import { ValidateAuthenticationQuery as ValidateAuthenticationQuery } from '../../queries/validate-authentication.query';
|
||||||
import { Auth } from '../entities/auth';
|
import { Authentication } from '../entities/authentication';
|
||||||
import * as bcrypt from 'bcrypt';
|
import * as bcrypt from 'bcrypt';
|
||||||
import { NotFoundException, UnauthorizedException } from '@nestjs/common';
|
import { NotFoundException, UnauthorizedException } from '@nestjs/common';
|
||||||
import { UsernameRepository } from '../../adapters/secondaries/username.repository';
|
import { UsernameRepository } from '../../adapters/secondaries/username.repository';
|
||||||
import { Username } from '../entities/username';
|
import { Username } from '../entities/username';
|
||||||
|
|
||||||
@QueryHandler(ValidateAuthQuery)
|
@QueryHandler(ValidateAuthenticationQuery)
|
||||||
export class ValidateAuthUseCase {
|
export class ValidateAuthenticationUseCase {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly _authRepository: AuthRepository,
|
private readonly _authenticationRepository: AuthenticationRepository,
|
||||||
private readonly _usernameRepository: UsernameRepository,
|
private readonly _usernameRepository: UsernameRepository,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async execute(validate: ValidateAuthQuery): Promise<Auth> {
|
async execute(
|
||||||
|
validate: ValidateAuthenticationQuery,
|
||||||
|
): Promise<Authentication> {
|
||||||
let username = new Username();
|
let username = new Username();
|
||||||
try {
|
try {
|
||||||
username = await this._usernameRepository.findOne({
|
username = await this._usernameRepository.findOne({
|
||||||
|
@ -24,7 +26,7 @@ export class ValidateAuthUseCase {
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const auth = await this._authRepository.findOne({
|
const auth = await this._authenticationRepository.findOne({
|
||||||
uuid: username.uuid,
|
uuid: username.uuid,
|
||||||
});
|
});
|
||||||
if (auth) {
|
if (auth) {
|
|
@ -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);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
export class ValidateAuthQuery {
|
export class ValidateAuthenticationQuery {
|
||||||
readonly username: string;
|
readonly username: string;
|
||||||
readonly password: string;
|
readonly password: string;
|
||||||
|
|
|
@ -2,16 +2,16 @@ import { TestingModule, Test } from '@nestjs/testing';
|
||||||
import { DatabaseModule } from '../../../database/database.module';
|
import { DatabaseModule } from '../../../database/database.module';
|
||||||
import { PrismaService } from '../../../database/src/adapters/secondaries/prisma-service';
|
import { PrismaService } from '../../../database/src/adapters/secondaries/prisma-service';
|
||||||
import { DatabaseException } from '../../../database/src/exceptions/DatabaseException';
|
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 { v4 } from 'uuid';
|
||||||
import * as bcrypt from 'bcrypt';
|
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 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++) {
|
for (let i = 0; i < nbToCreate; i++) {
|
||||||
await prismaService.auth.create({
|
await prismaService.auth.create({
|
||||||
data: {
|
data: {
|
||||||
|
@ -25,11 +25,13 @@ describe('AuthRepository', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
imports: [DatabaseModule],
|
imports: [DatabaseModule],
|
||||||
providers: [AuthRepository, PrismaService],
|
providers: [AuthenticationRepository, PrismaService],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
prismaService = module.get<PrismaService>(PrismaService);
|
prismaService = module.get<PrismaService>(PrismaService);
|
||||||
authRepository = module.get<AuthRepository>(AuthRepository);
|
authenticationRepository = module.get<AuthenticationRepository>(
|
||||||
|
AuthenticationRepository,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
@ -42,7 +44,7 @@ describe('AuthRepository', () => {
|
||||||
|
|
||||||
describe('findAll', () => {
|
describe('findAll', () => {
|
||||||
it('should return an empty data array', async () => {
|
it('should return an empty data array', async () => {
|
||||||
const res = await authRepository.findAll();
|
const res = await authenticationRepository.findAll();
|
||||||
expect(res).toEqual({
|
expect(res).toEqual({
|
||||||
data: [],
|
data: [],
|
||||||
total: 0,
|
total: 0,
|
||||||
|
@ -50,22 +52,22 @@ describe('AuthRepository', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return a data array with 8 auths', async () => {
|
it('should return a data array with 8 auths', async () => {
|
||||||
await createAuths(8);
|
await createAuthentications(8);
|
||||||
const auths = await authRepository.findAll();
|
const auths = await authenticationRepository.findAll();
|
||||||
expect(auths.data.length).toBe(8);
|
expect(auths.data.length).toBe(8);
|
||||||
expect(auths.total).toBe(8);
|
expect(auths.total).toBe(8);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return a data array limited to 10 auths', async () => {
|
it('should return a data array limited to 10 authentications', async () => {
|
||||||
await createAuths(20);
|
await createAuthentications(20);
|
||||||
const auths = await authRepository.findAll();
|
const auths = await authenticationRepository.findAll();
|
||||||
expect(auths.data.length).toBe(10);
|
expect(auths.data.length).toBe(10);
|
||||||
expect(auths.total).toBe(20);
|
expect(auths.total).toBe(20);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('findOneByUuid', () => {
|
describe('findOneByUuid', () => {
|
||||||
it('should return an auth', async () => {
|
it('should return an authentication', async () => {
|
||||||
const authToFind = await prismaService.auth.create({
|
const authToFind = await prismaService.auth.create({
|
||||||
data: {
|
data: {
|
||||||
uuid: v4(),
|
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);
|
expect(auth.uuid).toBe(authToFind.uuid);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return null', async () => {
|
it('should return null', async () => {
|
||||||
const auth = await authRepository.findOneByUuid(
|
const auth = await authenticationRepository.findOneByUuid(
|
||||||
'544572be-11fb-4244-8235-587221fc9104',
|
'544572be-11fb-4244-8235-587221fc9104',
|
||||||
);
|
);
|
||||||
expect(auth).toBeNull();
|
expect(auth).toBeNull();
|
||||||
|
@ -86,59 +90,64 @@ describe('AuthRepository', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('create', () => {
|
describe('create', () => {
|
||||||
it('should create an auth', async () => {
|
it('should create an authentication', async () => {
|
||||||
const beforeCount = await prismaService.auth.count();
|
const beforeCount = await prismaService.auth.count();
|
||||||
|
|
||||||
const authToCreate: Auth = new Auth();
|
const authenticationToCreate: Authentication = new Authentication();
|
||||||
authToCreate.uuid = v4();
|
authenticationToCreate.uuid = v4();
|
||||||
authToCreate.password = bcrypt.hashSync(`password`, 10);
|
authenticationToCreate.password = bcrypt.hashSync(`password`, 10);
|
||||||
const auth = await authRepository.create(authToCreate);
|
const authentication = await authenticationRepository.create(
|
||||||
|
authenticationToCreate,
|
||||||
|
);
|
||||||
|
|
||||||
const afterCount = await prismaService.auth.count();
|
const afterCount = await prismaService.auth.count();
|
||||||
|
|
||||||
expect(afterCount - beforeCount).toBe(1);
|
expect(afterCount - beforeCount).toBe(1);
|
||||||
expect(auth.uuid).toBeDefined();
|
expect(authentication.uuid).toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('update', () => {
|
describe('update', () => {
|
||||||
it('should update auth password', async () => {
|
it('should update authentication password', async () => {
|
||||||
const authToUpdate = await prismaService.auth.create({
|
const authenticationToUpdate = await prismaService.auth.create({
|
||||||
data: {
|
data: {
|
||||||
uuid: v4(),
|
uuid: v4(),
|
||||||
password: bcrypt.hashSync(`password`, 10),
|
password: bcrypt.hashSync(`password`, 10),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const toUpdate: Auth = new Auth();
|
const toUpdate: Authentication = new Authentication();
|
||||||
toUpdate.password = bcrypt.hashSync(`newPassword`, 10);
|
toUpdate.password = bcrypt.hashSync(`newPassword`, 10);
|
||||||
const updatedAuth = await authRepository.update(
|
const updatedAuthentication = await authenticationRepository.update(
|
||||||
authToUpdate.uuid,
|
authenticationToUpdate.uuid,
|
||||||
toUpdate,
|
toUpdate,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(updatedAuth.uuid).toBe(authToUpdate.uuid);
|
expect(updatedAuthentication.uuid).toBe(authenticationToUpdate.uuid);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw DatabaseException', async () => {
|
it('should throw DatabaseException', async () => {
|
||||||
const toUpdate: Auth = new Auth();
|
const toUpdate: Authentication = new Authentication();
|
||||||
toUpdate.password = bcrypt.hashSync(`newPassword`, 10);
|
toUpdate.password = bcrypt.hashSync(`newPassword`, 10);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
authRepository.update('544572be-11fb-4244-8235-587221fc9104', toUpdate),
|
authenticationRepository.update(
|
||||||
|
'544572be-11fb-4244-8235-587221fc9104',
|
||||||
|
toUpdate,
|
||||||
|
),
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
).rejects.toBeInstanceOf(DatabaseException);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('delete', () => {
|
describe('delete', () => {
|
||||||
it('should delete an auth', async () => {
|
it('should delete an authentication', async () => {
|
||||||
const authToRemove = await prismaService.auth.create({
|
const authenticationToRemove = await prismaService.auth.create({
|
||||||
data: {
|
data: {
|
||||||
uuid: v4(),
|
uuid: v4(),
|
||||||
password: bcrypt.hashSync(`password`, 10),
|
password: bcrypt.hashSync(`password`, 10),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await authRepository.delete(authToRemove.uuid);
|
await authenticationRepository.delete(authenticationToRemove.uuid);
|
||||||
|
|
||||||
const count = await prismaService.auth.count();
|
const count = await prismaService.auth.count();
|
||||||
expect(count).toBe(0);
|
expect(count).toBe(0);
|
||||||
|
@ -146,7 +155,7 @@ describe('AuthRepository', () => {
|
||||||
|
|
||||||
it('should throw DatabaseException', async () => {
|
it('should throw DatabaseException', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
authRepository.delete('544572be-11fb-4244-8235-587221fc9104'),
|
authenticationRepository.delete('544572be-11fb-4244-8235-587221fc9104'),
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
).rejects.toBeInstanceOf(DatabaseException);
|
||||||
});
|
});
|
||||||
});
|
});
|
|
@ -1,7 +1,7 @@
|
||||||
import { classes } from '@automapper/classes';
|
import { classes } from '@automapper/classes';
|
||||||
import { AutomapperModule } from '@automapper/nestjs';
|
import { AutomapperModule } from '@automapper/nestjs';
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
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 { UsernameRepository } from '../../adapters/secondaries/username.repository';
|
||||||
import { Username } from '../../domain/entities/username';
|
import { Username } from '../../domain/entities/username';
|
||||||
import { Type } from '../../domain/dtos/type.enum';
|
import { Type } from '../../domain/dtos/type.enum';
|
||||||
|
@ -50,7 +50,7 @@ describe('AddUsernameUseCase', () => {
|
||||||
useValue: mockMessager,
|
useValue: mockMessager,
|
||||||
},
|
},
|
||||||
AddUsernameUseCase,
|
AddUsernameUseCase,
|
||||||
AuthProfile,
|
AuthenticationProfile,
|
||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
|
@ -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>(
|
||||||
|
CreateAuthenticationUseCase,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(createAuthenticationUseCase).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('execute', () => {
|
||||||
|
it('should create an authentication with an encrypted password', async () => {
|
||||||
|
const newAuthentication: Authentication =
|
||||||
|
await createAuthenticationUseCase.execute(newAuthCommand);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
bcrypt.compareSync(
|
||||||
|
newAuthenticationRequest.password,
|
||||||
|
newAuthentication.password,
|
||||||
|
),
|
||||||
|
).toBeTruthy();
|
||||||
|
});
|
||||||
|
it('should throw an error if user already exists', async () => {
|
||||||
|
await expect(
|
||||||
|
createAuthenticationUseCase.execute(newAuthCommand),
|
||||||
|
).rejects.toBeInstanceOf(Error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,13 +1,13 @@
|
||||||
import { classes } from '@automapper/classes';
|
import { classes } from '@automapper/classes';
|
||||||
import { AutomapperModule } from '@automapper/nestjs';
|
import { AutomapperModule } from '@automapper/nestjs';
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
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 { LoggingMessager } from '../../adapters/secondaries/logging.messager';
|
||||||
import { UsernameRepository } from '../../adapters/secondaries/username.repository';
|
import { UsernameRepository } from '../../adapters/secondaries/username.repository';
|
||||||
import { DeleteAuthCommand } from '../../commands/delete-auth.command';
|
import { DeleteAuthenticationCommand } from '../../commands/delete-authentication.command';
|
||||||
import { DeleteAuthRequest } from '../../domain/dtos/delete-auth.request';
|
import { DeleteAuthenticationRequest } from '../../domain/dtos/delete-authentication.request';
|
||||||
import { Type } from '../../domain/dtos/type.enum';
|
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 = {
|
const usernames = {
|
||||||
data: [
|
data: [
|
||||||
|
@ -25,13 +25,13 @@ const usernames = {
|
||||||
total: 2,
|
total: 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteAuthRequest: DeleteAuthRequest = new DeleteAuthRequest();
|
const deleteAuthenticationRequest: DeleteAuthenticationRequest =
|
||||||
deleteAuthRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a91';
|
new DeleteAuthenticationRequest();
|
||||||
const deleteAuthCommand: DeleteAuthCommand = new DeleteAuthCommand(
|
deleteAuthenticationRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a91';
|
||||||
deleteAuthRequest,
|
const deleteAuthenticationCommand: DeleteAuthenticationCommand =
|
||||||
);
|
new DeleteAuthenticationCommand(deleteAuthenticationRequest);
|
||||||
|
|
||||||
const mockAuthRepository = {
|
const mockAuthenticationRepository = {
|
||||||
delete: jest
|
delete: jest
|
||||||
.fn()
|
.fn()
|
||||||
.mockResolvedValueOnce(undefined)
|
.mockResolvedValueOnce(undefined)
|
||||||
|
@ -53,16 +53,16 @@ const mockMessager = {
|
||||||
publish: jest.fn().mockImplementation(),
|
publish: jest.fn().mockImplementation(),
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('DeleteAuthUseCase', () => {
|
describe('DeleteAuthenticationUseCase', () => {
|
||||||
let deleteAuthUseCase: DeleteAuthUseCase;
|
let deleteAuthenticationUseCase: DeleteAuthenticationUseCase;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })],
|
imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
provide: AuthRepository,
|
provide: AuthenticationRepository,
|
||||||
useValue: mockAuthRepository,
|
useValue: mockAuthenticationRepository,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: UsernameRepository,
|
provide: UsernameRepository,
|
||||||
|
@ -72,26 +72,30 @@ describe('DeleteAuthUseCase', () => {
|
||||||
provide: LoggingMessager,
|
provide: LoggingMessager,
|
||||||
useValue: mockMessager,
|
useValue: mockMessager,
|
||||||
},
|
},
|
||||||
DeleteAuthUseCase,
|
DeleteAuthenticationUseCase,
|
||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
deleteAuthUseCase = module.get<DeleteAuthUseCase>(DeleteAuthUseCase);
|
deleteAuthenticationUseCase = module.get<DeleteAuthenticationUseCase>(
|
||||||
|
DeleteAuthenticationUseCase,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be defined', () => {
|
it('should be defined', () => {
|
||||||
expect(deleteAuthUseCase).toBeDefined();
|
expect(deleteAuthenticationUseCase).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('execute', () => {
|
describe('execute', () => {
|
||||||
it('should delete an auth and its usernames', async () => {
|
it('should delete an authentication and its usernames', async () => {
|
||||||
const deletedAuth = await deleteAuthUseCase.execute(deleteAuthCommand);
|
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(
|
await expect(
|
||||||
deleteAuthUseCase.execute(deleteAuthCommand),
|
deleteAuthenticationUseCase.execute(deleteAuthenticationCommand),
|
||||||
).rejects.toBeInstanceOf(Error);
|
).rejects.toBeInstanceOf(Error);
|
||||||
});
|
});
|
||||||
});
|
});
|
|
@ -1,8 +1,8 @@
|
||||||
import { classes } from '@automapper/classes';
|
import { classes } from '@automapper/classes';
|
||||||
import { AutomapperModule } from '@automapper/nestjs';
|
import { AutomapperModule } from '@automapper/nestjs';
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { AuthRepository } from '../../adapters/secondaries/auth.repository';
|
import { AuthenticationRepository } from '../../adapters/secondaries/authentication.repository';
|
||||||
import { Auth } from '../../domain/entities/auth';
|
import { Authentication } from '../../domain/entities/authentication';
|
||||||
import * as bcrypt from 'bcrypt';
|
import * as bcrypt from 'bcrypt';
|
||||||
import { UpdatePasswordRequest } from '../../domain/dtos/update-password.request';
|
import { UpdatePasswordRequest } from '../../domain/dtos/update-password.request';
|
||||||
import { UpdatePasswordCommand } from '../../commands/update-password.command';
|
import { UpdatePasswordCommand } from '../../commands/update-password.command';
|
||||||
|
@ -18,7 +18,7 @@ const updatePasswordCommand: UpdatePasswordCommand = new UpdatePasswordCommand(
|
||||||
updatePasswordRequest,
|
updatePasswordRequest,
|
||||||
);
|
);
|
||||||
|
|
||||||
const mockAuthRepository = {
|
const mockAuthenticationRepository = {
|
||||||
update: jest
|
update: jest
|
||||||
.fn()
|
.fn()
|
||||||
.mockResolvedValueOnce({
|
.mockResolvedValueOnce({
|
||||||
|
@ -42,8 +42,8 @@ describe('UpdatePasswordUseCase', () => {
|
||||||
imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })],
|
imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
provide: AuthRepository,
|
provide: AuthenticationRepository,
|
||||||
useValue: mockAuthRepository,
|
useValue: mockAuthenticationRepository,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: LoggingMessager,
|
provide: LoggingMessager,
|
||||||
|
@ -64,7 +64,7 @@ describe('UpdatePasswordUseCase', () => {
|
||||||
|
|
||||||
describe('execute', () => {
|
describe('execute', () => {
|
||||||
it('should update an auth with an new encrypted password', async () => {
|
it('should update an auth with an new encrypted password', async () => {
|
||||||
const newAuth: Auth = await updatePasswordUseCase.execute(
|
const newAuth: Authentication = await updatePasswordUseCase.execute(
|
||||||
updatePasswordCommand,
|
updatePasswordCommand,
|
||||||
);
|
);
|
||||||
|
|
|
@ -18,37 +18,37 @@ const existingUsername = {
|
||||||
type: Type.EMAIL,
|
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 =
|
const updateUsernameRequest: UpdateUsernameRequest =
|
||||||
new UpdateUsernameRequest();
|
new UpdateUsernameRequest();
|
||||||
updateUsernameRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a91';
|
updateUsernameRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a91';
|
||||||
updateUsernameRequest.username = 'johnny.doe@email.com';
|
updateUsernameRequest.username = 'johnny.doe@email.com';
|
||||||
updateUsernameRequest.type = Type.EMAIL;
|
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 =
|
const unknownUsernameRequest: UpdateUsernameRequest =
|
||||||
new UpdateUsernameRequest();
|
new UpdateUsernameRequest();
|
||||||
unknownUsernameRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a91';
|
unknownUsernameRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a92';
|
||||||
unknownUsernameRequest.username = 'unknown@email.com';
|
unknownUsernameRequest.username = 'unknown@email.com';
|
||||||
unknownUsernameRequest.type = Type.EMAIL;
|
unknownUsernameRequest.type = Type.EMAIL;
|
||||||
|
|
||||||
const updateUsernameCommand: UpdateUsernameCommand = new UpdateUsernameCommand(
|
const invalidUpdateUsernameRequest: UpdateUsernameRequest =
|
||||||
updateUsernameRequest,
|
new UpdateUsernameRequest();
|
||||||
);
|
invalidUpdateUsernameRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a93';
|
||||||
|
invalidUpdateUsernameRequest.username = '';
|
||||||
|
invalidUpdateUsernameRequest.type = Type.EMAIL;
|
||||||
|
|
||||||
const newUsernameCommand: UpdateUsernameCommand = new UpdateUsernameCommand(
|
const newUsernameCommand: UpdateUsernameCommand = new UpdateUsernameCommand(
|
||||||
newUsernameRequest,
|
newUsernameRequest,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const updateUsernameCommand: UpdateUsernameCommand = new UpdateUsernameCommand(
|
||||||
|
updateUsernameRequest,
|
||||||
|
);
|
||||||
|
|
||||||
const invalidUpdateUsernameCommand: UpdateUsernameCommand =
|
const invalidUpdateUsernameCommand: UpdateUsernameCommand =
|
||||||
new UpdateUsernameCommand(invalidUpdateUsernameRequest);
|
new UpdateUsernameCommand(invalidUpdateUsernameRequest);
|
||||||
|
|
||||||
|
@ -56,19 +56,22 @@ const unknownUpdateUsernameCommand: UpdateUsernameCommand =
|
||||||
new UpdateUsernameCommand(unknownUsernameRequest);
|
new UpdateUsernameCommand(unknownUsernameRequest);
|
||||||
|
|
||||||
const mockUsernameRepository = {
|
const mockUsernameRepository = {
|
||||||
findOne: jest.fn().mockResolvedValue(existingUsername),
|
findOne: jest.fn().mockImplementation((request) => {
|
||||||
updateWhere: jest
|
if (request.uuid == 'bb281075-1b98-4456-89d6-c643d3044a90') {
|
||||||
.fn()
|
return Promise.resolve(null);
|
||||||
.mockImplementationOnce(() => {
|
}
|
||||||
return Promise.resolve(updateUsernameRequest);
|
return Promise.resolve(existingUsername);
|
||||||
})
|
}),
|
||||||
.mockImplementationOnce(() => {
|
updateWhere: jest.fn().mockImplementation((request) => {
|
||||||
|
if (request.uuid_type.uuid == 'bb281075-1b98-4456-89d6-c643d3044a90') {
|
||||||
return Promise.resolve(newUsernameRequest);
|
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');
|
throw new Error('Error');
|
||||||
})
|
}
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
return Promise.resolve(invalidUpdateUsernameRequest);
|
return Promise.resolve(invalidUpdateUsernameRequest);
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
@ -115,15 +118,6 @@ describe('UpdateUsernameUseCase', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('execute', () => {
|
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 () => {
|
it('should create a new username', async () => {
|
||||||
const newUsername: Username = await updateUsernameUseCase.execute(
|
const newUsername: Username = await updateUsernameUseCase.execute(
|
||||||
newUsernameCommand,
|
newUsernameCommand,
|
||||||
|
@ -133,6 +127,15 @@ describe('UpdateUsernameUseCase', () => {
|
||||||
expect(newUsername.type).toBe(newUsernameRequest.type);
|
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 () => {
|
it('should throw an error if username does not exist', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
updateUsernameUseCase.execute(unknownUpdateUsernameCommand),
|
updateUsernameUseCase.execute(unknownUpdateUsernameCommand),
|
|
@ -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>(
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -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<AuthorizationPresenter> {
|
||||||
|
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',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { AutoMap } from '@automapper/classes';
|
||||||
|
|
||||||
|
export class AuthorizationPresenter {
|
||||||
|
@AutoMap()
|
||||||
|
allow: boolean;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export class DecisionResult {
|
||||||
|
allow: boolean;
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { DecisionResult } from './decision-result';
|
||||||
|
|
||||||
|
export class Decision {
|
||||||
|
decision_id: string;
|
||||||
|
result: DecisionResult;
|
||||||
|
}
|
|
@ -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<ContextItem>,
|
||||||
|
): Promise<Authorization> {
|
||||||
|
const reducedContext = context.reduce(
|
||||||
|
(obj, item) => Object.assign(obj, { [item.name]: item.value }),
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
const { data } = await lastValueFrom(
|
||||||
|
this._httpService.post<Decision>(
|
||||||
|
this._configService.get<string>('OPA_URL') + domain + '/' + action,
|
||||||
|
{
|
||||||
|
input: {
|
||||||
|
uuid,
|
||||||
|
...reducedContext,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return new Authorization(data.result.allow);
|
||||||
|
} catch (e) {
|
||||||
|
return new Authorization(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {}
|
|
@ -0,0 +1,7 @@
|
||||||
|
export enum Action {
|
||||||
|
create = 'create',
|
||||||
|
read = 'read',
|
||||||
|
update = 'update',
|
||||||
|
delete = 'delete',
|
||||||
|
list = 'list',
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
export class ContextItem {
|
||||||
|
name: string;
|
||||||
|
value: any;
|
||||||
|
|
||||||
|
constructor(name: string, value: any) {
|
||||||
|
this.name = name;
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<ContextItem>;
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export enum Domain {
|
||||||
|
user = 'user',
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { AutoMap } from '@automapper/classes';
|
||||||
|
|
||||||
|
export class Authorization {
|
||||||
|
@AutoMap()
|
||||||
|
allow: boolean;
|
||||||
|
|
||||||
|
constructor(allow: boolean) {
|
||||||
|
this.allow = allow;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<Authorization>;
|
||||||
|
}
|
|
@ -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<Authorization> {
|
||||||
|
return this._decisionMaker.decide(
|
||||||
|
decisionQuery.uuid,
|
||||||
|
decisionQuery.domain,
|
||||||
|
decisionQuery.action,
|
||||||
|
decisionQuery.context,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,18 +1,18 @@
|
||||||
import { createMap, Mapper } from '@automapper/core';
|
import { createMap, Mapper } from '@automapper/core';
|
||||||
import { AutomapperProfile, InjectMapper } from '@automapper/nestjs';
|
import { AutomapperProfile, InjectMapper } from '@automapper/nestjs';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { AuthPresenter } from '../adapters/primaries/auth.presenter';
|
import { AuthorizationPresenter } from '../adapters/primaries/authorization.presenter';
|
||||||
import { Auth } from '../domain/entities/auth';
|
import { Authorization } from '../domain/entities/authorization';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthProfile extends AutomapperProfile {
|
export class AuthorizationProfile extends AutomapperProfile {
|
||||||
constructor(@InjectMapper() mapper: Mapper) {
|
constructor(@InjectMapper() mapper: Mapper) {
|
||||||
super(mapper);
|
super(mapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
override get profile() {
|
override get profile() {
|
||||||
return (mapper: any) => {
|
return (mapper: any) => {
|
||||||
createMap(mapper, Auth, AuthPresenter);
|
createMap(mapper, Authorization, AuthorizationPresenter);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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<ContextItem>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
uuid: string,
|
||||||
|
domain: Domain,
|
||||||
|
action: Action,
|
||||||
|
context?: Array<ContextItem>,
|
||||||
|
) {
|
||||||
|
this.uuid = uuid;
|
||||||
|
this.domain = domain;
|
||||||
|
this.action = action;
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>(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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -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>(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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,10 +1,10 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { AuthRepository } from '../auth/adapters/secondaries/auth.repository';
|
import { AuthenticationRepository } from '../authentication/adapters/secondaries/authentication.repository';
|
||||||
import { UsernameRepository } from '../auth/adapters/secondaries/username.repository';
|
import { UsernameRepository } from '../authentication/adapters/secondaries/username.repository';
|
||||||
import { PrismaService } from './src/adapters/secondaries/prisma-service';
|
import { PrismaService } from './src/adapters/secondaries/prisma-service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [PrismaService, AuthRepository, UsernameRepository],
|
providers: [PrismaService, AuthenticationRepository, UsernameRepository],
|
||||||
exports: [PrismaService, AuthRepository, UsernameRepository],
|
exports: [PrismaService, AuthenticationRepository, UsernameRepository],
|
||||||
})
|
})
|
||||||
export class DatabaseModule {}
|
export class DatabaseModule {}
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
import { PrismaRepository } from '../adapters/secondaries/prisma-repository.abstract';
|
import { PrismaRepository } from '../adapters/secondaries/prisma-repository.abstract';
|
||||||
|
|
||||||
export class AuthNZRepository<T> extends PrismaRepository<T> {}
|
export class AuthRepository<T> extends PrismaRepository<T> {}
|
Loading…
Reference in New Issue