Compare commits
5 Commits
24c184a19f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 95ef52ed8b | |||
| fe02210c34 | |||
| 68f43b9bdf | |||
| 3d8d23ed69 | |||
| 223558d480 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -4,7 +4,7 @@
|
|||||||
/build
|
/build
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
logs
|
# logs # This should be allowed because of the entity called `logs`
|
||||||
*.log
|
*.log
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
pnpm-debug.log*
|
pnpm-debug.log*
|
||||||
|
|||||||
72
README.md
72
README.md
@@ -13,14 +13,82 @@ $ npm install
|
|||||||
|
|
||||||
## Running local infrastructure
|
## Running local infrastructure
|
||||||
|
|
||||||
To simplify development, all local infrastructure is handled with Docker. The folder `dev_infra` contains the folloeing:
|
To simplify development, all local infrastructure is handled with Docker. The folder `dev_infra` contains the following:
|
||||||
|
|
||||||
- `compose.yaml`: A Docker compose file that stands up a Postgres database and a pgAdmin instance to visualize.
|
- `compose.yaml`: A Docker compose file that stands up a Postgres database and a pgAdmin instance to visualize.
|
||||||
|
|
||||||
|
To start up the local infrastructure, open a new terminal session, navigate to the `dev_infra` folder, and run the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# start up the docker instances for the Postgres database and the pgAdmin interface
|
||||||
|
$ docker compose up
|
||||||
|
|
||||||
|
# use 'ctrl + c' on the keyboard to close the Docker view
|
||||||
|
```
|
||||||
|
|
||||||
|
Once finished, use the following command to tear down the local infrastructure:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# tear down the docker instances
|
||||||
|
$ docker compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
|
||||||
|
To test authentication in development, a dev cognito pool was created. Its' important values are included in the `set-env.sh` file. The pool consists of the following users:
|
||||||
|
|
||||||
|
- email: jesse.desjardins@ccascoe.org, username: 5c6d4528-5021-704a-27fd-68342d44783b, password: DuckTester25!
|
||||||
|
|
||||||
|
An access token will be required to test the routes. A safe way of getting one is as follows:
|
||||||
|
|
||||||
|
1. Logging into the default [Cognito Login page](https://ca-central-1ea62zdmoc.auth.ca-central-1.amazoncognito.com/login/continue?client_id=7ph76km1h1v4vkt4rj9c3s4o6&redirect_uri=https%3A%2F%2Fd84l1y8p4kdic.cloudfront.net&response_type=code&scope=email+openid+phone) using that URL, as it has the `openid` scope included.
|
||||||
|
|
||||||
|
2. After logging in, the URL will have a `code` parameter appended at then end. Use that code in the following curl command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST \
|
||||||
|
https://ca-central-1ea62zdmoc.auth.ca-central-1.amazoncognito.com/oauth2/token \
|
||||||
|
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||||
|
-d "grant_type=authorization_code" \
|
||||||
|
-d "client_id=7ph76km1h1v4vkt4rj9c3s4o6" \
|
||||||
|
-d "code=<enter code here>" \
|
||||||
|
-d "redirect_uri=https://d84l1y8p4kdic.cloudfront.net"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. The curl command will return, among other things, an `access_token` value. That token will be valid for 1 hour and will work for manual tests of the API if included in all API calls.
|
||||||
|
|
||||||
|
#### Alternative ways of retrieving tokens
|
||||||
|
|
||||||
|
Using the bolow command will get a token from the AWS CLI, but it will not have the required "openId" scope and will fail authorization in the API:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
aws cognito-idp admin-initiate-auth --user-pool-id "ca-central-1_ea62zDmOC" --client-id "7ph76km1h1v4vkt4rj9c3s4o6" --auth-flow "ADMIN_USER_PASSWORD_AUTH" --auth-parameters "USERNAME=5c6d4528-5021-704a-27fd-68342d44783b,PASSWORD=DuckTester25!"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Migrations
|
||||||
|
|
||||||
|
Any time a change is made to a `.entity.ts` file, a new migration will need to be generated and added to the `appDataSource.ts` file. This ensures the API is refencing the most up-to-date (and valid!) database schema. These are the migration commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# generate migration
|
||||||
|
$ npm run db:gen_migration
|
||||||
|
|
||||||
|
# generate migration with a custom name
|
||||||
|
$ npm run db:gen_migration_named --name=<any_name_here>
|
||||||
|
|
||||||
|
# show migrations
|
||||||
|
$ npm run db:show_migrations
|
||||||
|
|
||||||
|
# run migrations
|
||||||
|
$ npm run db:run_migrations
|
||||||
|
```
|
||||||
|
|
||||||
|
**NOTE ON MIGRATIONS IN TYPEORM**: In order for any new migration to be considered in the `db:show_migrations` or `db:run_migrations` commands, they'll need to be added to the `appDataSource.ts` file, under the `migrations: []` option.
|
||||||
|
|
||||||
## Compile and run the project
|
## Compile and run the project
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Set env variables
|
# Set env variables, which default to local infrastructure
|
||||||
$ . ./set-env
|
$ . ./set-env
|
||||||
|
|
||||||
# development
|
# development
|
||||||
|
|||||||
94
package-lock.json
generated
94
package-lock.json
generated
@@ -9,11 +9,13 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@nestjs/axios": "^4.0.1",
|
||||||
"@nestjs/common": "^11.0.1",
|
"@nestjs/common": "^11.0.1",
|
||||||
"@nestjs/config": "^4.0.2",
|
"@nestjs/config": "^4.0.2",
|
||||||
"@nestjs/core": "^11.0.1",
|
"@nestjs/core": "^11.0.1",
|
||||||
"@nestjs/platform-express": "^11.0.1",
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
"@nestjs/typeorm": "^11.0.0",
|
"@nestjs/typeorm": "^11.0.0",
|
||||||
|
"axios": "^1.13.4",
|
||||||
"pg": "^8.18.0",
|
"pg": "^8.18.0",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
@@ -1354,9 +1356,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@isaacs/brace-expansion": {
|
"node_modules/@isaacs/brace-expansion": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz",
|
||||||
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
|
"integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2075,6 +2077,17 @@
|
|||||||
"@tybys/wasm-util": "^0.10.0"
|
"@tybys/wasm-util": "^0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@nestjs/axios": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-68pFJgu+/AZbWkGu65Z3r55bTsCPlgyKaV4BSG8yUAD72q1PPuyVRgUwFv6BxdnibTUHlyxm06FmYWNC+bjN7A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@nestjs/common": "^10.0.0 || ^11.0.0",
|
||||||
|
"axios": "^1.3.1",
|
||||||
|
"rxjs": "^7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@nestjs/cli": {
|
"node_modules/@nestjs/cli": {
|
||||||
"version": "11.0.16",
|
"version": "11.0.16",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-11.0.16.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-11.0.16.tgz",
|
||||||
@@ -2152,14 +2165,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@nestjs/config": {
|
"node_modules/@nestjs/config": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/config/-/config-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/config/-/config-4.0.3.tgz",
|
||||||
"integrity": "sha512-McMW6EXtpc8+CwTUwFdg6h7dYcBUpH5iUILCclAsa+MbCEvC9ZKu4dCHRlJqALuhjLw97pbQu62l4+wRwGeZqA==",
|
"integrity": "sha512-FQ3M3Ohqfl+nHAn5tp7++wUQw0f2nAk+SFKe8EpNRnIifPqvfJP6JQxPKtFLMOHbyer4X646prFG4zSRYEssQQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dotenv": "16.4.7",
|
"dotenv": "17.2.3",
|
||||||
"dotenv-expand": "12.0.1",
|
"dotenv-expand": "12.0.3",
|
||||||
"lodash": "4.17.21"
|
"lodash": "4.17.23"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@nestjs/common": "^10.0.0 || ^11.0.0",
|
"@nestjs/common": "^10.0.0 || ^11.0.0",
|
||||||
@@ -2167,9 +2180,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@nestjs/config/node_modules/dotenv": {
|
"node_modules/@nestjs/config/node_modules/dotenv": {
|
||||||
"version": "16.4.7",
|
"version": "17.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
|
||||||
"integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
|
"integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
@@ -2178,12 +2191,6 @@
|
|||||||
"url": "https://dotenvx.com"
|
"url": "https://dotenvx.com"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@nestjs/config/node_modules/lodash": {
|
|
||||||
"version": "4.17.21",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
|
||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@nestjs/core": {
|
"node_modules/@nestjs/core": {
|
||||||
"version": "11.1.12",
|
"version": "11.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.12.tgz",
|
||||||
@@ -3792,7 +3799,6 @@
|
|||||||
"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,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/available-typed-arrays": {
|
"node_modules/available-typed-arrays": {
|
||||||
@@ -3810,6 +3816,17 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/axios": {
|
||||||
|
"version": "1.13.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz",
|
||||||
|
"integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.15.6",
|
||||||
|
"form-data": "^4.0.4",
|
||||||
|
"proxy-from-env": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/babel-jest": {
|
"node_modules/babel-jest": {
|
||||||
"version": "30.2.0",
|
"version": "30.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz",
|
||||||
@@ -4417,7 +4434,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,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"delayed-stream": "~1.0.0"
|
"delayed-stream": "~1.0.0"
|
||||||
@@ -4702,7 +4718,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,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
@@ -4761,9 +4776,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/dotenv-expand": {
|
"node_modules/dotenv-expand": {
|
||||||
"version": "12.0.1",
|
"version": "12.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.3.tgz",
|
||||||
"integrity": "sha512-LaKRbou8gt0RNID/9RoI+J2rvXsBRPMV7p+ElHlPhcSARbCPDYcYG2s1TIzAfWv4YSgyY5taidWzzs31lNV3yQ==",
|
"integrity": "sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==",
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dotenv": "^16.4.5"
|
"dotenv": "^16.4.5"
|
||||||
@@ -4901,7 +4916,6 @@
|
|||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
@@ -5459,6 +5473,26 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/follow-redirects": {
|
||||||
|
"version": "1.15.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||||
|
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"debug": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/for-each": {
|
"node_modules/for-each": {
|
||||||
"version": "0.3.5",
|
"version": "0.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
|
||||||
@@ -5522,7 +5556,6 @@
|
|||||||
"version": "4.0.5",
|
"version": "4.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
||||||
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"asynckit": "^0.4.0",
|
"asynckit": "^0.4.0",
|
||||||
@@ -5539,7 +5572,6 @@
|
|||||||
"version": "1.52.0",
|
"version": "1.52.0",
|
||||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
@@ -5549,7 +5581,6 @@
|
|||||||
"version": "2.1.35",
|
"version": "2.1.35",
|
||||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mime-db": "1.52.0"
|
"mime-db": "1.52.0"
|
||||||
@@ -7228,7 +7259,6 @@
|
|||||||
"version": "4.17.23",
|
"version": "4.17.23",
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
|
||||||
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
|
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/lodash.memoize": {
|
"node_modules/lodash.memoize": {
|
||||||
@@ -8254,6 +8284,12 @@
|
|||||||
"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==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/punycode": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
|
|||||||
@@ -24,15 +24,15 @@
|
|||||||
"db:show_migrations": "ts-node ./node_modules/typeorm/cli.js migration:show -d src/appDataSource.ts",
|
"db:show_migrations": "ts-node ./node_modules/typeorm/cli.js migration:show -d src/appDataSource.ts",
|
||||||
"db:run_migrations": "ts-node ./node_modules/typeorm/cli.js migration:run -d src/appDataSource.ts",
|
"db:run_migrations": "ts-node ./node_modules/typeorm/cli.js migration:run -d src/appDataSource.ts",
|
||||||
"db:run_migrations_prod": "ts-node ./node_modules/typeorm/cli.js migration:run -d dist/appDataSource.ts"
|
"db:run_migrations_prod": "ts-node ./node_modules/typeorm/cli.js migration:run -d dist/appDataSource.ts"
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@nestjs/axios": "^4.0.1",
|
||||||
"@nestjs/common": "^11.0.1",
|
"@nestjs/common": "^11.0.1",
|
||||||
"@nestjs/config": "^4.0.2",
|
"@nestjs/config": "^4.0.2",
|
||||||
"@nestjs/core": "^11.0.1",
|
"@nestjs/core": "^11.0.1",
|
||||||
"@nestjs/platform-express": "^11.0.1",
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
"@nestjs/typeorm": "^11.0.0",
|
"@nestjs/typeorm": "^11.0.0",
|
||||||
|
"axios": "^1.13.4",
|
||||||
"pg": "^8.18.0",
|
"pg": "^8.18.0",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# Databse connection info
|
||||||
export DB_TYPE=postgres
|
export DB_TYPE=postgres
|
||||||
export DB_HOST=localhost
|
export DB_HOST=localhost
|
||||||
export DB_PORT=5432
|
export DB_PORT=5432
|
||||||
@@ -5,4 +6,10 @@ export DB_USERNAME=glossary_dev_user
|
|||||||
export DB_PASSWORD=glossary_dev_password
|
export DB_PASSWORD=glossary_dev_password
|
||||||
export DB_NAME=glossary_dev_db
|
export DB_NAME=glossary_dev_db
|
||||||
|
|
||||||
|
# Authentication server info
|
||||||
|
export AUTH_USER_POOL_ID=ca-central-1_ea62zDmOC
|
||||||
|
export AUTH_REGION=ca-central-1
|
||||||
|
export AUTH_BASE_URL=https://ca-central-1ea62zdmoc.auth.ca-central-1.amazoncognito.com
|
||||||
|
export AUTH_APP_CLIENT_ID=7ph76km1h1v4vkt4rj9c3s4o6
|
||||||
|
|
||||||
npm run db:run_migrations
|
npm run db:run_migrations
|
||||||
@@ -7,12 +7,16 @@ import { AppService } from './app.service';
|
|||||||
import { dataSourceOptions } from './appDataSource';
|
import { dataSourceOptions } from './appDataSource';
|
||||||
import { EntryModule } from './entries/entries.module';
|
import { EntryModule } from './entries/entries.module';
|
||||||
import { DataSource, EntityManager } from 'typeorm';
|
import { DataSource, EntityManager } from 'typeorm';
|
||||||
|
import { UsersModule } from './users/users.module';
|
||||||
|
import { LogsModule } from './logs/logs.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule.forRoot(),
|
ConfigModule.forRoot(),
|
||||||
TypeOrmModule.forRoot(dataSourceOptions),
|
TypeOrmModule.forRoot(dataSourceOptions),
|
||||||
EntryModule
|
EntryModule,
|
||||||
|
UsersModule,
|
||||||
|
LogsModule,
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService],
|
providers: [AppService],
|
||||||
|
|||||||
@@ -1,18 +1,27 @@
|
|||||||
import { DataSource, DataSourceOptions } from "typeorm";
|
import { DataSource, DataSourceOptions } from 'typeorm';
|
||||||
import { Entry } from "./entries/entries.entity";
|
import { Entry } from './entries/entries.entity';
|
||||||
import { Initial1770020781006 } from "./migrations/1770020781006-initial";
|
import { Initial1770020781006 } from './migrations/1770020781006-initial';
|
||||||
import { AddCreateUpdateDeleteColsAndRenameSourceToSourceText1770067407619 } from "./migrations/1770067407619-add_create_update_delete_cols_and_rename_source_to_sourceText";
|
import { AddCreateUpdateDeleteColsAndRenameSourceToSourceText1770067407619 } from './migrations/1770067407619-add_create_update_delete_cols_and_rename_source_to_sourceText';
|
||||||
|
import { User } from './users/users.entity';
|
||||||
|
import { AddUserEntity1770147074119 } from './migrations/1770147074119-add_user_entity';
|
||||||
|
import { Log } from './logs/logs.entity';
|
||||||
|
import { AddLogEntity1770232943778 } from './migrations/1770232943778-add_log_entity';
|
||||||
|
|
||||||
export const dataSourceOptions: DataSourceOptions = {
|
export const dataSourceOptions: DataSourceOptions = {
|
||||||
type: "postgres",
|
type: 'postgres',
|
||||||
host: process.env.DB_HOST,
|
host: process.env.DB_HOST,
|
||||||
port: parseInt(process.env.DB_PORT as string, 10) || 5432,
|
port: parseInt(process.env.DB_PORT as string, 10) || 5432,
|
||||||
username: process.env.DB_USERNAME,
|
username: process.env.DB_USERNAME,
|
||||||
password: process.env.DB_PASSWORD,
|
password: process.env.DB_PASSWORD,
|
||||||
database: process.env.DB_NAME,
|
database: process.env.DB_NAME,
|
||||||
entities: [Entry],
|
entities: [Entry, User, Log],
|
||||||
migrations: [Initial1770020781006, AddCreateUpdateDeleteColsAndRenameSourceToSourceText1770067407619],
|
migrations: [
|
||||||
|
Initial1770020781006,
|
||||||
|
AddCreateUpdateDeleteColsAndRenameSourceToSourceText1770067407619,
|
||||||
|
AddUserEntity1770147074119,
|
||||||
|
AddLogEntity1770232943778,
|
||||||
|
],
|
||||||
synchronize: false,
|
synchronize: false,
|
||||||
}
|
};
|
||||||
|
|
||||||
export const appDataSource = new DataSource(dataSourceOptions)
|
export const appDataSource = new DataSource(dataSourceOptions);
|
||||||
|
|||||||
91
src/authentication/authentication.guard.ts
Normal file
91
src/authentication/authentication.guard.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import {
|
||||||
|
CanActivate,
|
||||||
|
ExecutionContext,
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
Injectable,
|
||||||
|
UnauthorizedException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { AuthenticationService } from './authentication.service';
|
||||||
|
import { LogsService } from 'src/logs/logs.service';
|
||||||
|
import { Request } from 'express';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthenticationGuard implements CanActivate {
|
||||||
|
constructor(
|
||||||
|
private readonly authenticationService: AuthenticationService,
|
||||||
|
private readonly logger: LogsService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used by the guard attached to a controller's route. Determines wether the current request is allowed to proceed.
|
||||||
|
*
|
||||||
|
* @param context
|
||||||
|
*/
|
||||||
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
|
const request = context.switchToHttp().getRequest();
|
||||||
|
const token = this.extractTokenFromHeader(request);
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
const exception = new UnauthorizedException(
|
||||||
|
'Authrorization: Bearer <token> header invalid',
|
||||||
|
);
|
||||||
|
this.logger.error(
|
||||||
|
exception.message,
|
||||||
|
exception.stack,
|
||||||
|
'AuthenticationGuard',
|
||||||
|
);
|
||||||
|
throw exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// store the user on the request object if we want to retrieve it from the controllers
|
||||||
|
request['user'] =
|
||||||
|
await this.authenticationService.authenticateWithCognito(token);
|
||||||
|
} catch {
|
||||||
|
const exception = new UnauthorizedException();
|
||||||
|
this.logger.error(
|
||||||
|
exception.message,
|
||||||
|
exception.stack,
|
||||||
|
'AuthenticationGuard',
|
||||||
|
);
|
||||||
|
throw exception;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractTokenFromHeader(request: Request): string | undefined {
|
||||||
|
const authHeader: string | null = '';
|
||||||
|
const [type, token] = request.headers.authorization?.split(' ') ?? [];
|
||||||
|
|
||||||
|
if (!type) {
|
||||||
|
const exception = new HttpException(
|
||||||
|
'Authrorization: Bearer <token> header missing',
|
||||||
|
HttpStatus.UNAUTHORIZED,
|
||||||
|
);
|
||||||
|
this.logger.error(
|
||||||
|
exception.message,
|
||||||
|
exception.stack,
|
||||||
|
'AuthenticationGuard',
|
||||||
|
);
|
||||||
|
throw exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type !== 'Bearer') {
|
||||||
|
const exception = new HttpException(
|
||||||
|
'Authrorization: Bearer <token> header invalid',
|
||||||
|
HttpStatus.UNAUTHORIZED,
|
||||||
|
);
|
||||||
|
this.logger.error(
|
||||||
|
exception.message,
|
||||||
|
exception.stack,
|
||||||
|
'AuthenticationGuard',
|
||||||
|
);
|
||||||
|
throw exception;
|
||||||
|
}
|
||||||
|
return type === 'Bearer' ? token : undefined;
|
||||||
|
|
||||||
|
// const [type, token] = request.headers.authorization?.split(' ') ?? [];
|
||||||
|
// return type === 'Bearer' ? token : undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/authentication/authentication.module.ts
Normal file
12
src/authentication/authentication.module.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { HttpModule } from '@nestjs/axios';
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { AuthenticationGuard } from './authentication.guard';
|
||||||
|
import { AuthenticationService } from './authentication.service';
|
||||||
|
import { LogsModule } from 'src/logs/logs.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [HttpModule, LogsModule],
|
||||||
|
providers: [AuthenticationGuard, AuthenticationService],
|
||||||
|
exports: [AuthenticationService],
|
||||||
|
})
|
||||||
|
export class AuthenticationModule {}
|
||||||
76
src/authentication/authentication.service.ts
Normal file
76
src/authentication/authentication.service.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { HttpService } from '@nestjs/axios';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { lastValueFrom } from 'rxjs';
|
||||||
|
import { LogsService } from 'src/logs/logs.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error thrown when an authentication check has failed
|
||||||
|
*/
|
||||||
|
export class AuthenticationError extends Error {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple user class that holds key info for reference later
|
||||||
|
*/
|
||||||
|
export class AuthUser {
|
||||||
|
/**
|
||||||
|
* Identifying user ID (i.e. "sub" field in AWS Cognito)
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User's customized username
|
||||||
|
*/
|
||||||
|
username: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthenticationService {
|
||||||
|
constructor(
|
||||||
|
private httpService: HttpService,
|
||||||
|
private readonly logger: LogsService,
|
||||||
|
) {
|
||||||
|
this.baseURL = process.env.AUTH_BASE_URL;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* The base URL of the service being used for authentication
|
||||||
|
*/
|
||||||
|
private readonly baseURL: string | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls the OpenID Connect UserInfo endpoint.
|
||||||
|
*
|
||||||
|
* If the call succeeds, the token is valid and the user info is returned in the response.
|
||||||
|
* If the call fails, the token is either invalid or expired.
|
||||||
|
*
|
||||||
|
* @param accessToken Access token passed into the HTTP call for authentication
|
||||||
|
*/
|
||||||
|
async authenticateWithCognito(accessToken: string): Promise<AuthUser> {
|
||||||
|
const authUrl = `${this.baseURL}/oauth2/userInfo`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = this.httpService.get(authUrl, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const responseData = await lastValueFrom(response);
|
||||||
|
await this.logger.log(
|
||||||
|
`Access attempted with resolved user: ${responseData.data.sub}`,
|
||||||
|
'AuthenticationService',
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: responseData?.data.sub ?? '',
|
||||||
|
username: responseData?.data.preferred_username ?? '',
|
||||||
|
};
|
||||||
|
} catch (err: any) {
|
||||||
|
const authErr = new AuthenticationError(err.message);
|
||||||
|
await this.logger.error(
|
||||||
|
authErr.message,
|
||||||
|
authErr.stack,
|
||||||
|
'AuthenticationService',
|
||||||
|
);
|
||||||
|
throw authErr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,50 +1,68 @@
|
|||||||
import { Body, Controller, Delete, Get, Inject, Param, Post, Put } from '@nestjs/common';
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Delete,
|
||||||
|
Get,
|
||||||
|
Inject,
|
||||||
|
Param,
|
||||||
|
Post,
|
||||||
|
Put,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
import { EntryService } from './entries.service';
|
import { EntryService } from './entries.service';
|
||||||
import { EntryDTO } from './entries.dto';
|
import { EntryDTO } from './entries.dto';
|
||||||
import { Entry } from './entries.entity';
|
import { Entry } from './entries.entity';
|
||||||
|
import { AuthenticationGuard } from 'src/authentication/authentication.guard';
|
||||||
|
|
||||||
@Controller('entries')
|
@Controller('entries')
|
||||||
export class EntriesController {
|
export class EntriesController {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(EntryService)
|
@Inject(EntryService)
|
||||||
private readonly entryService: EntryService
|
private readonly entryService: EntryService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
|
@UseGuards(AuthenticationGuard)
|
||||||
async saveEntry(@Body() entry: EntryDTO): Promise<string | undefined> {
|
async saveEntry(@Body() entry: EntryDTO): Promise<string | undefined> {
|
||||||
return (await this.entryService.save(entry)).uuid
|
return (await this.entryService.save(entry)).uuid;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put(":uuid")
|
@Put(':uuid')
|
||||||
async updateEntry(@Param("uuid") uuid: string, @Body() entry: EntryDTO): Promise<string | undefined> {
|
@UseGuards(AuthenticationGuard)
|
||||||
return (await this.entryService.updateByUuid(uuid, entry)).uuid
|
async updateEntry(
|
||||||
|
@Param('uuid') uuid: string,
|
||||||
|
@Body() entry: EntryDTO,
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
return (await this.entryService.updateByUuid(uuid, entry)).uuid;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
async findAll(): Promise<Entry[]>
|
@UseGuards(AuthenticationGuard)
|
||||||
{
|
async findAll(): Promise<Entry[]> {
|
||||||
const entries = await this.entryService.findAll()
|
const entries = await this.entryService.findAll();
|
||||||
return entries.sort((a, b) => {
|
return entries.sort((a, b) => {
|
||||||
return (
|
return (
|
||||||
new Date(b.created_at as Date).getTime() - new Date(a.created_at as Date).getTime()
|
new Date(b.created_at as Date).getTime() -
|
||||||
)
|
new Date(a.created_at as Date).getTime()
|
||||||
})
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(":uuid")
|
@Get(':uuid')
|
||||||
async findOneByUuid(@Param("uuid") uuid: string): Promise<Entry | null> {
|
@UseGuards(AuthenticationGuard)
|
||||||
return await this.entryService.findOneByUuid(uuid)
|
async findOneByUuid(@Param('uuid') uuid: string): Promise<Entry | null> {
|
||||||
|
return await this.entryService.findOneByUuid(uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(":uuid")
|
@Delete(':uuid')
|
||||||
async softDelete(@Param("uuid") uuid: string): Promise<void>
|
@UseGuards(AuthenticationGuard)
|
||||||
{
|
async softDelete(@Param('uuid') uuid: string): Promise<void> {
|
||||||
await this.entryService.softDeleteByUuid(uuid)
|
await this.entryService.softDeleteByUuid(uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put("/restore/:uuid")
|
@Put('/restore/:uuid')
|
||||||
async restoreSoftDeleted(@Param("uuid") uuid: string): Promise<Entry | null> {
|
@UseGuards(AuthenticationGuard)
|
||||||
return await this.entryService.restoreDeletedByUuid(uuid)
|
async restoreSoftDeleted(@Param('uuid') uuid: string): Promise<Entry | null> {
|
||||||
|
return await this.entryService.restoreDeletedByUuid(uuid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Entry } from "./entries.entity";
|
import { Entry } from './entries.entity';
|
||||||
|
|
||||||
export class EntryDTO implements Partial<Entry> {
|
export class EntryDTO implements Partial<Entry> {
|
||||||
title?: string;
|
title?: string;
|
||||||
|
|||||||
@@ -1,24 +1,32 @@
|
|||||||
import { Column, CreateDateColumn, DeleteDateColumn, Entity, Generated, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm";
|
import {
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
DeleteDateColumn,
|
||||||
|
Entity,
|
||||||
|
Generated,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
|
||||||
@Entity({ name: "entries"})
|
@Entity({ name: 'entries' })
|
||||||
export class Entry {
|
export class Entry {
|
||||||
@Column()
|
@Column()
|
||||||
@Generated("uuid")
|
@Generated('uuid')
|
||||||
uuid?: string;
|
uuid?: string;
|
||||||
|
|
||||||
@PrimaryGeneratedColumn("increment")
|
@PrimaryGeneratedColumn('increment')
|
||||||
id?: number;
|
id?: number;
|
||||||
|
|
||||||
@Column({ type: String, default: "" })
|
@Column({ type: String, default: '' })
|
||||||
title?: string;
|
title?: string;
|
||||||
|
|
||||||
@Column({ type: String, default: "" })
|
@Column({ type: String, default: '' })
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
||||||
@Column({ type: String, default: "" })
|
@Column({ type: String, default: '' })
|
||||||
sourceText?: string;
|
sourceText?: string;
|
||||||
|
|
||||||
@Column({ type: String, default: "" })
|
@Column({ type: String, default: '' })
|
||||||
sourceUrl?: string;
|
sourceUrl?: string;
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import { Module } from "@nestjs/common";
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from "@nestjs/typeorm";
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { Entry } from "./entries.entity";
|
import { Entry } from './entries.entity';
|
||||||
import { EntryService } from "./entries.service";
|
import { EntryService } from './entries.service';
|
||||||
import { EntriesController } from "./entries.controller";
|
import { EntriesController } from './entries.controller';
|
||||||
|
import { LogsModule } from 'src/logs/logs.module';
|
||||||
|
import { AuthenticationModule } from 'src/authentication/authentication.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([Entry]),
|
TypeOrmModule.forFeature([Entry]),
|
||||||
|
LogsModule,
|
||||||
|
AuthenticationModule,
|
||||||
],
|
],
|
||||||
exports: [EntryService],
|
exports: [EntryService],
|
||||||
providers: [EntryService],
|
providers: [EntryService],
|
||||||
|
|||||||
@@ -1,85 +1,104 @@
|
|||||||
import { Injectable, Logger } from "@nestjs/common";
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from "@nestjs/typeorm";
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Entry } from "./entries.entity";
|
import { Entry } from './entries.entity';
|
||||||
import { Repository } from "typeorm";
|
import { Repository } from 'typeorm';
|
||||||
import { EntryDTO } from "./entries.dto";
|
import { EntryDTO } from './entries.dto';
|
||||||
import EntryNotFoundException from "./exceptions/entryNotFound.exception";
|
import EntryNotFoundException from './exceptions/entryNotFound.exception';
|
||||||
|
import { LogsService } from 'src/logs/logs.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class EntryService {
|
export class EntryService {
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(Entry)
|
@InjectRepository(Entry)
|
||||||
private entryRepository: Repository<Entry>
|
private entryRepository: Repository<Entry>,
|
||||||
|
private readonly logger: LogsService,
|
||||||
) {}
|
) {}
|
||||||
private readonly logger = new Logger(EntryService.name)
|
|
||||||
|
|
||||||
async save(data: EntryDTO): Promise<Entry> {
|
async save(data: EntryDTO): Promise<Entry> {
|
||||||
this.logger.log("Creating entry")
|
await this.logger.log('Creating entry', 'EntryService');
|
||||||
const lEntry = new Entry();
|
const lEntry = new Entry();
|
||||||
lEntry.description = data.description
|
lEntry.description = data.description;
|
||||||
lEntry.sourceText = data.sourceText
|
lEntry.sourceText = data.sourceText;
|
||||||
lEntry.sourceUrl = data.sourceUrl
|
lEntry.sourceUrl = data.sourceUrl;
|
||||||
lEntry.title = data.title
|
lEntry.title = data.title;
|
||||||
await this.entryRepository.save(lEntry)
|
await this.entryRepository.save(lEntry);
|
||||||
return lEntry
|
return lEntry;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateByUuid(uuid: string, data: EntryDTO): Promise<Entry> {
|
async updateByUuid(uuid: string, data: EntryDTO): Promise<Entry> {
|
||||||
this.logger.log(`Updating entry with UUID ${uuid}`)
|
await this.logger.log(`Updating entry with UUID ${uuid}`, 'EntryService');
|
||||||
const oldEntry = await this.entryRepository.findOneBy({ uuid: uuid})
|
const oldEntry = await this.entryRepository.findOneBy({ uuid: uuid });
|
||||||
if (!oldEntry) {
|
if (!oldEntry) {
|
||||||
throw new EntryNotFoundException(uuid)
|
throw new EntryNotFoundException(uuid);
|
||||||
} else {
|
} else {
|
||||||
const lEntry = new Entry();
|
const lEntry = new Entry();
|
||||||
// `id` is the primary key
|
// `id` is the primary key
|
||||||
lEntry.id = oldEntry.id
|
lEntry.id = oldEntry.id;
|
||||||
// These options can be updated
|
// These options can be updated
|
||||||
lEntry.description = data.description
|
lEntry.description = data.description;
|
||||||
lEntry.sourceText = data.sourceText
|
lEntry.sourceText = data.sourceText;
|
||||||
lEntry.sourceUrl = data.sourceUrl
|
lEntry.sourceUrl = data.sourceUrl;
|
||||||
lEntry.title = data.title
|
lEntry.title = data.title;
|
||||||
await this.entryRepository.save(lEntry)
|
await this.entryRepository.save(lEntry);
|
||||||
return lEntry
|
return lEntry;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOneByUuid(uuid: string): Promise<Entry | null> {
|
async findOneByUuid(uuid: string): Promise<Entry | null> {
|
||||||
this.logger.log(`Returning entry with UUID ${uuid}`)
|
await this.logger.log(`Returning entry with UUID ${uuid}`, 'EntryService');
|
||||||
return this.entryRepository.findOneByOrFail({uuid: uuid})
|
return this.entryRepository.findOneByOrFail({ uuid: uuid });
|
||||||
}
|
}
|
||||||
|
|
||||||
async findAll(): Promise<Entry[]> {
|
async findAll(): Promise<Entry[]> {
|
||||||
this.logger.log("Returning all entries")
|
await this.logger.log('Returning all entries', 'EntryService');
|
||||||
return this.entryRepository.find()
|
return this.entryRepository.find();
|
||||||
}
|
}
|
||||||
|
|
||||||
async findAllAndDeleted(): Promise<Entry[]> {
|
async findAllAndDeleted(): Promise<Entry[]> {
|
||||||
this.logger.log("Returning all entries, active and deleted")
|
await this.logger.log(
|
||||||
return this.entryRepository.find({ withDeleted: true })
|
'Returning all entries, active and deleted',
|
||||||
|
'EntryService',
|
||||||
|
);
|
||||||
|
return this.entryRepository.find({ withDeleted: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
async softDeleteByUuid(uuid: string): Promise<void> {
|
async softDeleteByUuid(uuid: string): Promise<void> {
|
||||||
this.logger.log(`Soft deleting entry with UUID ${uuid}`)
|
await this.logger.log(
|
||||||
|
`Soft deleting entry with UUID ${uuid}`,
|
||||||
|
'EntryService',
|
||||||
|
);
|
||||||
const deleteResponse = await this.entryRepository.softDelete({
|
const deleteResponse = await this.entryRepository.softDelete({
|
||||||
uuid: uuid
|
uuid: uuid,
|
||||||
})
|
});
|
||||||
if (!deleteResponse.affected) {
|
if (!deleteResponse.affected) {
|
||||||
throw new EntryNotFoundException(uuid)
|
const exception = new EntryNotFoundException(uuid);
|
||||||
|
await this.logger.error(
|
||||||
|
exception.message,
|
||||||
|
exception.stack,
|
||||||
|
'EntryService',
|
||||||
|
);
|
||||||
|
throw exception;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async restoreDeletedByUuid(uuid: string): Promise<Entry | null> {
|
async restoreDeletedByUuid(uuid: string): Promise<Entry | null> {
|
||||||
this.logger.log(`Restoring entry with UUID ${uuid}`)
|
await this.logger.log(`Restoring entry with UUID ${uuid}`, 'EntryService');
|
||||||
const restoreResponse = await this.entryRepository.restore({
|
const restoreResponse = await this.entryRepository.restore({
|
||||||
uuid: uuid
|
uuid: uuid,
|
||||||
})
|
});
|
||||||
|
|
||||||
if (!restoreResponse.affected) {
|
if (!restoreResponse.affected) {
|
||||||
throw new EntryNotFoundException(uuid)
|
const exception = new EntryNotFoundException(uuid);
|
||||||
|
await this.logger.error(
|
||||||
|
exception.message,
|
||||||
|
exception.stack,
|
||||||
|
'EntryService',
|
||||||
|
);
|
||||||
|
throw exception;
|
||||||
} else {
|
} else {
|
||||||
return this.entryRepository.findOneByOrFail({
|
return this.entryRepository.findOneByOrFail({
|
||||||
uuid: uuid
|
uuid: uuid,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { NotFoundException } from "@nestjs/common";
|
import { NotFoundException } from '@nestjs/common';
|
||||||
|
|
||||||
class EntryNotFoundException extends NotFoundException {
|
class EntryNotFoundException extends NotFoundException {
|
||||||
constructor(uuid: string) {
|
constructor(uuid: string) {
|
||||||
super(`Entry with UUID ${uuid} not found`)
|
super(`Entry with UUID ${uuid} not found`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EntryNotFoundException
|
export default EntryNotFoundException;
|
||||||
|
|||||||
24
src/logs/logs.entity.ts
Normal file
24
src/logs/logs.entity.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
|
||||||
|
@Entity({ name: 'logs' })
|
||||||
|
export class Log {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
level: string; // 'log', 'error', 'warn', etc.
|
||||||
|
|
||||||
|
@Column({ type: String })
|
||||||
|
message: string;
|
||||||
|
|
||||||
|
@Column({ type: String, nullable: true })
|
||||||
|
context?: string;
|
||||||
|
|
||||||
|
@CreateDateColumn()
|
||||||
|
timestamp: Date;
|
||||||
|
}
|
||||||
12
src/logs/logs.module.ts
Normal file
12
src/logs/logs.module.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
// src/logs/logs.module.ts
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { Log } from './logs.entity';
|
||||||
|
import { LogsService } from './logs.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([Log])],
|
||||||
|
exports: [LogsService],
|
||||||
|
providers: [LogsService],
|
||||||
|
})
|
||||||
|
export class LogsModule {}
|
||||||
46
src/logs/logs.service.ts
Normal file
46
src/logs/logs.service.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { Injectable, Logger, LoggerService } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { Log } from './logs.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class LogsService extends Logger implements LoggerService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(Log)
|
||||||
|
private readonly logRepository: Repository<Log>,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
async log(message: string, context?: string) {
|
||||||
|
// output to console using default Logger
|
||||||
|
super.log(message, context);
|
||||||
|
// Save in DB using TypeORM
|
||||||
|
await this.saveLog('log', message, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
async error(message: string, stack?: string, context?: string) {
|
||||||
|
super.error(message, stack, context);
|
||||||
|
await this.saveLog('error', `${message} - ${stack || ''}`, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
async warn(message: string, context?: string) {
|
||||||
|
super.warn(message, context);
|
||||||
|
await this.saveLog('warn', message, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
async debug(message: string, context?: string) {
|
||||||
|
super.debug(message, context);
|
||||||
|
await this.saveLog('debug', message, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async saveLog(level: string, message: string, context?: string) {
|
||||||
|
try {
|
||||||
|
const log = this.logRepository.create({ level, message, context });
|
||||||
|
await this.logRepository.save(log);
|
||||||
|
} catch (err) {
|
||||||
|
// maybe have a backup log (local file?) to catch these
|
||||||
|
console.error('Failed to save log to database:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
export class Initial1770020781006 implements MigrationInterface {
|
export class Initial1770020781006 implements MigrationInterface {
|
||||||
name = 'Initial1770020781006'
|
name = 'Initial1770020781006';
|
||||||
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
await queryRunner.query(`CREATE TABLE "entries" ("uuid" uuid NOT NULL DEFAULT uuid_generate_v4(), "id" SERIAL NOT NULL, "title" character varying NOT NULL DEFAULT '', "description" character varying NOT NULL DEFAULT '', "source" character varying NOT NULL DEFAULT '', "sourceUrl" character varying NOT NULL DEFAULT '', CONSTRAINT "PK_8640855ae82083455cbb806173d" PRIMARY KEY ("id"))`);
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "entries" ("uuid" uuid NOT NULL DEFAULT uuid_generate_v4(), "id" SERIAL NOT NULL, "title" character varying NOT NULL DEFAULT '', "description" character varying NOT NULL DEFAULT '', "source" character varying NOT NULL DEFAULT '', "sourceUrl" character varying NOT NULL DEFAULT '', CONSTRAINT "PK_8640855ae82083455cbb806173d" PRIMARY KEY ("id"))`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
await queryRunner.query(`DROP TABLE "entries"`);
|
await queryRunner.query(`DROP TABLE "entries"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
export class AddCreateUpdateDeleteColsAndRenameSourceToSourceText1770067407619 implements MigrationInterface {
|
export class AddCreateUpdateDeleteColsAndRenameSourceToSourceText1770067407619 implements MigrationInterface {
|
||||||
name = 'AddCreateUpdateDeleteColsAndRenameSourceToSourceText1770067407619'
|
name = 'AddCreateUpdateDeleteColsAndRenameSourceToSourceText1770067407619';
|
||||||
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
await queryRunner.query(`ALTER TABLE "entries" DROP COLUMN "source"`);
|
await queryRunner.query(`ALTER TABLE "entries" DROP COLUMN "source"`);
|
||||||
await queryRunner.query(`ALTER TABLE "entries" ADD "sourceText" character varying NOT NULL DEFAULT ''`);
|
await queryRunner.query(
|
||||||
await queryRunner.query(`ALTER TABLE "entries" ADD "created_at" TIMESTAMP NOT NULL DEFAULT now()`);
|
`ALTER TABLE "entries" ADD "sourceText" character varying NOT NULL DEFAULT ''`,
|
||||||
await queryRunner.query(`ALTER TABLE "entries" ADD "updated_at" TIMESTAMP NOT NULL DEFAULT now()`);
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "entries" ADD "created_at" TIMESTAMP NOT NULL DEFAULT now()`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "entries" ADD "updated_at" TIMESTAMP NOT NULL DEFAULT now()`,
|
||||||
|
);
|
||||||
await queryRunner.query(`ALTER TABLE "entries" ADD "deleted_at" TIMESTAMP`);
|
await queryRunner.query(`ALTER TABLE "entries" ADD "deleted_at" TIMESTAMP`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,7 +22,8 @@ export class AddCreateUpdateDeleteColsAndRenameSourceToSourceText1770067407619 i
|
|||||||
await queryRunner.query(`ALTER TABLE "entries" DROP COLUMN "updated_at"`);
|
await queryRunner.query(`ALTER TABLE "entries" DROP COLUMN "updated_at"`);
|
||||||
await queryRunner.query(`ALTER TABLE "entries" DROP COLUMN "created_at"`);
|
await queryRunner.query(`ALTER TABLE "entries" DROP COLUMN "created_at"`);
|
||||||
await queryRunner.query(`ALTER TABLE "entries" DROP COLUMN "sourceText"`);
|
await queryRunner.query(`ALTER TABLE "entries" DROP COLUMN "sourceText"`);
|
||||||
await queryRunner.query(`ALTER TABLE "entries" ADD "source" character varying NOT NULL DEFAULT ''`);
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "entries" ADD "source" character varying NOT NULL DEFAULT ''`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
15
src/migrations/1770147074119-add_user_entity.ts
Normal file
15
src/migrations/1770147074119-add_user_entity.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class AddUserEntity1770147074119 implements MigrationInterface {
|
||||||
|
name = 'AddUserEntity1770147074119';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "users" ("uuid" uuid NOT NULL DEFAULT uuid_generate_v4(), "id" SERIAL NOT NULL, "username" character varying NOT NULL DEFAULT '', "email" character varying NOT NULL DEFAULT '', "firstName" character varying NOT NULL DEFAULT '', "lastName" character varying NOT NULL DEFAULT '', "isAdmin" boolean NOT NULL DEFAULT false, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP, CONSTRAINT "PK_a3ffb1c0c8416b9fc6f907b7433" PRIMARY KEY ("id"))`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`DROP TABLE "users"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/migrations/1770232943778-add_log_entity.ts
Normal file
15
src/migrations/1770232943778-add_log_entity.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class AddLogEntity1770232943778 implements MigrationInterface {
|
||||||
|
name = 'AddLogEntity1770232943778';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "logs" ("id" SERIAL NOT NULL, "level" character varying NOT NULL, "message" character varying NOT NULL, "context" character varying, "timestamp" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_fb1b805f2f7795de79fa69340ba" PRIMARY KEY ("id"))`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`DROP TABLE "logs"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/users/users.controller.spec.ts
Normal file
18
src/users/users.controller.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { UsersController } from './users.controller';
|
||||||
|
|
||||||
|
describe('UsersController', () => {
|
||||||
|
let controller: UsersController;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
controllers: [UsersController],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
controller = module.get<UsersController>(UsersController);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(controller).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
22
src/users/users.controller.ts
Normal file
22
src/users/users.controller.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Controller, Inject } from '@nestjs/common';
|
||||||
|
import { UserService } from './users.service';
|
||||||
|
|
||||||
|
@Controller('users')
|
||||||
|
export class UsersController {
|
||||||
|
constructor(
|
||||||
|
@Inject(UserService)
|
||||||
|
private readonly userService: UserService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
// TODO: POST: save (create new) user
|
||||||
|
|
||||||
|
// TODO: PUT: update user by UUID
|
||||||
|
|
||||||
|
// TODO: GET: find all users
|
||||||
|
|
||||||
|
// TODO: GET: find user by UUID
|
||||||
|
|
||||||
|
// TODO: DELETE: soft delete user
|
||||||
|
|
||||||
|
// TODO: PUT: restore soft deleted user
|
||||||
|
}
|
||||||
9
src/users/users.dto.ts
Normal file
9
src/users/users.dto.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { User } from './users.entity';
|
||||||
|
|
||||||
|
export class UserDTO implements Partial<User> {
|
||||||
|
email?: string;
|
||||||
|
firstName?: string;
|
||||||
|
isAdmin?: boolean;
|
||||||
|
lastName?: string;
|
||||||
|
username?: string;
|
||||||
|
}
|
||||||
43
src/users/users.entity.ts
Normal file
43
src/users/users.entity.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import {
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
DeleteDateColumn,
|
||||||
|
Entity,
|
||||||
|
Generated,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
|
||||||
|
@Entity({ name: 'users' })
|
||||||
|
export class User {
|
||||||
|
@Column()
|
||||||
|
@Generated('uuid')
|
||||||
|
uuid?: string;
|
||||||
|
|
||||||
|
@PrimaryGeneratedColumn('increment')
|
||||||
|
id?: number;
|
||||||
|
|
||||||
|
@Column({ type: String, default: '' })
|
||||||
|
username?: string;
|
||||||
|
|
||||||
|
@Column({ type: String, default: '' })
|
||||||
|
email?: string;
|
||||||
|
|
||||||
|
@Column({ type: String, default: '' })
|
||||||
|
firstName?: string;
|
||||||
|
|
||||||
|
@Column({ type: String, default: '' })
|
||||||
|
lastName?: string;
|
||||||
|
|
||||||
|
@Column({ type: Boolean, default: false })
|
||||||
|
isAdmin?: boolean;
|
||||||
|
|
||||||
|
@CreateDateColumn()
|
||||||
|
created_at?: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn()
|
||||||
|
updated_at?: Date;
|
||||||
|
|
||||||
|
@DeleteDateColumn()
|
||||||
|
deleted_at?: Date;
|
||||||
|
}
|
||||||
14
src/users/users.module.ts
Normal file
14
src/users/users.module.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { User } from './users.entity';
|
||||||
|
import { UserService } from './users.service';
|
||||||
|
import { UsersController } from './users.controller';
|
||||||
|
import { AuthenticationModule } from 'src/authentication/authentication.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([User]), AuthenticationModule],
|
||||||
|
exports: [UserService],
|
||||||
|
providers: [UserService],
|
||||||
|
controllers: [UsersController],
|
||||||
|
})
|
||||||
|
export class UsersModule {}
|
||||||
27
src/users/users.service.ts
Normal file
27
src/users/users.service.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { User } from './users.entity';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UserService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(User)
|
||||||
|
private userRepository: Repository<User>,
|
||||||
|
) {}
|
||||||
|
private readonly logger = new Logger(UserService.name);
|
||||||
|
|
||||||
|
// TODO: save (create new)
|
||||||
|
|
||||||
|
// TODO: update by UUID
|
||||||
|
|
||||||
|
// TODO: find one by UUID
|
||||||
|
|
||||||
|
// TODO: find all
|
||||||
|
|
||||||
|
// TODO: find all and deleted
|
||||||
|
|
||||||
|
// TODO: soft delete
|
||||||
|
|
||||||
|
// TODO: restore soft deleted by UUID
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user