commit
8aaad17fcb
104 changed files with 12608 additions and 0 deletions
-
7.editorconfig
-
76.github/CODE_OF_CONDUCT.md
-
49.github/CONTRIBUTING.md
-
243.gitignore
-
4.prettierrc
-
21LICENSE.md
-
69README.md
-
5_templates/generator/help/index.ejs
-
18_templates/generator/new/hello.ejs
-
18_templates/generator/with-prompt/hello.ejs
-
14_templates/generator/with-prompt/prompt.ejs
-
17_templates/module/empty-module/module.ts.ejs
-
10_templates/module/empty-module/prompt.js
-
6_templates/module/help/index.ejs
-
3_templates/module/with-crud/constants.ts.ejs
-
3_templates/module/with-crud/controller.spec.ts.ejs
-
96_templates/module/with-crud/controller.ts.ejs
-
15_templates/module/with-crud/dto.ts.ejs
-
16_templates/module/with-crud/entity.ts.ejs
-
24_templates/module/with-crud/module.ts.ejs
-
10_templates/module/with-crud/prompt.js
-
19_templates/module/with-crud/repository.ts.ejs
-
13_templates/module/with-crud/routing-body.ts.ejs
-
10_templates/module/with-crud/routing-head.ts.ejs
-
3_templates/module/with-crud/service.spec.ts.ejs
-
62_templates/module/with-crud/service.ts.ejs
-
18_templates/module/with-routing/module.ts.ejs
-
10_templates/module/with-routing/prompt.js
-
13_templates/module/with-routing/routing-body.ts.ejs
-
10_templates/module/with-routing/routing-head.ts.ejs
-
6compodoc.server.json
-
9docker/postgresql.yml
-
14exemple.env
-
5nest-cli.json
-
122package.json
-
21src/app.controller.spec.ts
-
13src/app.controller.ts
-
58src/app.module.ts
-
19src/app.routes.ts
-
8src/decorators/currentUser.decorator.ts
-
3src/decorators/public.decorator.ts
-
3src/decorators/roles.decorator.ts
-
0src/exceptions/.gitkeep
-
0src/filters/.gitkeep
-
24src/guards/roles.guard.ts
-
0src/interceptors/.gitkeep
-
0src/interfaces/.gitkeep
-
71src/main.ts
-
0src/middlewares/.gitkeep
-
40src/modules/core/auth/auth.controller.ts
-
17src/modules/core/auth/auth.dto.ts
-
29src/modules/core/auth/auth.guard.ts
-
33src/modules/core/auth/auth.module.ts
-
32src/modules/core/auth/auth.service.ts
-
22src/modules/core/auth/jwt.strategy.ts
-
9src/modules/core/config/config.module.ts
-
118src/modules/core/config/config.service.spec.ts
-
75src/modules/core/config/config.service.ts
-
8src/modules/core/crypto/crypto.module.ts
-
32src/modules/core/crypto/crypto.service.spec.ts
-
28src/modules/core/crypto/crypto.service.ts
-
43src/modules/core/logger/logger-exception.interceptor.ts
-
9src/modules/core/logger/logger.constants.ts
-
11src/modules/core/logger/logger.module.ts
-
33src/modules/core/logger/logger.providers.ts
-
21src/modules/core/logger/logger.service.spec.ts
-
46src/modules/core/logger/logger.service.ts
-
11src/modules/core/metrics/common/prom.decorators.ts
-
11src/modules/core/metrics/common/prom.utils.ts
-
66src/modules/core/metrics/interceptors/metrics.interceptor.ts
-
33src/modules/core/metrics/interfaces/metric.type.ts
-
27src/modules/core/metrics/interfaces/prom-options.interface.ts
-
90src/modules/core/metrics/metrics-core.module.ts
-
3src/modules/core/metrics/metrics.constants.ts
-
15src/modules/core/metrics/metrics.constroller.ts
-
147src/modules/core/metrics/metrics.module.ts
-
98src/modules/core/metrics/metrics.providers.ts
-
32src/modules/core/metrics/middleware/inbound.middleware.ts
-
10src/modules/core/pagination/pagination.decorator.ts
-
14src/modules/core/pagination/pagination.validation.ts
-
14src/modules/core/validation/validation.exception.ts
-
59src/modules/core/validation/validation.util.ts
-
24src/modules/core/validation/validator.pipe.ts
-
2src/modules/roles/roles.constants.ts
-
43src/modules/roles/roles.controller.ts
-
15src/modules/roles/roles.entity.ts
-
14src/modules/roles/roles.module.ts
-
14src/modules/roles/roles.repository.ts
-
41src/modules/roles/roles.service.ts
-
0src/modules/user/user.constants.ts
-
132src/modules/user/user.controller.ts
-
54src/modules/user/user.dto.ts
-
33src/modules/user/user.entity.ts
-
20src/modules/user/user.module.ts
-
22src/modules/user/user.repository.ts
-
112src/modules/user/user.service.ts
-
0src/pipes/.gitkeep
-
29src/utils/dbmodel.model.ts
-
28test/app.e2e-spec.ts
-
9test/jest-e2e.json
@ -0,0 +1,7 @@ |
|||||
|
|
||||
|
# Unix-style newlines with a newline ending every file |
||||
|
[*] |
||||
|
end_of_line = lf |
||||
|
insert_final_newline = true |
||||
|
indent_style = space |
||||
|
indent_size = 2 |
||||
@ -0,0 +1,76 @@ |
|||||
|
# Contributor Covenant Code of Conduct |
||||
|
|
||||
|
## Our Pledge |
||||
|
|
||||
|
In the interest of fostering an open and welcoming environment, we as |
||||
|
contributors and maintainers pledge to making participation in our project and |
||||
|
our community a harassment-free experience for everyone, regardless of age, body |
||||
|
size, disability, ethnicity, sex characteristics, gender identity and expression, |
||||
|
level of experience, education, socio-economic status, nationality, personal |
||||
|
appearance, race, religion, or sexual identity and orientation. |
||||
|
|
||||
|
## Our Standards |
||||
|
|
||||
|
Examples of behavior that contributes to creating a positive environment |
||||
|
include: |
||||
|
|
||||
|
- Using welcoming and inclusive language |
||||
|
- Being respectful of differing viewpoints and experiences |
||||
|
- Gracefully accepting constructive criticism |
||||
|
- Focusing on what is best for the community |
||||
|
- Showing empathy towards other community members |
||||
|
|
||||
|
Examples of unacceptable behavior by participants include: |
||||
|
|
||||
|
- The use of sexualized language or imagery and unwelcome sexual attention or |
||||
|
advances |
||||
|
- Trolling, insulting/derogatory comments, and personal or political attacks |
||||
|
- Public or private harassment |
||||
|
- Publishing others' private information, such as a physical or electronic |
||||
|
address, without explicit permission |
||||
|
- Other conduct which could reasonably be considered inappropriate in a |
||||
|
professional setting |
||||
|
|
||||
|
## Our Responsibilities |
||||
|
|
||||
|
Project maintainers are responsible for clarifying the standards of acceptable |
||||
|
behavior and are expected to take appropriate and fair corrective action in |
||||
|
response to any instances of unacceptable behavior. |
||||
|
|
||||
|
Project maintainers have the right and responsibility to remove, edit, or |
||||
|
reject comments, commits, code, wiki edits, issues, and other contributions |
||||
|
that are not aligned to this Code of Conduct, or to ban temporarily or |
||||
|
permanently any contributor for other behaviors that they deem inappropriate, |
||||
|
threatening, offensive, or harmful. |
||||
|
|
||||
|
## Scope |
||||
|
|
||||
|
This Code of Conduct applies both within project spaces and in public spaces |
||||
|
when an individual is representing the project or its community. Examples of |
||||
|
representing a project or community include using an official project e-mail |
||||
|
address, posting via an official social media account, or acting as an appointed |
||||
|
representative at an online or offline event. Representation of a project may be |
||||
|
further defined and clarified by project maintainers. |
||||
|
|
||||
|
## Enforcement |
||||
|
|
||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be |
||||
|
reported by contacting the project team at nic.beaussart@gmail.com. All |
||||
|
complaints will be reviewed and investigated and will result in a response that |
||||
|
is deemed necessary and appropriate to the circumstances. The project team is |
||||
|
obligated to maintain confidentiality with regard to the reporter of an incident. |
||||
|
Further details of specific enforcement policies may be posted separately. |
||||
|
|
||||
|
Project maintainers who do not follow or enforce the Code of Conduct in good |
||||
|
faith may face temporary or permanent repercussions as determined by other |
||||
|
members of the project's leadership. |
||||
|
|
||||
|
## Attribution |
||||
|
|
||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, |
||||
|
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html |
||||
|
|
||||
|
[homepage]: https://www.contributor-covenant.org |
||||
|
|
||||
|
For answers to common questions about this code of conduct, see |
||||
|
https://www.contributor-covenant.org/faq |
||||
@ -0,0 +1,49 @@ |
|||||
|
# Contributing to this project |
||||
|
|
||||
|
First of all thank you for expressing interest in this project! :+1:<br /> |
||||
|
We are very happy to welcome new contributors. :tada: |
||||
|
|
||||
|
## How can I contribute? :man_technologist: |
||||
|
|
||||
|
### Report a bug :bug: |
||||
|
|
||||
|
If you think you have found a bug, please search [our issue tracker][issues] to see if anyone has already reported it.<br /> |
||||
|
If you are the first to have noticed it, please [create an issue][new_issue], and make sure to provide any information that might help us resolve it.<br /> |
||||
|
You are welcome to try and fix it by submitting a pull request if you would like to (see Pull requests section for more information). |
||||
|
|
||||
|
### Feature requests and enhancements :sparkles: |
||||
|
|
||||
|
We are open to feature requests, be sure to search [our issue tracker][issues] to see if anyone has already asked for it.<br /> |
||||
|
If not, please [create an issue][new_issue] describing what you want, what your use case is, and an example of code.<br /> |
||||
|
You are welcome to try and do it yourself by submitting a pull request if you would like to (see Pull requests section for more information).<br /> |
||||
|
As immutadot is still a very young project, we are also open to enhancement suggestions; if you think of anything that might help us improve the project, please [create an issue][new_issue] and we will be happy to discuss it with you. |
||||
|
|
||||
|
### Pull requests :arrow_up: |
||||
|
|
||||
|
#### Installation :package: |
||||
|
|
||||
|
We use [yarn](https://yarnpkg.com/) as our package manager.<br /> |
||||
|
We don't support [npm](https://www.npmjs.com/) due to the overhead of managing two lock files and the inconsistency of `package-lock.json` between Linux and MacOS (see npm/npm#17722).<br /> |
||||
|
Once you have cloned the project, run `yarn` to install all the dependencies. |
||||
|
|
||||
|
#### Tests and Code style :policeman: |
||||
|
|
||||
|
If you write any code, be sure to write the test that goes with it in a file located at the same place and named `<something>.spec.js`.<br /> |
||||
|
We use [tslint](https://palantir.github.io/tslint/) to enforce some coding rules (2 spaces indentation, etc.), ideally use an IDE to help you comply with these rules.<br /> |
||||
|
We use [prettier](https://prettier.io/) to have a uniform code style for everybody. It should run on pre commit automatically :tada:.<br /> |
||||
|
Before pushing anything, please be sure to check that the tests are OK by running `yarn test` and that your code complies with the coding rules by running `yarn lint && yarn format:check`. |
||||
|
|
||||
|
#### Documentation :bulb: |
||||
|
|
||||
|
The better the documentation, the fewer things users will have to wonder about.<br /> |
||||
|
We [use jsdoc](http://usejsdoc.org/) to document our code, if you write any new code please write the documentation with it and try to conform to the existing documention.<br /> |
||||
|
|
||||
|
#### emojis :beers: |
||||
|
|
||||
|
We really :heart: emojis, and we would like you to share our :heart:.<br /> |
||||
|
Each and every commit message has to be prefixed by an emoji.<br /> |
||||
|
Please refer to [the gitmoji guide](https://gitmoji.carloscuesta.me/) to know which one to use. |
||||
|
|
||||
|
## Any questions :question: |
||||
|
|
||||
|
If you are not sure whether to report a bug or ask for a new feature, or if you just have a question about anything, please [create an issue][new_issue]. |
||||
@ -0,0 +1,243 @@ |
|||||
|
|
||||
|
# Created by https://www.gitignore.io/api/code,node,linux,windows,intellij,sublimetext |
||||
|
|
||||
|
### Code ### |
||||
|
# Visual Studio Code - https://code.visualstudio.com/ |
||||
|
.settings/ |
||||
|
.vscode/ |
||||
|
jsconfig.json |
||||
|
|
||||
|
### Intellij ### |
||||
|
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm |
||||
|
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 |
||||
|
|
||||
|
# User-specific stuff |
||||
|
.idea/**/workspace.xml |
||||
|
.idea/**/tasks.xml |
||||
|
.idea/**/usage.statistics.xml |
||||
|
.idea/**/dictionaries |
||||
|
.idea/**/shelf |
||||
|
|
||||
|
# Generated files |
||||
|
.idea/**/contentModel.xml |
||||
|
|
||||
|
# Sensitive or high-churn files |
||||
|
.idea/**/dataSources/ |
||||
|
.idea/**/dataSources.ids |
||||
|
.idea/**/dataSources.local.xml |
||||
|
.idea/**/sqlDataSources.xml |
||||
|
.idea/**/dynamic.xml |
||||
|
.idea/**/uiDesigner.xml |
||||
|
.idea/**/dbnavigator.xml |
||||
|
|
||||
|
# Gradle |
||||
|
.idea/**/gradle.xml |
||||
|
.idea/**/libraries |
||||
|
|
||||
|
# Gradle and Maven with auto-import |
||||
|
# When using Gradle or Maven with auto-import, you should exclude module files, |
||||
|
# since they will be recreated, and may cause churn. Uncomment if using |
||||
|
# auto-import. |
||||
|
# .idea/modules.xml |
||||
|
# .idea/*.iml |
||||
|
# .idea/modules |
||||
|
|
||||
|
# CMake |
||||
|
cmake-build-*/ |
||||
|
|
||||
|
# Mongo Explorer plugin |
||||
|
.idea/**/mongoSettings.xml |
||||
|
|
||||
|
# File-based project format |
||||
|
*.iws |
||||
|
|
||||
|
# IntelliJ |
||||
|
out/ |
||||
|
|
||||
|
# mpeltonen/sbt-idea plugin |
||||
|
.idea_modules/ |
||||
|
|
||||
|
# JIRA plugin |
||||
|
atlassian-ide-plugin.xml |
||||
|
|
||||
|
# Cursive Clojure plugin |
||||
|
.idea/replstate.xml |
||||
|
|
||||
|
# Crashlytics plugin (for Android Studio and IntelliJ) |
||||
|
com_crashlytics_export_strings.xml |
||||
|
crashlytics.properties |
||||
|
crashlytics-build.properties |
||||
|
fabric.properties |
||||
|
|
||||
|
# Editor-based Rest Client |
||||
|
.idea/httpRequests |
||||
|
|
||||
|
# Android studio 3.1+ serialized cache file |
||||
|
.idea/caches/build_file_checksums.ser |
||||
|
|
||||
|
### Intellij Patch ### |
||||
|
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 |
||||
|
|
||||
|
# *.iml |
||||
|
# modules.xml |
||||
|
# .idea/misc.xml |
||||
|
# *.ipr |
||||
|
|
||||
|
# Sonarlint plugin |
||||
|
.idea/sonarlint |
||||
|
|
||||
|
.idea/ |
||||
|
|
||||
|
### Linux ### |
||||
|
*~ |
||||
|
|
||||
|
# temporary files which can be created if a process still has a handle open of a deleted file |
||||
|
.fuse_hidden* |
||||
|
|
||||
|
# KDE directory preferences |
||||
|
.directory |
||||
|
|
||||
|
# Linux trash folder which might appear on any partition or disk |
||||
|
.Trash-* |
||||
|
|
||||
|
# .nfs files are created when an open file is removed but is still being accessed |
||||
|
.nfs* |
||||
|
|
||||
|
### Node ### |
||||
|
# Logs |
||||
|
logs |
||||
|
*.log |
||||
|
npm-debug.log* |
||||
|
yarn-debug.log* |
||||
|
yarn-error.log* |
||||
|
|
||||
|
# Runtime data |
||||
|
pids |
||||
|
*.pid |
||||
|
*.seed |
||||
|
*.pid.lock |
||||
|
|
||||
|
# Directory for instrumented libs generated by jscoverage/JSCover |
||||
|
lib-cov |
||||
|
|
||||
|
# Coverage directory used by tools like istanbul |
||||
|
coverage |
||||
|
|
||||
|
# nyc test coverage |
||||
|
.nyc_output |
||||
|
|
||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) |
||||
|
.grunt |
||||
|
|
||||
|
# Bower dependency directory (https://bower.io/) |
||||
|
bower_components |
||||
|
|
||||
|
# node-waf configuration |
||||
|
.lock-wscript |
||||
|
|
||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html) |
||||
|
build/Release |
||||
|
|
||||
|
# Dependency directories |
||||
|
node_modules/ |
||||
|
jspm_packages/ |
||||
|
|
||||
|
# TypeScript v1 declaration files |
||||
|
typings/ |
||||
|
|
||||
|
# Optional npm cache directory |
||||
|
.npm |
||||
|
|
||||
|
# Optional eslint cache |
||||
|
.eslintcache |
||||
|
|
||||
|
# Optional REPL history |
||||
|
.node_repl_history |
||||
|
|
||||
|
# Output of 'npm pack' |
||||
|
*.tgz |
||||
|
|
||||
|
# Yarn Integrity file |
||||
|
.yarn-integrity |
||||
|
|
||||
|
# dotenv environment variables file |
||||
|
.env |
||||
|
|
||||
|
# parcel-bundler cache (https://parceljs.org/) |
||||
|
.cache |
||||
|
|
||||
|
# next.js build output |
||||
|
.next |
||||
|
|
||||
|
# nuxt.js build output |
||||
|
.nuxt |
||||
|
|
||||
|
# vuepress build output |
||||
|
.vuepress/dist |
||||
|
|
||||
|
# Serverless directories |
||||
|
.serverless |
||||
|
|
||||
|
### SublimeText ### |
||||
|
# Cache files for Sublime Text |
||||
|
*.tmlanguage.cache |
||||
|
*.tmPreferences.cache |
||||
|
*.stTheme.cache |
||||
|
|
||||
|
# Workspace files are user-specific |
||||
|
*.sublime-workspace |
||||
|
|
||||
|
# Project files should be checked into the repository, unless a significant |
||||
|
# proportion of contributors will probably not be using Sublime Text |
||||
|
# *.sublime-project |
||||
|
|
||||
|
# SFTP configuration file |
||||
|
sftp-config.json |
||||
|
|
||||
|
# Package control specific files |
||||
|
Package Control.last-run |
||||
|
Package Control.ca-list |
||||
|
Package Control.ca-bundle |
||||
|
Package Control.system-ca-bundle |
||||
|
Package Control.cache/ |
||||
|
Package Control.ca-certs/ |
||||
|
Package Control.merged-ca-bundle |
||||
|
Package Control.user-ca-bundle |
||||
|
oscrypto-ca-bundle.crt |
||||
|
bh_unicode_properties.cache |
||||
|
|
||||
|
# Sublime-github package stores a github token in this file |
||||
|
# https://packagecontrol.io/packages/sublime-github |
||||
|
GitHub.sublime-settings |
||||
|
|
||||
|
### Windows ### |
||||
|
# Windows thumbnail cache files |
||||
|
Thumbs.db |
||||
|
ehthumbs.db |
||||
|
ehthumbs_vista.db |
||||
|
|
||||
|
# Dump file |
||||
|
*.stackdump |
||||
|
|
||||
|
# Folder config file |
||||
|
[Dd]esktop.ini |
||||
|
|
||||
|
# Recycle Bin used on file shares |
||||
|
$RECYCLE.BIN/ |
||||
|
|
||||
|
# Windows Installer files |
||||
|
*.cab |
||||
|
*.msi |
||||
|
*.msix |
||||
|
*.msm |
||||
|
*.msp |
||||
|
|
||||
|
# Windows shortcuts |
||||
|
*.lnk |
||||
|
|
||||
|
|
||||
|
# End of https://www.gitignore.io/api/code,node,linux,windows,intellij,sublimetext |
||||
|
|
||||
|
.env |
||||
|
dist/ |
||||
|
doc-server |
||||
@ -0,0 +1,4 @@ |
|||||
|
{ |
||||
|
"singleQuote": true, |
||||
|
"trailingComma": "all" |
||||
|
} |
||||
@ -0,0 +1,21 @@ |
|||||
|
MIT License |
||||
|
|
||||
|
Copyright (c) 2019 Nicolas Beaussart-Hatchuel |
||||
|
|
||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||
|
of this software and associated documentation files (the "Software"), to deal |
||||
|
in the Software without restriction, including without limitation the rights |
||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||
|
copies of the Software, and to permit persons to whom the Software is |
||||
|
furnished to do so, subject to the following conditions: |
||||
|
|
||||
|
The above copyright notice and this permission notice shall be included in all |
||||
|
copies or substantial portions of the Software. |
||||
|
|
||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
|
SOFTWARE. |
||||
@ -0,0 +1,69 @@ |
|||||
|
<h1 align="center">Welcome to TP on CI and CircleCI 👋</h1> |
||||
|
|
||||
|
|
||||
|
You cand find the codelabs here on iCampus. |
||||
|
|
||||
|
## Installation |
||||
|
|
||||
|
```bash |
||||
|
$ yarn install |
||||
|
``` |
||||
|
|
||||
|
## Running the app |
||||
|
|
||||
|
```bash |
||||
|
# development |
||||
|
yarn start |
||||
|
|
||||
|
# watch mode |
||||
|
yarn start:dev |
||||
|
|
||||
|
# production mode |
||||
|
$ yarn start:prod |
||||
|
``` |
||||
|
|
||||
|
## Test |
||||
|
|
||||
|
```bash |
||||
|
# unit tests |
||||
|
$ yarn test |
||||
|
|
||||
|
# e2e tests |
||||
|
$ yarn test:e2e |
||||
|
|
||||
|
# test coverage |
||||
|
$ yarn test:cov |
||||
|
``` |
||||
|
|
||||
|
## Lints |
||||
|
|
||||
|
```bash |
||||
|
# Check |
||||
|
$ yarn format:check |
||||
|
|
||||
|
# Format code |
||||
|
$ yarn format:check |
||||
|
|
||||
|
$ yarn lint |
||||
|
``` |
||||
|
|
||||
|
## Generate |
||||
|
|
||||
|
```bash |
||||
|
$ yarn generate module with-crud --name myModule |
||||
|
``` |
||||
|
|
||||
|
## Author |
||||
|
|
||||
|
👤 **Nicolas Beaussart <nic.beaussart@gmail.com>** |
||||
|
|
||||
|
- Twitter: [@beaussan](https://twitter.com/beaussan) |
||||
|
- Github: [@beaussan](https://github.com/beaussan) |
||||
|
|
||||
|
## Show your support |
||||
|
|
||||
|
Give a ⭐️ if this project helped you! |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
_This README was generated with ❤️ by [readme-md-generator](https://github.com/kefranabg/readme-md-generator)_ |
||||
@ -0,0 +1,5 @@ |
|||||
|
--- |
||||
|
message: | |
||||
|
hygen {bold generator new} --name [NAME] --action [ACTION] |
||||
|
hygen {bold generator with-prompt} --name [NAME] --action [ACTION] |
||||
|
--- |
||||
@ -0,0 +1,18 @@ |
|||||
|
--- |
||||
|
to: _templates/<%= name %>/<%= action || 'new' %>/hello.ejs |
||||
|
--- |
||||
|
--- |
||||
|
to: app/hello.js |
||||
|
--- |
||||
|
const hello = ``` |
||||
|
Hello! |
||||
|
This is your first hygen template. |
||||
|
|
||||
|
Learn what it can do here: |
||||
|
|
||||
|
https://github.com/jondot/hygen |
||||
|
``` |
||||
|
|
||||
|
console.log(hello) |
||||
|
|
||||
|
|
||||
@ -0,0 +1,18 @@ |
|||||
|
--- |
||||
|
to: _templates/<%= name %>/<%= action || 'new' %>/hello.ejs |
||||
|
--- |
||||
|
--- |
||||
|
to: app/hello.js |
||||
|
--- |
||||
|
const hello = ``` |
||||
|
Hello! |
||||
|
This is your first prompt based hygen template. |
||||
|
|
||||
|
Learn what it can do here: |
||||
|
|
||||
|
https://github.com/jondot/hygen |
||||
|
``` |
||||
|
|
||||
|
console.log(hello) |
||||
|
|
||||
|
|
||||
@ -0,0 +1,14 @@ |
|||||
|
--- |
||||
|
to: _templates/<%= name %>/<%= action || 'new' %>/prompt.js |
||||
|
--- |
||||
|
|
||||
|
// see types of prompts: |
||||
|
// https://github.com/enquirer/enquirer/tree/master/examples |
||||
|
// |
||||
|
module.exports = [ |
||||
|
{ |
||||
|
type: 'input', |
||||
|
name: 'message', |
||||
|
message: "What's your message?" |
||||
|
} |
||||
|
] |
||||
@ -0,0 +1,17 @@ |
|||||
|
--- |
||||
|
to: src/modules/<%=h.changeCase.param(name)%>/<%=h.changeCase.param(name)%>.module.ts |
||||
|
--- |
||||
|
<% |
||||
|
properName = h.changeCase.pascal(name); |
||||
|
kebabName = h.changeCase.param(name); |
||||
|
%>import { Module } from '@nestjs/common'; |
||||
|
|
||||
|
@Module({ |
||||
|
imports: [], |
||||
|
controllers: [], |
||||
|
providers: [], |
||||
|
exports: [], |
||||
|
}) |
||||
|
export class <%=properName%>Module {} |
||||
|
|
||||
|
|
||||
@ -0,0 +1,10 @@ |
|||||
|
// see types of prompts:
|
||||
|
// https://github.com/enquirer/enquirer/tree/master/examples
|
||||
|
//
|
||||
|
module.exports = [ |
||||
|
{ |
||||
|
type: 'input', |
||||
|
name: 'name', |
||||
|
message: 'What is your module name?', |
||||
|
}, |
||||
|
]; |
||||
@ -0,0 +1,6 @@ |
|||||
|
--- |
||||
|
message: | |
||||
|
hygen {bold module with-crud} --name [NAME] |
||||
|
hygen {bold module empty-module} --name [NAME] |
||||
|
hygen {bold module with-routing} --name [NAME] |
||||
|
--- |
||||
@ -0,0 +1,3 @@ |
|||||
|
--- |
||||
|
to: src/modules/<%=h.changeCase.param(name)%>/<%=h.changeCase.param(name)%>.constants.ts |
||||
|
--- |
||||
@ -0,0 +1,3 @@ |
|||||
|
--- |
||||
|
to: src/modules/<%=h.changeCase.param(name)%>/<%=h.changeCase.param(name)%>.controller.spec.ts |
||||
|
--- |
||||
@ -0,0 +1,96 @@ |
|||||
|
--- |
||||
|
to: src/modules/<%=h.changeCase.param(name)%>/<%=h.changeCase.param(name)%>.controller.ts |
||||
|
--- |
||||
|
<% |
||||
|
properName = h.changeCase.pascal(name); |
||||
|
kebabName = h.changeCase.param(name); |
||||
|
camelCase = h.changeCase.camel(name); |
||||
|
%>import { |
||||
|
Body, |
||||
|
Controller, |
||||
|
Delete, |
||||
|
Get, |
||||
|
NotFoundException, |
||||
|
Param, |
||||
|
ParseIntPipe, |
||||
|
Post, |
||||
|
Put, |
||||
|
Query, |
||||
|
} from '@nestjs/common'; |
||||
|
import { <%= properName %> } from './<%= kebabName %>.entity'; |
||||
|
import { <%= properName %>Service } from './<%= kebabName %>.service'; |
||||
|
import { |
||||
|
ApiBearerAuth, |
||||
|
ApiImplicitParam, |
||||
|
ApiResponse, |
||||
|
ApiUseTags, |
||||
|
} from '@nestjs/swagger'; |
||||
|
import { <%= properName %>Dto } from './<%= kebabName %>.dto'; |
||||
|
|
||||
|
|
||||
|
@ApiUseTags('{{ sentenceCase name }}') |
||||
|
@Controller() |
||||
|
// @ApiBearerAuth() |
||||
|
export class <%= properName %>Controller { |
||||
|
constructor(private readonly <%= camelCase %>Service: <%= properName %>Service) {} |
||||
|
|
||||
|
@Get() |
||||
|
@ApiResponse({ |
||||
|
status: 200, |
||||
|
description: 'Get a list of all {{ sentenceCase name }}.', |
||||
|
type: <%= properName %>, |
||||
|
isArray: true, |
||||
|
}) |
||||
|
getAll(): Promise<<%= properName %>[]> { |
||||
|
return this.<%= camelCase %>Service.getAll(); |
||||
|
} |
||||
|
|
||||
|
@Post() |
||||
|
@ApiResponse({ |
||||
|
status: 201, |
||||
|
description: 'The {{ sentenceCase name }} has been created.', |
||||
|
type: <%= properName %>, |
||||
|
}) |
||||
|
saveNew(@Body() <%= camelCase %>Dto: <%= properName %>Dto): Promise<<%= properName %>> { |
||||
|
return this.<%= camelCase %>Service.saveNew(<%= camelCase %>Dto); |
||||
|
} |
||||
|
|
||||
|
@Get(':id') |
||||
|
@ApiResponse({ |
||||
|
status: 200, |
||||
|
description: 'The {{ sentenceCase name }} with the matching id', |
||||
|
type: <%= properName %>, |
||||
|
}) |
||||
|
@ApiResponse({ status: 404, description: 'Not found.' }) |
||||
|
async findOne( |
||||
|
@Param('id', new ParseIntPipe()) id: number, |
||||
|
): Promise<<%= properName %>> { |
||||
|
return (await this.<%= camelCase %>Service.getOneById(id)).orElseThrow( |
||||
|
() => new NotFoundException(), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
@Put(':id') |
||||
|
@ApiResponse({ |
||||
|
status: 200, |
||||
|
description: 'The updated {{ sentenceCase name }} with the matching id', |
||||
|
type: <%= properName %>, |
||||
|
}) |
||||
|
@ApiResponse({ status: 404, description: 'Not found.' }) |
||||
|
async updateOne( |
||||
|
@Param('id', new ParseIntPipe()) id: number, |
||||
|
@Body() <%= camelCase %>Dto: <%= properName %>Dto, |
||||
|
): Promise<<%= properName %>> { |
||||
|
return this.<%= camelCase %>Service.update(id, <%= camelCase %>Dto); |
||||
|
} |
||||
|
|
||||
|
@Delete(':id') |
||||
|
@ApiResponse({ |
||||
|
status: 200, |
||||
|
description: 'The {{ sentenceCase name }} with the matching id was deleted', |
||||
|
}) |
||||
|
@ApiResponse({ status: 404, description: 'Not found.' }) |
||||
|
async deleteOne(@Param('id', new ParseIntPipe()) id: number): Promise<void> { |
||||
|
await this.<%= camelCase %>Service.deleteById(id); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,15 @@ |
|||||
|
--- |
||||
|
to: src/modules/<%=h.changeCase.param(name)%>/<%=h.changeCase.param(name)%>.dto.ts |
||||
|
--- |
||||
|
<% |
||||
|
properName = h.changeCase.pascal(name); |
||||
|
kebabName = h.changeCase.param(name); |
||||
|
camelCase = h.changeCase.camel(name); |
||||
|
%>import { IsArray, IsOptional, IsString, Min, MinLength } from 'class-validator'; |
||||
|
import { Type } from 'class-transformer'; |
||||
|
import { ApiModelProperty } from '@nestjs/swagger'; |
||||
|
|
||||
|
export class <%= properName %>Dto { |
||||
|
|
||||
|
} |
||||
|
|
||||
@ -0,0 +1,16 @@ |
|||||
|
--- |
||||
|
to: src/modules/<%=h.changeCase.param(name)%>/<%=h.changeCase.param(name)%>.entity.ts |
||||
|
--- |
||||
|
<% |
||||
|
properName = h.changeCase.pascal(name); |
||||
|
kebabName = h.changeCase.param(name); |
||||
|
camelCase = h.changeCase.camel(name); |
||||
|
%>import { DbAuditModel } from '../../utils/dbmodel.model'; |
||||
|
import { Column, Entity, ManyToOne, OneToMany } from 'typeorm'; |
||||
|
import { ApiModelProperty } from '@nestjs/swagger'; |
||||
|
|
||||
|
|
||||
|
@Entity() |
||||
|
export class <%= properName %> extends DbAuditModel { |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
--- |
||||
|
to: src/modules/<%=h.changeCase.param(name)%>/<%=h.changeCase.param(name)%>.module.ts |
||||
|
--- |
||||
|
<% |
||||
|
properName = h.changeCase.pascal(name); |
||||
|
kebabName = h.changeCase.param(name); |
||||
|
%>import { Module } from '@nestjs/common'; |
||||
|
import { TypeOrmModule } from '@nestjs/typeorm'; |
||||
|
import { <%=properName%>Controller } from './<%=kebabName%>.controller'; |
||||
|
import { <%=properName%>Service } from './<%=kebabName%>.service'; |
||||
|
import { <%=properName%> } from './<%=kebabName%>.entity'; |
||||
|
import { <%=properName%>Repository } from './<%=kebabName%>.repository'; |
||||
|
|
||||
|
@Module({ |
||||
|
imports: [ |
||||
|
TypeOrmModule.forFeature([<%=properName%>, <%=properName%>Repository]), |
||||
|
], |
||||
|
controllers: [<%=properName%>Controller], |
||||
|
providers: [<%=properName%>Service], |
||||
|
exports: [<%=properName%>Service], |
||||
|
}) |
||||
|
export class <%=properName%>Module {} |
||||
|
|
||||
|
|
||||
@ -0,0 +1,10 @@ |
|||||
|
// see types of prompts:
|
||||
|
// https://github.com/enquirer/enquirer/tree/master/examples
|
||||
|
//
|
||||
|
module.exports = [ |
||||
|
{ |
||||
|
type: 'input', |
||||
|
name: 'name', |
||||
|
message: 'What is your module name?', |
||||
|
}, |
||||
|
]; |
||||
@ -0,0 +1,19 @@ |
|||||
|
--- |
||||
|
to: src/modules/<%=h.changeCase.param(name)%>/<%=h.changeCase.param(name)%>.repository.ts |
||||
|
--- |
||||
|
<% |
||||
|
properName = h.changeCase.pascal(name); |
||||
|
kebabName = h.changeCase.param(name); |
||||
|
camelCase = h.changeCase.camel(name); |
||||
|
%>import { EntityRepository, Repository } from 'typeorm'; |
||||
|
import { <%= properName %> } from './<%= kebabName %>.entity'; |
||||
|
import { Optional } from 'typescript-optional'; |
||||
|
|
||||
|
@EntityRepository(<%= properName %>) |
||||
|
export class <%= properName %>Repository extends Repository<<%= properName %>> { |
||||
|
async findOneById(id: number): Promise<Optional<<%= properName %>>> { |
||||
|
return Optional.ofNullable( |
||||
|
await this.findOne(id, {}), |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
--- |
||||
|
to: src/app.routes.ts |
||||
|
after: export const appRoutes.* |
||||
|
inject: true |
||||
|
skip_if: /<%= h.changeCase.param(name) %> |
||||
|
--- |
||||
|
<% |
||||
|
properName = h.changeCase.pascal(name); |
||||
|
kebabName = h.changeCase.param(name); |
||||
|
%> { |
||||
|
path: '/<%= kebabName %>s', |
||||
|
module: <%= properName %>Module, |
||||
|
}, |
||||
@ -0,0 +1,10 @@ |
|||||
|
--- |
||||
|
to: src/app.routes.ts |
||||
|
after: import { Routes } from 'nest-router'; |
||||
|
inject: true |
||||
|
skip_if: modules/<%= h.changeCase.param(name) %> |
||||
|
--- |
||||
|
<% |
||||
|
properName = h.changeCase.pascal(name); |
||||
|
kebabName = h.changeCase.param(name); |
||||
|
%>import { <%= properName %>Module } from './modules/<%= kebabName %>/<%= kebabName %>.module'; |
||||
@ -0,0 +1,3 @@ |
|||||
|
--- |
||||
|
to: src/modules/<%=h.changeCase.param(name)%>/<%=h.changeCase.param(name)%>.service.spec.ts |
||||
|
--- |
||||
@ -0,0 +1,62 @@ |
|||||
|
--- |
||||
|
to: src/modules/<%=h.changeCase.param(name)%>/<%=h.changeCase.param(name)%>.service.ts |
||||
|
--- |
||||
|
<% |
||||
|
properName = h.changeCase.pascal(name); |
||||
|
kebabName = h.changeCase.param(name); |
||||
|
camelCase = h.changeCase.camel(name); |
||||
|
%>import { <%= properName %> } from './<%= kebabName %>.entity'; |
||||
|
import { Inject, Injectable, NotFoundException } from '@nestjs/common'; |
||||
|
import { InjectRepository } from '@nestjs/typeorm'; |
||||
|
import { <%= properName %>Repository } from './<%= kebabName %>.repository'; |
||||
|
// import { } from './<%= kebabName %>.constants'; |
||||
|
import { <%= properName %>Dto } from './<%= kebabName %>.dto'; |
||||
|
import { Optional } from 'typescript-optional'; |
||||
|
|
||||
|
|
||||
|
@Injectable() |
||||
|
export class <%= properName %>Service { |
||||
|
|
||||
|
constructor( |
||||
|
@InjectRepository(<%= properName %>Repository) |
||||
|
private readonly <%= camelCase %>Repository: <%= properName %>Repository, |
||||
|
) { } |
||||
|
|
||||
|
async getAll(): Promise<<%= properName %>[]> { |
||||
|
return this.<%= camelCase %>Repository.find({}); |
||||
|
} |
||||
|
|
||||
|
async getOneById(id: number): Promise<Optional<<%= properName %>>> { |
||||
|
return this.<%= camelCase %>Repository.findOneById(id); |
||||
|
} |
||||
|
|
||||
|
async saveNew(body: <%= properName %>Dto): Promise<<%= properName %>> { |
||||
|
let <%= camelCase %>New = new <%= properName %>(); |
||||
|
|
||||
|
// Complete with the mappings |
||||
|
|
||||
|
<%= camelCase %>New = await this.<%= camelCase %>Repository.save(<%= camelCase %>New); |
||||
|
|
||||
|
return <%= camelCase %>New; |
||||
|
} |
||||
|
|
||||
|
async update(id: number, body: <%= properName %>Dto): Promise<<%= properName %>> { |
||||
|
let <%= camelCase %>Found = (await this.<%= camelCase %>Repository.findOneById(id)).orElseThrow( |
||||
|
() => new NotFoundException(), |
||||
|
); |
||||
|
|
||||
|
// Complete with the mappings |
||||
|
|
||||
|
<%= camelCase %>Found = await this.<%= camelCase %>Repository.save(<%= camelCase %>Found); |
||||
|
|
||||
|
return <%= camelCase %>Found; |
||||
|
} |
||||
|
|
||||
|
async deleteById(id: number): Promise<void> { |
||||
|
const <%= camelCase %>Found = (await this.<%= camelCase %>Repository.findOneById( |
||||
|
id, |
||||
|
)).orElseThrow(() => new NotFoundException()); |
||||
|
await this.<%= camelCase %>Repository.remove(<%= camelCase %>Found); |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,18 @@ |
|||||
|
--- |
||||
|
to: src/modules/<%=h.changeCase.param(name)%>/<%=h.changeCase.param(name)%>.module.ts |
||||
|
--- |
||||
|
<% |
||||
|
properName = h.changeCase.pascal(name); |
||||
|
kebabName = h.changeCase.param(name); |
||||
|
%>import { Module } from '@nestjs/common'; |
||||
|
import { TypeOrmModule } from '@nestjs/typeorm'; |
||||
|
|
||||
|
@Module({ |
||||
|
imports: [], |
||||
|
controllers: [], |
||||
|
providers: [], |
||||
|
exports: [], |
||||
|
}) |
||||
|
export class <%=properName%>Module {} |
||||
|
|
||||
|
|
||||
@ -0,0 +1,10 @@ |
|||||
|
// see types of prompts:
|
||||
|
// https://github.com/enquirer/enquirer/tree/master/examples
|
||||
|
//
|
||||
|
module.exports = [ |
||||
|
{ |
||||
|
type: 'input', |
||||
|
name: 'name', |
||||
|
message: 'What is your module name?', |
||||
|
}, |
||||
|
]; |
||||
@ -0,0 +1,13 @@ |
|||||
|
--- |
||||
|
to: src/app.routes.ts |
||||
|
after: export const appRoutes.* |
||||
|
inject: true |
||||
|
skip_if: /<%= h.changeCase.param(name) %> |
||||
|
--- |
||||
|
<% |
||||
|
properName = h.changeCase.pascal(name); |
||||
|
kebabName = h.changeCase.param(name); |
||||
|
%> { |
||||
|
path: '/<%= kebabName %>s', |
||||
|
module: <%= properName %>Module, |
||||
|
}, |
||||
@ -0,0 +1,10 @@ |
|||||
|
--- |
||||
|
to: src/app.routes.ts |
||||
|
after: import { Routes } from 'nest-router'; |
||||
|
inject: true |
||||
|
skip_if: modules/<%= h.changeCase.param(name) %> |
||||
|
--- |
||||
|
<% |
||||
|
properName = h.changeCase.pascal(name); |
||||
|
kebabName = h.changeCase.param(name); |
||||
|
%>import { <%= properName %>Module } from './modules/<%= kebabName %>/<%= kebabName %>.module'; |
||||
@ -0,0 +1,6 @@ |
|||||
|
{ |
||||
|
"port": 9911, |
||||
|
"name": "TP_CI SERVER", |
||||
|
"output": "doc-server", |
||||
|
"tsconfig": "./tsconfig.json" |
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
version: '2' |
||||
|
services: |
||||
|
ognion-boilerplate-postgresql: |
||||
|
image: postgres:9.6.5 |
||||
|
environment: |
||||
|
- POSTGRES_USER=tpCi |
||||
|
- POSTGRES_PASSWORD=someNotSecurePassword |
||||
|
ports: |
||||
|
- 5432:5432 |
||||
@ -0,0 +1,14 @@ |
|||||
|
## API |
||||
|
API_PORT=3000 |
||||
|
API_HOST=localhost |
||||
|
API_PROTOCOL=http |
||||
|
|
||||
|
## LOGGER |
||||
|
LOG_LEVEL=debug |
||||
|
LOG_SQL_REQUEST=true |
||||
|
|
||||
|
## DB [TypeORM] |
||||
|
DATABASE_URL=postgres://tpCi:someNotSecurePassword@localhost:5432/tpCi |
||||
|
|
||||
|
## AUTHENTICATION [JWT] |
||||
|
JWT_SECRET=bananana |
||||
@ -0,0 +1,5 @@ |
|||||
|
{ |
||||
|
"language": "ts", |
||||
|
"collection": "@nestjs/schematics", |
||||
|
"sourceRoot": "src" |
||||
|
} |
||||
@ -0,0 +1,122 @@ |
|||||
|
{ |
||||
|
"name": "ci-tp", |
||||
|
"version": "0.0.1", |
||||
|
"description": "A CI tp for M1 III", |
||||
|
"author": "Nicolas Beaussart <nic.beaussart@gmail.com>", |
||||
|
"license": "MIT", |
||||
|
"scripts": { |
||||
|
"prebuild": "rimraf dist", |
||||
|
"build": "nest build", |
||||
|
"start": "nest start", |
||||
|
"start:dev": "nest start --watch", |
||||
|
"start:debug": "nest start --debug --watch", |
||||
|
"start:prod": "node dist/main", |
||||
|
"lint": "tslint -p tsconfig.json -c tslint.json", |
||||
|
"test": "jest", |
||||
|
"test:ci": "jest --runInBand --coverage", |
||||
|
"doc:build": "./node_modules/.bin/compodoc -c compodoc.server.json", |
||||
|
"doc": "./node_modules/.bin/compodoc -c compodoc.server.json -s -o -w", |
||||
|
"test:watch": "jest --watch", |
||||
|
"test:cov": "jest --coverage", |
||||
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", |
||||
|
"test:e2e": "jest --config ./test/jest-e2e.json", |
||||
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\" \"./**/*.{yaml,yml}\"", |
||||
|
"format:check": "prettier --list-different \"src/**/*.ts\" \"test/**/*.ts\" \"./**/*.{yaml,yml}\"", |
||||
|
"generate": "hygen" |
||||
|
}, |
||||
|
"dependencies": { |
||||
|
"@hapi/joi": "^16.1.5", |
||||
|
"@nestjs/common": "^6.1.1", |
||||
|
"@nestjs/core": "^6.1.1", |
||||
|
"@nestjs/jwt": "^6.0.0", |
||||
|
"@nestjs/passport": "^6.0.0", |
||||
|
"@nestjs/platform-express": "^6.1.1", |
||||
|
"@nestjs/swagger": "^3.0.2", |
||||
|
"@nestjs/typeorm": "^6.1.0", |
||||
|
"argon2": "0.25.1", |
||||
|
"class-sanitizer": "^0.0.5", |
||||
|
"class-transformer": "^0.2.2", |
||||
|
"class-validator": "^0.10.1", |
||||
|
"dotenv": "^8.0.0", |
||||
|
"nest-router": "^1.0.9", |
||||
|
"passport": "^0.4.0", |
||||
|
"passport-jwt": "^4.0.0", |
||||
|
"pg": "^7.10.0", |
||||
|
"prom-client": "^11.3.0", |
||||
|
"reflect-metadata": "^0.1.13", |
||||
|
"rxjs": "^6.5.2", |
||||
|
"swagger-ui-express": "^4.0.2", |
||||
|
"typeorm": "^0.2.17", |
||||
|
"typescript": "^3.4.5", |
||||
|
"typescript-optional": "^2.0.1", |
||||
|
"url-value-parser": "^2.0.1", |
||||
|
"winston": "^3.2.1" |
||||
|
}, |
||||
|
"devDependencies": { |
||||
|
"@compodoc/compodoc": "^1.1.11", |
||||
|
"@nestjs/cli": "^6.10.1", |
||||
|
"@nestjs/testing": "^6.8.0", |
||||
|
"@types/express": "^4.16.1", |
||||
|
"@types/jest": "^24.0.12", |
||||
|
"@types/joi": "^14.3.3", |
||||
|
"@types/node": "^12.0.0", |
||||
|
"@types/supertest": "^2.0.5", |
||||
|
"husky": "^3.0.8", |
||||
|
"hygen": "^5.0.1", |
||||
|
"jest": "^24.8.0", |
||||
|
"lint-staged": "^9.4.1", |
||||
|
"prettier": "^1.17.0", |
||||
|
"rimraf": "^3.0.0", |
||||
|
"supertest": "^4.0.2", |
||||
|
"ts-jest": "^24.0.2", |
||||
|
"ts-loader": "^6.0.0", |
||||
|
"ts-node": "^8.1.0", |
||||
|
"tsconfig-paths": "^3.8.0", |
||||
|
"tslint": "5.20.0", |
||||
|
"tslint-config-prettier": "^1.18.0" |
||||
|
}, |
||||
|
"husky": { |
||||
|
"hooks": { |
||||
|
"pre-commit": "lint-staged" |
||||
|
} |
||||
|
}, |
||||
|
"lint-staged": { |
||||
|
"*.ts": [ |
||||
|
"prettier --write", |
||||
|
"tslint -p tsconfig.json -c tslint.json", |
||||
|
"git add" |
||||
|
], |
||||
|
"*.{js,json,md}": [ |
||||
|
"prettier --write", |
||||
|
"git add" |
||||
|
] |
||||
|
}, |
||||
|
"jest": { |
||||
|
"moduleFileExtensions": [ |
||||
|
"js", |
||||
|
"json", |
||||
|
"ts" |
||||
|
], |
||||
|
"rootDir": "src", |
||||
|
"testRegex": ".spec.ts$", |
||||
|
"transform": { |
||||
|
"^.+\\.(t|j)s$": "ts-jest" |
||||
|
}, |
||||
|
"coverageDirectory": "../coverage", |
||||
|
"testEnvironment": "node" |
||||
|
}, |
||||
|
"greenkeeper": { |
||||
|
"commitMessages": { |
||||
|
"initialBadge": ":pencil: Add Greenkeeper badge", |
||||
|
"initialDependencies": ":arrow_up: Upgrade: Update dependencies", |
||||
|
"initialBranches": ":tada: Build: Whitelist greenkeeper branches", |
||||
|
"dependencyUpdate": ":arrow_up: Upgrade: Update ${dependency} to version ${version}", |
||||
|
"devDependencyUpdate": ":arrow_up: Upgrade: Update ${dependency} to version ${version}", |
||||
|
"dependencyPin": ":pushpin: Pin ${dependency} to ${oldVersion}", |
||||
|
"devDependencyPin": ":pushpin: Pin ${dependency} to ${oldVersion}", |
||||
|
"addConfigFile": ":wrench: Add Greenkeeper config file", |
||||
|
"updateConfigFile": ":wrench: Update Greenkeeper config file", |
||||
|
"lockfileUpdate": ":wrench: Update lockfile ${lockfilePath}" |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,21 @@ |
|||||
|
import { Test, TestingModule } from '@nestjs/testing'; |
||||
|
import { INestApplication } from '@nestjs/common'; |
||||
|
import { AppController } from './app.controller'; |
||||
|
|
||||
|
describe('AppController', () => { |
||||
|
let app: TestingModule; |
||||
|
|
||||
|
beforeAll(async () => { |
||||
|
app = await Test.createTestingModule({ |
||||
|
controllers: [AppController], |
||||
|
providers: [], |
||||
|
}).compile(); |
||||
|
}); |
||||
|
|
||||
|
describe('ping', () => { |
||||
|
it('should return "Hello World!"', () => { |
||||
|
const appController = app.get<AppController>(AppController); |
||||
|
expect(appController.root()).toBe('pong'); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,13 @@ |
|||||
|
import { Get, Controller } from '@nestjs/common'; |
||||
|
import { Public } from './decorators/public.decorator'; |
||||
|
|
||||
|
@Controller() |
||||
|
export class AppController { |
||||
|
constructor() {} |
||||
|
|
||||
|
@Get('ping') |
||||
|
@Public() |
||||
|
root(): string { |
||||
|
return 'pong'; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,58 @@ |
|||||
|
import { |
||||
|
ClassSerializerInterceptor, |
||||
|
MiddlewareConsumer, |
||||
|
Module, |
||||
|
NestModule, |
||||
|
} from '@nestjs/common'; |
||||
|
import { AppController } from './app.controller'; |
||||
|
import { LoggerModule } from './modules/core/logger/logger.module'; |
||||
|
import { RouterModule } from 'nest-router'; |
||||
|
import { appRoutes } from './app.routes'; |
||||
|
import { ConfigModule } from './modules/core/config/config.module'; |
||||
|
import { RolesGuard } from './guards/roles.guard'; |
||||
|
import { UserModule } from './modules/user/user.module'; |
||||
|
import { ConfigService } from './modules/core/config/config.service'; |
||||
|
import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm'; |
||||
|
|
||||
|
import { RolesModule } from './modules/roles/roles.module'; |
||||
|
import { AuthModule } from './modules/core/auth/auth.module'; |
||||
|
import { PromModule } from './modules/core/metrics/metrics.module'; |
||||
|
import { InboundMiddleware } from './modules/core/metrics/middleware/inbound.middleware'; |
||||
|
// needle-module-import
|
||||
|
|
||||
|
@Module({ |
||||
|
imports: [ |
||||
|
ConfigModule, // Global
|
||||
|
TypeOrmModule.forRootAsync({ |
||||
|
imports: [ConfigModule], |
||||
|
useFactory: async ( |
||||
|
configService: ConfigService, |
||||
|
): Promise<TypeOrmModuleOptions> => ({ |
||||
|
type: 'postgres', |
||||
|
url: configService.databaseUrl, |
||||
|
entities: [__dirname + '/**/*.entity{.ts,.js}'], |
||||
|
synchronize: true, |
||||
|
logging: configService.isLoggingDb ? 'all' : false, |
||||
|
}), |
||||
|
inject: [ConfigService], |
||||
|
}), |
||||
|
PromModule.forRoot({ |
||||
|
defaultLabels: { |
||||
|
app: 'v1.0.0', |
||||
|
}, |
||||
|
}), |
||||
|
LoggerModule, // Global
|
||||
|
RouterModule.forRoutes(appRoutes), |
||||
|
AuthModule, |
||||
|
UserModule, |
||||
|
RolesModule, |
||||
|
// needle-module-includes
|
||||
|
], |
||||
|
controllers: [AppController], |
||||
|
providers: [RolesGuard, ClassSerializerInterceptor], |
||||
|
}) |
||||
|
export class AppModule implements NestModule { |
||||
|
configure(consumer: MiddlewareConsumer): MiddlewareConsumer | void { |
||||
|
consumer.apply(InboundMiddleware).forRoutes('*'); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
import { Routes } from 'nest-router'; |
||||
|
import { UserModule } from './modules/user/user.module'; |
||||
|
import { RolesModule } from './modules/roles/roles.module'; |
||||
|
import { AuthController } from './modules/core/auth/auth.controller'; |
||||
|
|
||||
|
export const appRoutes: Routes = [ |
||||
|
{ |
||||
|
path: '/users', |
||||
|
module: UserModule, |
||||
|
}, |
||||
|
{ |
||||
|
path: '/roles', |
||||
|
module: RolesModule, |
||||
|
}, |
||||
|
{ |
||||
|
path: '/auth', |
||||
|
module: AuthController, |
||||
|
}, |
||||
|
]; |
||||
@ -0,0 +1,8 @@ |
|||||
|
import { createParamDecorator } from '@nestjs/common'; |
||||
|
import { User } from '../modules/user/user.entity'; |
||||
|
|
||||
|
export const CurrentUser = createParamDecorator( |
||||
|
(data: any, req: any): User => { |
||||
|
return req.user; |
||||
|
}, |
||||
|
); |
||||
@ -0,0 +1,3 @@ |
|||||
|
import { SetMetadata } from '@nestjs/common'; |
||||
|
|
||||
|
export const Public = () => SetMetadata('isPublic', true); |
||||
@ -0,0 +1,3 @@ |
|||||
|
import { SetMetadata } from '@nestjs/common'; |
||||
|
|
||||
|
export const Roles = (...roles: string[]) => SetMetadata('roles', roles); |
||||
@ -0,0 +1,24 @@ |
|||||
|
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; |
||||
|
import { Reflector } from '@nestjs/core'; |
||||
|
import { User } from '../modules/user/user.entity'; |
||||
|
import { Role } from '../modules/roles/roles.entity'; |
||||
|
|
||||
|
@Injectable() |
||||
|
export class RolesGuard implements CanActivate { |
||||
|
constructor(private readonly reflector: Reflector) {} |
||||
|
|
||||
|
canActivate(context: ExecutionContext): boolean { |
||||
|
const roles = this.reflector.get<string[]>('roles', context.getHandler()); |
||||
|
|
||||
|
if (!roles) { |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
const request = context.switchToHttp().getRequest(); |
||||
|
const user: User = request.user; |
||||
|
const hasRole = () => |
||||
|
user.roles.some((role: Role) => roles.includes(role.name)); |
||||
|
|
||||
|
return user && user.roles && hasRole(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,71 @@ |
|||||
|
import { NestFactory, Reflector } from '@nestjs/core'; |
||||
|
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; |
||||
|
import { config } from 'dotenv'; |
||||
|
import { AppModule } from './app.module'; |
||||
|
import { LoggerExceptionInterceptor } from './modules/core/logger/logger-exception.interceptor'; |
||||
|
import { LoggerModule } from './modules/core/logger/logger.module'; |
||||
|
import { ValidatorPipe } from './modules/core/validation/validator.pipe'; |
||||
|
import { LoggerService } from './modules/core/logger/logger.service'; |
||||
|
import { RolesGuard } from './guards/roles.guard'; |
||||
|
import { ConfigService } from './modules/core/config/config.service'; |
||||
|
import { ConfigModule } from './modules/core/config/config.module'; |
||||
|
import { NestExpressApplication } from '@nestjs/platform-express'; |
||||
|
import { ClassSerializerInterceptor } from '@nestjs/common'; |
||||
|
import { InboundMiddleware } from './modules/core/metrics/middleware/inbound.middleware'; |
||||
|
import { PromModule } from './modules/core/metrics/metrics.module'; |
||||
|
import { MetricsInterceptor } from './modules/core/metrics/interceptors/metrics.interceptor'; |
||||
|
import { AuthGuard } from './modules/core/auth/auth.guard'; |
||||
|
|
||||
|
async function bootstrap() { |
||||
|
// Use .env to configure environment variables (process.env)
|
||||
|
config(); |
||||
|
|
||||
|
const app = await NestFactory.create<NestExpressApplication>(AppModule); |
||||
|
|
||||
|
// Set logger
|
||||
|
app.useLogger(app.get(LoggerService)); |
||||
|
|
||||
|
// Enable cors
|
||||
|
app.enableCors(); |
||||
|
|
||||
|
// Interceptors
|
||||
|
const loggerInterceptor = app |
||||
|
.select(LoggerModule) |
||||
|
.get(LoggerExceptionInterceptor); |
||||
|
const classSerializer = app.select(AppModule).get(ClassSerializerInterceptor); |
||||
|
const metricsInterceptor = app.select(AppModule).get(MetricsInterceptor); |
||||
|
app.useGlobalInterceptors( |
||||
|
metricsInterceptor, |
||||
|
loggerInterceptor, // Log exceptions
|
||||
|
classSerializer, |
||||
|
); |
||||
|
|
||||
|
const connfigService = app.select(ConfigModule).get(ConfigService); |
||||
|
|
||||
|
// Guards
|
||||
|
const reflector = app.get(Reflector); |
||||
|
const rolesGuard = app.select(AppModule).get(RolesGuard); |
||||
|
app.useGlobalGuards(new AuthGuard(reflector), rolesGuard); |
||||
|
|
||||
|
// Validators
|
||||
|
app.useGlobalPipes( |
||||
|
new ValidatorPipe(), // Validate inputs
|
||||
|
); |
||||
|
|
||||
|
// Swagger
|
||||
|
const options = new DocumentBuilder() |
||||
|
.setTitle('Boilerplate nest') |
||||
|
.setDescription('The boilerplate API description') |
||||
|
.setVersion('0.0.1') |
||||
|
.addBearerAuth() |
||||
|
.build(); |
||||
|
const document = SwaggerModule.createDocument(app, options); |
||||
|
SwaggerModule.setup('/docs', app, document); |
||||
|
|
||||
|
const server = await app.listen(connfigService.port); |
||||
|
app |
||||
|
.get(LoggerService) |
||||
|
.info(`Application is listening on port ${connfigService.port}.`); |
||||
|
return server; |
||||
|
} |
||||
|
bootstrap(); |
||||
@ -0,0 +1,40 @@ |
|||||
|
import { ApiUseTags, ApiBearerAuth } from '@nestjs/swagger'; |
||||
|
import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common'; |
||||
|
import { LoginDto, TokenDto } from './auth.dto'; |
||||
|
import { AuthService } from './auth.service'; |
||||
|
import { User } from '../../user/user.entity'; |
||||
|
import { UserService } from '../../user/user.service'; |
||||
|
import { UserDtoRegister } from '../../user/user.dto'; |
||||
|
import { CurrentUser } from '../../../decorators/currentUser.decorator'; |
||||
|
import { Public } from '../../../decorators/public.decorator'; |
||||
|
|
||||
|
@ApiUseTags('Auth') |
||||
|
@Controller() |
||||
|
export class AuthController { |
||||
|
constructor( |
||||
|
private readonly authService: AuthService, |
||||
|
private readonly userService: UserService, |
||||
|
) {} |
||||
|
|
||||
|
@Public() |
||||
|
@Post('login') |
||||
|
async signIn(@Body() userLogin: LoginDto): Promise<TokenDto> { |
||||
|
const token = await this.authService.signIn( |
||||
|
userLogin.email, |
||||
|
userLogin.password, |
||||
|
); |
||||
|
return { token }; |
||||
|
} |
||||
|
|
||||
|
@Public() |
||||
|
@Post('register') |
||||
|
async registerUser(@Body() userRegister: UserDtoRegister): Promise<User> { |
||||
|
return this.userService.saveNew(userRegister); |
||||
|
} |
||||
|
|
||||
|
@Get('me') |
||||
|
@ApiBearerAuth() |
||||
|
async getMe(@CurrentUser() loggedUser: User) { |
||||
|
return loggedUser; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,17 @@ |
|||||
|
import { IsEmail, MinLength } from 'class-validator'; |
||||
|
import { ApiModelProperty } from '@nestjs/swagger'; |
||||
|
|
||||
|
export class TokenDto { |
||||
|
token: string; |
||||
|
} |
||||
|
|
||||
|
export class LoginDto { |
||||
|
@MinLength(1) |
||||
|
@IsEmail() |
||||
|
@ApiModelProperty({ example: 'foo@bar.fr' }) |
||||
|
email: string; |
||||
|
|
||||
|
@MinLength(1) |
||||
|
@ApiModelProperty({ example: 'azerty' }) |
||||
|
password: string; |
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
import { |
||||
|
ExecutionContext, |
||||
|
Injectable, |
||||
|
UnauthorizedException, |
||||
|
} from '@nestjs/common'; |
||||
|
import { AuthGuard as NestAuthGuard } from '@nestjs/passport'; |
||||
|
import { Reflector } from '@nestjs/core'; |
||||
|
|
||||
|
@Injectable() |
||||
|
export class AuthGuard extends NestAuthGuard('jwt') { |
||||
|
public constructor(private readonly reflector: Reflector) { |
||||
|
super(); |
||||
|
} |
||||
|
|
||||
|
canActivate(context: ExecutionContext) { |
||||
|
const isPublic = this.reflector.get<boolean>( |
||||
|
'isPublic', |
||||
|
context.getHandler(), |
||||
|
); |
||||
|
|
||||
|
if (isPublic) { |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
// Add your custom authentication logic here
|
||||
|
// for example, call super.logIn(request) to establish a session.
|
||||
|
return super.canActivate(context); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,33 @@ |
|||||
|
import { Global, Module } from '@nestjs/common'; |
||||
|
import { AuthService } from './auth.service'; |
||||
|
import { UserModule } from '../../user/user.module'; |
||||
|
import { PassportModule } from '@nestjs/passport'; |
||||
|
import { JwtModule, JwtModuleOptions } from '@nestjs/jwt'; |
||||
|
import { ConfigModule } from '../config/config.module'; |
||||
|
import { ConfigService } from '../config/config.service'; |
||||
|
import { JwtStrategy } from './jwt.strategy'; |
||||
|
import { AuthController } from './auth.controller'; |
||||
|
|
||||
|
@Global() |
||||
|
@Module({ |
||||
|
imports: [ |
||||
|
UserModule, |
||||
|
// ConfigModule,
|
||||
|
PassportModule.register({ defaultStrategy: 'jwt' }), |
||||
|
JwtModule.registerAsync({ |
||||
|
imports: [ConfigModule], |
||||
|
inject: [ConfigService], |
||||
|
useFactory: async ( |
||||
|
configService: ConfigService, |
||||
|
): Promise<JwtModuleOptions> => ({ |
||||
|
secretOrPrivateKey: configService.jwtSecret, |
||||
|
signOptions: { |
||||
|
expiresIn: 3600, |
||||
|
}, |
||||
|
}), |
||||
|
}), |
||||
|
], |
||||
|
providers: [AuthService, JwtStrategy], |
||||
|
controllers: [AuthController], |
||||
|
}) |
||||
|
export class AuthModule {} |
||||
@ -0,0 +1,32 @@ |
|||||
|
import { Injectable, UnauthorizedException } from '@nestjs/common'; |
||||
|
import { UserService } from '../../user/user.service'; |
||||
|
import { JwtService } from '@nestjs/jwt'; |
||||
|
|
||||
|
export interface JwtPayload { |
||||
|
idUser: number; |
||||
|
} |
||||
|
|
||||
|
@Injectable() |
||||
|
export class AuthService { |
||||
|
constructor( |
||||
|
private readonly usersService: UserService, |
||||
|
private readonly jwtService: JwtService, |
||||
|
) {} |
||||
|
|
||||
|
async signIn(email: string, password: string): Promise<string> { |
||||
|
const userFound = (await this.usersService.getOnWithEmail( |
||||
|
email, |
||||
|
)).orElseThrow(() => new UnauthorizedException()); |
||||
|
if (!(await this.usersService.doPasswordMatch(userFound, password))) { |
||||
|
throw new UnauthorizedException(); |
||||
|
} |
||||
|
const user: JwtPayload = { idUser: userFound.id }; |
||||
|
return this.jwtService.sign(user); |
||||
|
} |
||||
|
|
||||
|
async validateUser(payload: JwtPayload): Promise<any> { |
||||
|
return (await this.usersService.getOneById(payload.idUser)).orElseThrow( |
||||
|
() => new UnauthorizedException(), |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,22 @@ |
|||||
|
import { Injectable } from '@nestjs/common'; |
||||
|
import { AuthService, JwtPayload } from './auth.service'; |
||||
|
import { PassportStrategy } from '@nestjs/passport'; |
||||
|
import { ExtractJwt, Strategy } from 'passport-jwt'; |
||||
|
import { ConfigService } from '../config/config.service'; |
||||
|
|
||||
|
@Injectable() |
||||
|
export class JwtStrategy extends PassportStrategy(Strategy) { |
||||
|
constructor( |
||||
|
private readonly authService: AuthService, |
||||
|
private readonly configService: ConfigService, |
||||
|
) { |
||||
|
super({ |
||||
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), |
||||
|
secretOrKey: configService.jwtSecret, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
async validate(payload: JwtPayload) { |
||||
|
return await this.authService.validateUser(payload); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
import { Global, Module } from '@nestjs/common'; |
||||
|
import { ConfigService } from './config.service'; |
||||
|
|
||||
|
@Global() |
||||
|
@Module({ |
||||
|
providers: [ConfigService], |
||||
|
exports: [ConfigService], |
||||
|
}) |
||||
|
export class ConfigModule {} |
||||
@ -0,0 +1,118 @@ |
|||||
|
import { Test, TestingModule } from '@nestjs/testing'; |
||||
|
import { ConfigService } from './config.service'; |
||||
|
import { expand } from 'rxjs/operators'; |
||||
|
|
||||
|
describe('ConfigService', () => { |
||||
|
const OLD_ENV = process.env; |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
jest.resetModules(); // this is important - it clears the cache
|
||||
|
process.env = { ...OLD_ENV }; |
||||
|
delete process.env.NODE_ENV; |
||||
|
}); |
||||
|
|
||||
|
afterEach(() => { |
||||
|
process.env = OLD_ENV; |
||||
|
}); |
||||
|
|
||||
|
const loadService = async (): Promise<ConfigService> => { |
||||
|
const module: TestingModule = await Test.createTestingModule({ |
||||
|
providers: [ConfigService], |
||||
|
}).compile(); |
||||
|
|
||||
|
return module.get<ConfigService>(ConfigService); |
||||
|
}; |
||||
|
|
||||
|
it('should have default values', async () => { |
||||
|
const dbUrl = 'gotodb please'; |
||||
|
const jwtSecrets = 'shhhh, secret'; |
||||
|
|
||||
|
// set the variables
|
||||
|
process.env.NODE_ENV = undefined; |
||||
|
process.env.API_PORT = undefined; |
||||
|
process.env.API_PROTOCOL = undefined; |
||||
|
process.env.LOG_LEVEL = undefined; |
||||
|
process.env.LOG_SQL_REQUEST = undefined; |
||||
|
process.env.DATABASE_URL = dbUrl; |
||||
|
process.env.JWT_SECRET = jwtSecrets; |
||||
|
|
||||
|
const configService = await loadService(); |
||||
|
|
||||
|
expect(configService.nodeEnv).toEqual('development'); |
||||
|
expect(configService.port).toEqual(3000); |
||||
|
expect(configService.loggerLevel).toEqual('debug'); |
||||
|
expect(configService.databaseUrl).toEqual(dbUrl); |
||||
|
expect(configService.jwtSecret).toEqual(jwtSecrets); |
||||
|
expect(configService.isLoggingDb).toEqual(false); |
||||
|
}); |
||||
|
|
||||
|
it('should error when no db url set', async () => { |
||||
|
const jwtSecrets = 'shhhh, secret'; |
||||
|
|
||||
|
// set the variables
|
||||
|
process.env.NODE_ENV = undefined; |
||||
|
process.env.API_PORT = undefined; |
||||
|
process.env.API_PROTOCOL = undefined; |
||||
|
process.env.LOG_LEVEL = undefined; |
||||
|
process.env.LOG_SQL_REQUEST = undefined; |
||||
|
process.env.DATABASE_URL = undefined; |
||||
|
process.env.JWT_SECRET = jwtSecrets; |
||||
|
|
||||
|
try { |
||||
|
const configService = await loadService(); |
||||
|
fail(); |
||||
|
} catch (e) { |
||||
|
expect(e.message).toEqual( |
||||
|
'Config validation error: "DATABASE_URL" is required', |
||||
|
); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
it('should error when no jwt token url set', async () => { |
||||
|
const dbUrl = 'gotodb please'; |
||||
|
|
||||
|
// set the variables
|
||||
|
process.env.NODE_ENV = undefined; |
||||
|
process.env.API_PORT = undefined; |
||||
|
process.env.API_PROTOCOL = undefined; |
||||
|
process.env.LOG_LEVEL = undefined; |
||||
|
process.env.LOG_SQL_REQUEST = undefined; |
||||
|
process.env.DATABASE_URL = dbUrl; |
||||
|
process.env.JWT_SECRET = undefined; |
||||
|
|
||||
|
try { |
||||
|
const configService = await loadService(); |
||||
|
fail(); |
||||
|
} catch (e) { |
||||
|
expect(e.message).toEqual( |
||||
|
'Config validation error: "JWT_SECRET" is required', |
||||
|
); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
it('should set correct values', async () => { |
||||
|
const dbUrl = 'gotodb please'; |
||||
|
const jwtSecrets = 'shhhh, secret'; |
||||
|
const nodeEnv = 'test'; |
||||
|
const apiPort = 2020; |
||||
|
const logLevel = 'silly'; |
||||
|
const logSqlRequest = true; |
||||
|
|
||||
|
// set the variables
|
||||
|
process.env.NODE_ENV = nodeEnv; |
||||
|
process.env.API_PORT = `${apiPort}`; |
||||
|
process.env.LOG_LEVEL = logLevel; |
||||
|
process.env.LOG_SQL_REQUEST = `${logSqlRequest}`; |
||||
|
process.env.DATABASE_URL = dbUrl; |
||||
|
process.env.JWT_SECRET = jwtSecrets; |
||||
|
|
||||
|
const configService = await loadService(); |
||||
|
|
||||
|
expect(configService.nodeEnv).toEqual(nodeEnv); |
||||
|
expect(configService.port).toEqual(apiPort); |
||||
|
expect(configService.loggerLevel).toEqual(logLevel); |
||||
|
expect(configService.databaseUrl).toEqual(dbUrl); |
||||
|
expect(configService.jwtSecret).toEqual(jwtSecrets); |
||||
|
expect(configService.isLoggingDb).toEqual(logSqlRequest); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,75 @@ |
|||||
|
import { Injectable } from '@nestjs/common'; |
||||
|
import * as Joi from '@hapi/joi'; |
||||
|
import { config as parseConfig } from 'dotenv'; |
||||
|
|
||||
|
interface EnvConfig { |
||||
|
[key: string]: any; |
||||
|
} |
||||
|
|
||||
|
@Injectable() |
||||
|
export class ConfigService { |
||||
|
private readonly envConfig: EnvConfig; |
||||
|
|
||||
|
constructor() { |
||||
|
try { |
||||
|
parseConfig(); |
||||
|
} catch (e) {} |
||||
|
|
||||
|
this.envConfig = this.validateInput(process.env); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Ensures all needed variables are set, and returns the validated JavaScript object |
||||
|
* including the applied default values. |
||||
|
*/ |
||||
|
private validateInput(envConfig: EnvConfig): EnvConfig { |
||||
|
const envVarsSchema: Joi.ObjectSchema = Joi.object({ |
||||
|
NODE_ENV: Joi.string() |
||||
|
.valid('development', 'production', 'test', 'provision') |
||||
|
.default('development'), |
||||
|
API_PORT: Joi.number().default(3000), |
||||
|
LOG_LEVEL: Joi.string() |
||||
|
.valid('error', 'warning', 'info', 'debug', 'silly') |
||||
|
.default('debug'), |
||||
|
DATABASE_URL: Joi.string().required(), |
||||
|
JWT_SECRET: Joi.string().required(), |
||||
|
LOG_SQL_REQUEST: Joi.boolean().default(false), |
||||
|
}); |
||||
|
|
||||
|
const { error, value: validatedEnvConfig } = envVarsSchema.validate( |
||||
|
envConfig, |
||||
|
{ |
||||
|
stripUnknown: true, |
||||
|
}, |
||||
|
); |
||||
|
|
||||
|
if (error) { |
||||
|
throw new Error(`Config validation error: ${error.message}`); |
||||
|
} |
||||
|
return validatedEnvConfig; |
||||
|
} |
||||
|
|
||||
|
get nodeEnv(): string { |
||||
|
return this.envConfig.NODE_ENV; |
||||
|
} |
||||
|
|
||||
|
get databaseUrl(): string { |
||||
|
return this.envConfig.DATABASE_URL; |
||||
|
} |
||||
|
|
||||
|
get jwtSecret(): string { |
||||
|
return this.envConfig.JWT_SECRET; |
||||
|
} |
||||
|
|
||||
|
get isLoggingDb(): boolean { |
||||
|
return this.envConfig.LOG_SQL_REQUEST; |
||||
|
} |
||||
|
|
||||
|
get loggerLevel(): string { |
||||
|
return this.envConfig.LOG_LEVEL; |
||||
|
} |
||||
|
|
||||
|
get port(): number { |
||||
|
return this.envConfig.API_PORT; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,8 @@ |
|||||
|
import { Module } from '@nestjs/common'; |
||||
|
import { CryptoService } from './crypto.service'; |
||||
|
|
||||
|
@Module({ |
||||
|
providers: [CryptoService], |
||||
|
exports: [CryptoService], |
||||
|
}) |
||||
|
export class CryptoModule {} |
||||
@ -0,0 +1,32 @@ |
|||||
|
import { Test, TestingModule } from '@nestjs/testing'; |
||||
|
import { CryptoService } from './crypto.service'; |
||||
|
import * as argon from 'argon2'; |
||||
|
|
||||
|
describe('CryptoService', () => { |
||||
|
let service: CryptoService; |
||||
|
beforeAll(async () => { |
||||
|
const module: TestingModule = await Test.createTestingModule({ |
||||
|
providers: [CryptoService], |
||||
|
}).compile(); |
||||
|
service = module.get<CryptoService>(CryptoService); |
||||
|
}); |
||||
|
it('should be defined', () => { |
||||
|
expect(service).toBeDefined(); |
||||
|
}); |
||||
|
|
||||
|
it('should hash a password and verify if it is correct', async () => { |
||||
|
const toHash = 'some random string'; |
||||
|
|
||||
|
const hashed = await service.hash(toHash); |
||||
|
|
||||
|
expect(await service.compare(toHash, hashed)).toBeTruthy(); |
||||
|
}); |
||||
|
|
||||
|
it('should hash a password and return false if not same password', async () => { |
||||
|
const toHash = 'some random string'; |
||||
|
|
||||
|
const hashed = await service.hash(toHash); |
||||
|
|
||||
|
expect(await service.compare(toHash + 'A', hashed)).toBeFalsy(); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,28 @@ |
|||||
|
import { Injectable } from '@nestjs/common'; |
||||
|
import { argon2id, hash, verify } from 'argon2'; |
||||
|
|
||||
|
@Injectable() |
||||
|
export class CryptoService { |
||||
|
private readonly type = argon2id; |
||||
|
|
||||
|
constructor() {} |
||||
|
|
||||
|
/** |
||||
|
* Compare hash |
||||
|
* @param {string} plain |
||||
|
* @param {string} hash |
||||
|
* @returns {Promise<boolean>} |
||||
|
*/ |
||||
|
public async compare(plain: string, hashString: string): Promise<boolean> { |
||||
|
return await verify(hashString, plain); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Generate hash |
||||
|
* @param {string} plain |
||||
|
* @returns {Promise<string>} |
||||
|
*/ |
||||
|
public async hash(plain: string): Promise<string> { |
||||
|
return await hash(plain, { type: this.type }); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,43 @@ |
|||||
|
import { |
||||
|
CallHandler, |
||||
|
ExecutionContext, |
||||
|
HttpException, |
||||
|
Injectable, |
||||
|
NestInterceptor, |
||||
|
} from '@nestjs/common'; |
||||
|
import { Observable } from 'rxjs'; |
||||
|
import { catchError } from 'rxjs/operators'; |
||||
|
import { LoggerService } from './logger.service'; |
||||
|
|
||||
|
@Injectable() |
||||
|
export class LoggerExceptionInterceptor implements NestInterceptor { |
||||
|
constructor(private loggerService: LoggerService) {} |
||||
|
|
||||
|
intercept(context: ExecutionContext, next: CallHandler): Observable<any> { |
||||
|
const request = context.switchToHttp().getRequest(); |
||||
|
return next.handle().pipe( |
||||
|
catchError(exception => { |
||||
|
if (exception instanceof HttpException) { |
||||
|
// If 500, log as error
|
||||
|
if (500 <= exception.getStatus()) |
||||
|
this.loggerService.error( |
||||
|
'HttpException ' + exception.getStatus(), |
||||
|
request.path, |
||||
|
exception.getResponse(), |
||||
|
); |
||||
|
// Else log as debug (we don't want 4xx errors in production)
|
||||
|
else { |
||||
|
this.loggerService.debug( |
||||
|
'HttpException ' + exception.getStatus(), |
||||
|
request.path, |
||||
|
exception.getResponse(), |
||||
|
); |
||||
|
} |
||||
|
} else { |
||||
|
this.loggerService.error('Unexpected error', request.path, exception); |
||||
|
} |
||||
|
throw exception; |
||||
|
}), |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
export const LOGGER_WINSTON_PROVIDER = 'LOGGER_WINSTON_PROVIDER'; |
||||
|
|
||||
|
export enum LOGGER_LEVEL { |
||||
|
ERROR = 'error', |
||||
|
WARNING = 'warning', |
||||
|
INFO = 'info', |
||||
|
DEBUG = 'debug', |
||||
|
SILLY = 'silly', |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
import { Global, Module } from '@nestjs/common'; |
||||
|
import { LoggerService } from './logger.service'; |
||||
|
import { LoggerExceptionInterceptor } from './logger-exception.interceptor'; |
||||
|
import { loggerProviders } from './logger.providers'; |
||||
|
|
||||
|
@Global() |
||||
|
@Module({ |
||||
|
providers: [...loggerProviders, LoggerService, LoggerExceptionInterceptor], |
||||
|
exports: [LoggerService, LoggerExceptionInterceptor], |
||||
|
}) |
||||
|
export class LoggerModule {} |
||||
@ -0,0 +1,33 @@ |
|||||
|
import { LOGGER_LEVEL, LOGGER_WINSTON_PROVIDER } from './logger.constants'; |
||||
|
import { createLogger, transports, format } from 'winston'; |
||||
|
|
||||
|
export const loggerProviders = [ |
||||
|
{ |
||||
|
provide: LOGGER_WINSTON_PROVIDER, |
||||
|
useFactory: () => { |
||||
|
const LOG_LEVEL = LOGGER_LEVEL.DEBUG; |
||||
|
|
||||
|
const winstonTransports = [new transports.Console({})]; |
||||
|
|
||||
|
const winstonFormaters = format.combine( |
||||
|
format.colorize(), |
||||
|
format.timestamp({ |
||||
|
format: 'YYYY-MM-DD HH:mm:ss', |
||||
|
}), |
||||
|
format.align(), |
||||
|
format.printf( |
||||
|
info => |
||||
|
`[Nest] ${process.pid} - ${info.timestamp} ${ |
||||
|
info.level |
||||
|
}: ${info.message.trim()}`,
|
||||
|
), |
||||
|
); |
||||
|
|
||||
|
return createLogger({ |
||||
|
level: LOG_LEVEL, |
||||
|
transports: winstonTransports, |
||||
|
format: winstonFormaters, |
||||
|
}); |
||||
|
}, |
||||
|
}, |
||||
|
]; |
||||
@ -0,0 +1,21 @@ |
|||||
|
import { Test, TestingModule } from '@nestjs/testing'; |
||||
|
import { LoggerService } from './logger.service'; |
||||
|
import { ConfigService } from '../config/config.service'; |
||||
|
import { loggerProviders } from './logger.providers'; |
||||
|
|
||||
|
describe('LoggerService', () => { |
||||
|
let service: LoggerService; |
||||
|
beforeAll(async () => { |
||||
|
const module: TestingModule = await Test.createTestingModule({ |
||||
|
providers: [ |
||||
|
LoggerService, |
||||
|
{ provide: ConfigService, useValue: { loggerLevel: 'info' } }, |
||||
|
...loggerProviders, |
||||
|
], |
||||
|
}).compile(); |
||||
|
service = module.get<LoggerService>(LoggerService); |
||||
|
}); |
||||
|
it('should be defined', () => { |
||||
|
expect(service).toBeDefined(); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,46 @@ |
|||||
|
import { |
||||
|
Inject, |
||||
|
Injectable, |
||||
|
LoggerService as NestLoggerService, |
||||
|
} from '@nestjs/common'; |
||||
|
import { LOGGER_LEVEL, LOGGER_WINSTON_PROVIDER } from './logger.constants'; |
||||
|
import { Logger } from 'winston'; |
||||
|
import { ConfigService } from '../config/config.service'; |
||||
|
|
||||
|
@Injectable() |
||||
|
export class LoggerService implements NestLoggerService { |
||||
|
constructor( |
||||
|
@Inject(LOGGER_WINSTON_PROVIDER) private readonly winston: Logger, |
||||
|
private readonly configService: ConfigService, |
||||
|
) { |
||||
|
winston.transports[0].level = this.configService.loggerLevel; |
||||
|
} |
||||
|
|
||||
|
private logMessage(level: LOGGER_LEVEL, msg: string, ...meta): void { |
||||
|
this.winston.log(level, msg, ...meta); |
||||
|
} |
||||
|
|
||||
|
public log(msg: string, ...meta): void { |
||||
|
this.logMessage(LOGGER_LEVEL.INFO, msg, ...meta); |
||||
|
} |
||||
|
|
||||
|
public debug(msg: string, ...meta): void { |
||||
|
this.logMessage(LOGGER_LEVEL.DEBUG, msg, ...meta); |
||||
|
} |
||||
|
|
||||
|
public error(msg: string, ...meta): void { |
||||
|
this.logMessage(LOGGER_LEVEL.ERROR, msg, ...meta); |
||||
|
} |
||||
|
|
||||
|
public warn(msg: string, ...meta): void { |
||||
|
this.logMessage(LOGGER_LEVEL.WARNING, msg, ...meta); |
||||
|
} |
||||
|
|
||||
|
public info(msg: string, ...meta): void { |
||||
|
this.logMessage(LOGGER_LEVEL.INFO, msg, ...meta); |
||||
|
} |
||||
|
|
||||
|
public silly(msg: string, ...meta): void { |
||||
|
this.logMessage(LOGGER_LEVEL.SILLY, msg, ...meta); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
import { Inject } from '@nestjs/common'; |
||||
|
import { getMetricToken } from './prom.utils'; |
||||
|
|
||||
|
export const InjectCounterMetric = (name: string) => |
||||
|
Inject(getMetricToken(`Counter`, name)); |
||||
|
export const InjectGaugeMetric = (name: string) => |
||||
|
Inject(getMetricToken(`Gauge`, name)); |
||||
|
export const InjectHistogramMetric = (name: string) => |
||||
|
Inject(getMetricToken(`Histogram`, name)); |
||||
|
export const InjectSummaryMetric = (name: string) => |
||||
|
Inject(getMetricToken(`Summary`, name)); |
||||
@ -0,0 +1,11 @@ |
|||||
|
export function getMetricToken(type: string, name: string) { |
||||
|
return `${name}${type}`; |
||||
|
} |
||||
|
|
||||
|
export function getRegistryName(name: string) { |
||||
|
return `${name}PromRegistry`; |
||||
|
} |
||||
|
|
||||
|
export function getOptionsName(name: string) { |
||||
|
return `${name}PromOptions`; |
||||
|
} |
||||
@ -0,0 +1,66 @@ |
|||||
|
import { |
||||
|
Injectable, |
||||
|
NestInterceptor, |
||||
|
ExecutionContext, |
||||
|
CallHandler, |
||||
|
} from '@nestjs/common'; |
||||
|
import { Observable, throwError } from 'rxjs'; |
||||
|
import { catchError, tap } from 'rxjs/operators'; |
||||
|
import { Counter, Histogram } from 'prom-client'; |
||||
|
import { InjectHistogramMetric } from '../common/prom.decorators'; |
||||
|
import * as UrlValueParser from 'url-value-parser'; |
||||
|
import { parse } from 'url'; |
||||
|
|
||||
|
@Injectable() |
||||
|
export class MetricsInterceptor implements NestInterceptor<any, any> { |
||||
|
parser: any; |
||||
|
|
||||
|
constructor( |
||||
|
@InjectHistogramMetric('http_request_duration_seconds') |
||||
|
private readonly httpRequests: Histogram, |
||||
|
) {} |
||||
|
|
||||
|
intercept( |
||||
|
context: ExecutionContext, |
||||
|
next: CallHandler<any>, |
||||
|
): Observable<any> | Promise<Observable<any>> { |
||||
|
const req = context.switchToHttp().getRequest(); |
||||
|
const path = req.originalUrl || req.url; |
||||
|
|
||||
|
if (path.match(/\/metrics(\?.*?)?$/)) { |
||||
|
return next.handle(); |
||||
|
} |
||||
|
|
||||
|
const labels: any = {}; |
||||
|
const timer = this.httpRequests.startTimer({ |
||||
|
path: this.getPath(req), |
||||
|
method: req.method, |
||||
|
}); |
||||
|
|
||||
|
return next.handle().pipe( |
||||
|
tap(() => { |
||||
|
const res = context.switchToHttp().getResponse(); |
||||
|
timer({ status_code: this.getStatusCode(res) }); |
||||
|
}), |
||||
|
catchError(err => { |
||||
|
const statusCode = err.status ? err.status : '500'; |
||||
|
timer({ status_code: statusCode }); |
||||
|
return throwError(err); |
||||
|
}), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
getStatusCode(res: any): number { |
||||
|
return res.status_code || res.statusCode; |
||||
|
} |
||||
|
|
||||
|
getPath(req: any): string { |
||||
|
const path = parse(req.originalUrl || req.url).pathname; |
||||
|
|
||||
|
if (!this.parser) { |
||||
|
this.parser = new UrlValueParser(); |
||||
|
} |
||||
|
|
||||
|
return this.parser.replacePathValues(path); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,33 @@ |
|||||
|
import * as PromClient from 'prom-client'; |
||||
|
|
||||
|
export enum MetricType { |
||||
|
Counter, |
||||
|
Gauge, |
||||
|
Histogram, |
||||
|
Summary, |
||||
|
} |
||||
|
|
||||
|
export interface MetricTypeConfigurationInterface { |
||||
|
type: MetricType; |
||||
|
configuration?: any; |
||||
|
} |
||||
|
|
||||
|
export class MetricTypeCounter implements MetricTypeConfigurationInterface { |
||||
|
type: MetricType = MetricType.Counter; |
||||
|
configuration: PromClient.CounterConfiguration; |
||||
|
} |
||||
|
|
||||
|
export class MetricTypeGauge implements MetricTypeConfigurationInterface { |
||||
|
type: MetricType = MetricType.Gauge; |
||||
|
configuration: PromClient.GaugeConfiguration; |
||||
|
} |
||||
|
|
||||
|
export class MetricTypeHistogram implements MetricTypeConfigurationInterface { |
||||
|
type: MetricType = MetricType.Histogram; |
||||
|
configuration: PromClient.HistogramConfiguration; |
||||
|
} |
||||
|
|
||||
|
export class MetricTypeSummary implements MetricTypeConfigurationInterface { |
||||
|
type: MetricType = MetricType.Summary; |
||||
|
configuration: PromClient.SummaryConfiguration; |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
export interface PromModuleOptions { |
||||
|
[key: string]: any; |
||||
|
|
||||
|
/** |
||||
|
* Enable default metrics |
||||
|
* Under the hood, that call collectDefaultMetrics() |
||||
|
*/ |
||||
|
withDefaultsMetrics?: boolean; |
||||
|
|
||||
|
/** |
||||
|
* Enable internal controller to expose /metrics |
||||
|
* Caution: If you have a global prefix, don't forget to prefix it in prom |
||||
|
*/ |
||||
|
withDefaultController?: boolean; |
||||
|
|
||||
|
/** |
||||
|
* Create automatically http_requests_total counter |
||||
|
*/ |
||||
|
useHttpCounterMiddleware?: boolean; |
||||
|
|
||||
|
registryName?: string; |
||||
|
timeout?: number; |
||||
|
prefix?: string; |
||||
|
defaultLabels?: { |
||||
|
[key: string]: any; |
||||
|
}; |
||||
|
} |
||||
@ -0,0 +1,90 @@ |
|||||
|
import { Global, DynamicModule, Module } from '@nestjs/common'; |
||||
|
import { ModuleRef } from '@nestjs/core'; |
||||
|
import { PromModuleOptions } from './interfaces/prom-options.interface'; |
||||
|
import { |
||||
|
DEFAULT_PROM_REGISTRY, |
||||
|
PROM_REGISTRY_NAME, |
||||
|
DEFAULT_PROM_OPTIONS, |
||||
|
} from './metrics.constants'; |
||||
|
|
||||
|
import * as client from 'prom-client'; |
||||
|
import { |
||||
|
Registry, |
||||
|
collectDefaultMetrics, |
||||
|
DefaultMetricsCollectorConfiguration, |
||||
|
} from 'prom-client'; |
||||
|
import { getRegistryName, getOptionsName } from './common/prom.utils'; |
||||
|
|
||||
|
@Global() |
||||
|
@Module({}) |
||||
|
export class PromCoreModule { |
||||
|
constructor(private readonly moduleRef: ModuleRef) {} |
||||
|
|
||||
|
static forRoot(options: PromModuleOptions = {}): DynamicModule { |
||||
|
const { |
||||
|
withDefaultsMetrics, |
||||
|
registryName, |
||||
|
timeout, |
||||
|
prefix, |
||||
|
...promOptions |
||||
|
} = options; |
||||
|
|
||||
|
const promRegistryName = registryName |
||||
|
? getRegistryName(registryName) |
||||
|
: DEFAULT_PROM_REGISTRY; |
||||
|
|
||||
|
const promRegistryNameProvider = { |
||||
|
provide: PROM_REGISTRY_NAME, |
||||
|
useValue: promRegistryName, |
||||
|
}; |
||||
|
|
||||
|
// const promOptionName = registryName ?
|
||||
|
// getOptionsName(registryName)
|
||||
|
// : DEFAULT_PROM_OPTIONS;
|
||||
|
|
||||
|
const promRegistryOptionsProvider = { |
||||
|
provide: DEFAULT_PROM_OPTIONS, |
||||
|
useValue: options, |
||||
|
}; |
||||
|
|
||||
|
const registryProvider = { |
||||
|
provide: promRegistryName, |
||||
|
useFactory: (): Registry => { |
||||
|
let registry = client.register; |
||||
|
if (promRegistryName !== DEFAULT_PROM_REGISTRY) { |
||||
|
registry = new Registry(); |
||||
|
} |
||||
|
|
||||
|
if (withDefaultsMetrics !== false) { |
||||
|
const defaultMetricsOptions: DefaultMetricsCollectorConfiguration = { |
||||
|
register: registry, |
||||
|
}; |
||||
|
if (timeout) { |
||||
|
defaultMetricsOptions.timeout = timeout; |
||||
|
} |
||||
|
if (prefix) { |
||||
|
defaultMetricsOptions.prefix = prefix; |
||||
|
} |
||||
|
collectDefaultMetrics(defaultMetricsOptions); |
||||
|
} |
||||
|
|
||||
|
return registry; |
||||
|
}, |
||||
|
}; |
||||
|
|
||||
|
return { |
||||
|
module: PromCoreModule, |
||||
|
providers: [ |
||||
|
promRegistryNameProvider, |
||||
|
promRegistryOptionsProvider, |
||||
|
registryProvider, |
||||
|
], |
||||
|
exports: [registryProvider], |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* on destroy |
||||
|
*/ |
||||
|
onModuleDestroy() {} |
||||
|
} |
||||
@ -0,0 +1,3 @@ |
|||||
|
export const DEFAULT_PROM_REGISTRY = 'PromRegistry'; |
||||
|
export const DEFAULT_PROM_OPTIONS = 'PromOptions'; |
||||
|
export const PROM_REGISTRY_NAME = 'PromRegistryName'; |
||||
@ -0,0 +1,15 @@ |
|||||
|
import { Controller, Get, Res } from '@nestjs/common'; |
||||
|
import * as client from 'prom-client'; |
||||
|
import { ApiExcludeEndpoint } from '@nestjs/swagger'; |
||||
|
import { Public } from '../../../decorators/public.decorator'; |
||||
|
|
||||
|
@Controller('metrics') |
||||
|
export class PromController { |
||||
|
@Public() |
||||
|
@ApiExcludeEndpoint() |
||||
|
@Get() |
||||
|
index(@Res() res) { |
||||
|
res.set('Content-Type', client.register.contentType); |
||||
|
res.end(client.register.metrics()); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,147 @@ |
|||||
|
import { |
||||
|
Module, |
||||
|
DynamicModule, |
||||
|
NestModule, |
||||
|
MiddlewareConsumer, |
||||
|
Inject, |
||||
|
RequestMethod, |
||||
|
} from '@nestjs/common'; |
||||
|
import { PromCoreModule } from './metrics-core.module'; |
||||
|
import { |
||||
|
createPromCounterProvider, |
||||
|
createPromGaugeProvider, |
||||
|
createPromHistogramProvider, |
||||
|
createPromSummaryProvider, |
||||
|
} from './metrics.providers'; |
||||
|
import * as client from 'prom-client'; |
||||
|
import { InboundMiddleware } from './middleware/inbound.middleware'; |
||||
|
import { DEFAULT_PROM_OPTIONS } from './metrics.constants'; |
||||
|
import { PromModuleOptions } from './interfaces/prom-options.interface'; |
||||
|
import { |
||||
|
MetricType, |
||||
|
MetricTypeConfigurationInterface, |
||||
|
} from './interfaces/metric.type'; |
||||
|
import { PromController } from './metrics.constroller'; |
||||
|
import { MetricsInterceptor } from './interceptors/metrics.interceptor'; |
||||
|
import { APP_INTERCEPTOR } from '@nestjs/core'; |
||||
|
|
||||
|
@Module({}) |
||||
|
export class PromModule { |
||||
|
static forRoot(options: PromModuleOptions = {}): DynamicModule { |
||||
|
const { withDefaultController, ...promOptions } = options; |
||||
|
|
||||
|
// @ts-ignore
|
||||
|
const moduleForRoot: Required<DynamicModule> = { |
||||
|
module: PromModule, |
||||
|
imports: [PromCoreModule.forRoot(options)], |
||||
|
controllers: [PromController], |
||||
|
exports: [], |
||||
|
providers: [InboundMiddleware, MetricsInterceptor], |
||||
|
}; |
||||
|
|
||||
|
// if want to use the http counter
|
||||
|
/* |
||||
|
if (useHttpCounterMiddleware) { |
||||
|
const inboundProvider = createPromCounterProvider({ |
||||
|
name: 'http_requests_total', |
||||
|
help: 'http_requests_total Number of inbound request', |
||||
|
labelNames: ['method'], |
||||
|
}); |
||||
|
|
||||
|
moduleForRoot.providers = [...moduleForRoot.providers, inboundProvider]; |
||||
|
moduleForRoot.exports = [...moduleForRoot.exports, inboundProvider]; |
||||
|
} |
||||
|
*/ |
||||
|
|
||||
|
const requestProvider = createPromHistogramProvider({ |
||||
|
name: 'http_request_duration_seconds', |
||||
|
help: |
||||
|
'duration histogram of http responses labeled with: method, status_code and path', |
||||
|
labelNames: ['method', 'status_code', 'path'], |
||||
|
buckets: [0.003, 0.03, 0.1, 0.3, 1.5, 10], |
||||
|
}); |
||||
|
|
||||
|
const inboundProvider = createPromCounterProvider({ |
||||
|
name: 'http_requests_total', |
||||
|
help: 'http_requests_total Number of inbound request', |
||||
|
labelNames: ['method'], |
||||
|
}); |
||||
|
|
||||
|
moduleForRoot.providers = [ |
||||
|
...moduleForRoot.providers, |
||||
|
requestProvider, |
||||
|
inboundProvider, |
||||
|
]; |
||||
|
|
||||
|
moduleForRoot.exports = [ |
||||
|
...moduleForRoot.exports, |
||||
|
requestProvider, |
||||
|
inboundProvider, |
||||
|
]; |
||||
|
|
||||
|
return moduleForRoot; |
||||
|
} |
||||
|
|
||||
|
static forMetrics( |
||||
|
metrics: MetricTypeConfigurationInterface[], |
||||
|
): DynamicModule { |
||||
|
const providers = metrics.map(entry => { |
||||
|
switch (entry.type) { |
||||
|
case MetricType.Counter: |
||||
|
return createPromCounterProvider(entry.configuration); |
||||
|
case MetricType.Gauge: |
||||
|
return createPromGaugeProvider(entry.configuration); |
||||
|
case MetricType.Histogram: |
||||
|
return createPromHistogramProvider(entry.configuration); |
||||
|
case MetricType.Summary: |
||||
|
return createPromSummaryProvider(entry.configuration); |
||||
|
default: |
||||
|
throw new ReferenceError(`The type ${entry.type} is not supported`); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
return { |
||||
|
providers, |
||||
|
module: PromModule, |
||||
|
exports: providers, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
static forCounter(configuration: client.CounterConfiguration): DynamicModule { |
||||
|
const provider = createPromCounterProvider(configuration); |
||||
|
return { |
||||
|
module: PromModule, |
||||
|
providers: [provider], |
||||
|
exports: [provider], |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
static forGauge(configuration: client.GaugeConfiguration): DynamicModule { |
||||
|
const provider = createPromGaugeProvider(configuration); |
||||
|
return { |
||||
|
module: PromModule, |
||||
|
providers: [provider], |
||||
|
exports: [provider], |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
static forHistogram( |
||||
|
configuration: client.HistogramConfiguration, |
||||
|
): DynamicModule { |
||||
|
const provider = createPromHistogramProvider(configuration); |
||||
|
return { |
||||
|
module: PromModule, |
||||
|
providers: [provider], |
||||
|
exports: [provider], |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
static forSummary(configuration: client.SummaryConfiguration): DynamicModule { |
||||
|
const provider = createPromSummaryProvider(configuration); |
||||
|
return { |
||||
|
module: PromModule, |
||||
|
providers: [provider], |
||||
|
exports: [provider], |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,98 @@ |
|||||
|
import { DEFAULT_PROM_REGISTRY } from './metrics.constants'; |
||||
|
import { |
||||
|
Counter, |
||||
|
CounterConfiguration, |
||||
|
GaugeConfiguration, |
||||
|
Gauge, |
||||
|
HistogramConfiguration, |
||||
|
Histogram, |
||||
|
SummaryConfiguration, |
||||
|
Summary, |
||||
|
Registry, |
||||
|
} from 'prom-client'; |
||||
|
import { Provider } from '@nestjs/common'; |
||||
|
import { getMetricToken, getRegistryName } from './common/prom.utils'; |
||||
|
|
||||
|
export function createPromCounterProvider( |
||||
|
configuration: CounterConfiguration, |
||||
|
registryName: string = DEFAULT_PROM_REGISTRY, |
||||
|
): Provider { |
||||
|
return { |
||||
|
provide: getMetricToken('Counter', configuration.name), |
||||
|
useFactory: (registry: Registry) => { |
||||
|
const obj = new Counter({ |
||||
|
...configuration, |
||||
|
registers: [registry], |
||||
|
}); |
||||
|
return obj; |
||||
|
}, |
||||
|
inject: [ |
||||
|
registryName === DEFAULT_PROM_REGISTRY |
||||
|
? DEFAULT_PROM_REGISTRY |
||||
|
: getRegistryName(registryName), |
||||
|
], |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
export function createPromGaugeProvider( |
||||
|
configuration: GaugeConfiguration, |
||||
|
registryName: string = DEFAULT_PROM_REGISTRY, |
||||
|
): Provider { |
||||
|
return { |
||||
|
provide: getMetricToken('Gauge', configuration.name), |
||||
|
useFactory: (registry: Registry) => { |
||||
|
const obj = new Gauge({ |
||||
|
...configuration, |
||||
|
registers: [registry], |
||||
|
}); |
||||
|
return obj; |
||||
|
}, |
||||
|
inject: [ |
||||
|
registryName === DEFAULT_PROM_REGISTRY |
||||
|
? DEFAULT_PROM_REGISTRY |
||||
|
: getRegistryName(registryName), |
||||
|
], |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
export function createPromHistogramProvider( |
||||
|
configuration: HistogramConfiguration, |
||||
|
registryName: string = DEFAULT_PROM_REGISTRY, |
||||
|
): Provider { |
||||
|
return { |
||||
|
provide: getMetricToken('Histogram', configuration.name), |
||||
|
useFactory: (registry: Registry) => { |
||||
|
const obj = new Histogram({ |
||||
|
...configuration, |
||||
|
registers: [registry], |
||||
|
}); |
||||
|
return obj; |
||||
|
}, |
||||
|
inject: [ |
||||
|
registryName === DEFAULT_PROM_REGISTRY |
||||
|
? DEFAULT_PROM_REGISTRY |
||||
|
: getRegistryName(registryName), |
||||
|
], |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
export function createPromSummaryProvider( |
||||
|
configuration: SummaryConfiguration, |
||||
|
registryName: string = DEFAULT_PROM_REGISTRY, |
||||
|
): Provider { |
||||
|
return { |
||||
|
provide: getMetricToken('Summary', configuration.name), |
||||
|
useFactory: (registry: Registry) => { |
||||
|
const obj = new Summary({ |
||||
|
...configuration, |
||||
|
registers: [registry], |
||||
|
}); |
||||
|
return obj; |
||||
|
}, |
||||
|
inject: [ |
||||
|
registryName === DEFAULT_PROM_REGISTRY |
||||
|
? DEFAULT_PROM_REGISTRY |
||||
|
: getRegistryName(registryName), |
||||
|
], |
||||
|
}; |
||||
|
} |
||||
@ -0,0 +1,32 @@ |
|||||
|
import { Injectable, NestMiddleware } from '@nestjs/common'; |
||||
|
import { Counter } from 'prom-client'; |
||||
|
import { Request, Response } from 'express'; |
||||
|
import { InjectCounterMetric } from '../common/prom.decorators'; |
||||
|
|
||||
|
@Injectable() |
||||
|
export class InboundMiddleware implements NestMiddleware { |
||||
|
constructor( |
||||
|
@InjectCounterMetric('http_requests_total') |
||||
|
private readonly _counter: Counter, |
||||
|
) {} |
||||
|
|
||||
|
use(req: Request, res: Response, next: () => void) { |
||||
|
const url = req.baseUrl; |
||||
|
const method = req.method; |
||||
|
|
||||
|
// ignore favicon
|
||||
|
if (url.endsWith('/favicon.ico')) { |
||||
|
next(); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// ignore metrics itself
|
||||
|
if (url.match(/\/metrics(\?.*?)?$/)) { |
||||
|
next(); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
this._counter.labels(method).inc(1, new Date()); |
||||
|
next(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,10 @@ |
|||||
|
import { createParamDecorator } from '@nestjs/common'; |
||||
|
import { Request } from 'express'; |
||||
|
import { VPagination } from './pagination.validation'; |
||||
|
import { validateSync } from '../validation/validation.util'; |
||||
|
|
||||
|
export const Pagination = createParamDecorator( |
||||
|
(data: any, req: Request): VPagination => { |
||||
|
return validateSync(VPagination, req.query); |
||||
|
}, |
||||
|
); |
||||
@ -0,0 +1,14 @@ |
|||||
|
import { IsNumber, Min } from 'class-validator'; |
||||
|
import { Type } from 'class-transformer'; |
||||
|
|
||||
|
export class VPagination { |
||||
|
@IsNumber() |
||||
|
@Min(0) |
||||
|
@Type(() => Number) |
||||
|
page: number = 0; |
||||
|
|
||||
|
@IsNumber() |
||||
|
@Min(1) |
||||
|
@Type(() => Number) |
||||
|
limit: number = 10; |
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
import { HttpException, HttpStatus } from '@nestjs/common'; |
||||
|
|
||||
|
export class ValidationException extends HttpException { |
||||
|
constructor(errors: string | object | any) { |
||||
|
super( |
||||
|
HttpException.createBody( |
||||
|
errors, |
||||
|
'VALIDATION_EXCEPTION', |
||||
|
HttpStatus.BAD_REQUEST, |
||||
|
), |
||||
|
HttpStatus.BAD_REQUEST, |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,59 @@ |
|||||
|
import { plainToClass } from 'class-transformer'; |
||||
|
import { sanitize } from 'class-sanitizer'; |
||||
|
import { |
||||
|
validate as classValidate, |
||||
|
validateSync as classValidateSync, |
||||
|
} from 'class-validator'; |
||||
|
import { ValidationException } from './validation.exception'; |
||||
|
import { ClassType } from 'class-transformer/ClassTransformer'; |
||||
|
|
||||
|
/** |
||||
|
* Validate value for validator |
||||
|
* @param {ClassType<T>} validation |
||||
|
* @param {object} value |
||||
|
* @return {Promise<T>} |
||||
|
*/ |
||||
|
export const validate = async <T>( |
||||
|
validation: ClassType<T>, |
||||
|
value: object, |
||||
|
): Promise<T> => { |
||||
|
// Transform to class
|
||||
|
const entity = plainToClass<T, object>(validation, value); |
||||
|
|
||||
|
// Sanitize
|
||||
|
sanitize(entity); |
||||
|
|
||||
|
// Validate
|
||||
|
const errors = await classValidate(entity, { |
||||
|
skipMissingProperties: true, |
||||
|
whitelist: true, |
||||
|
}); |
||||
|
if (errors.length > 0) { |
||||
|
throw new ValidationException(errors); |
||||
|
} |
||||
|
|
||||
|
return entity; |
||||
|
}; |
||||
|
|
||||
|
/** |
||||
|
* Validate value for validator without async validators |
||||
|
* @param {ClassType<T>} validation |
||||
|
* @param {object} value |
||||
|
* @return {T} |
||||
|
*/ |
||||
|
export const validateSync = <T>(validation: ClassType<T>, value: object): T => { |
||||
|
// Transform to class
|
||||
|
const entity = plainToClass<T, object>(validation, value); |
||||
|
|
||||
|
// Sanitize
|
||||
|
sanitize(entity); |
||||
|
|
||||
|
// Validate
|
||||
|
const errors = classValidateSync(entity, { |
||||
|
skipMissingProperties: true, |
||||
|
whitelist: true, |
||||
|
}); |
||||
|
if (errors.length > 0) throw new ValidationException(errors); |
||||
|
|
||||
|
return entity; |
||||
|
}; |
||||
@ -0,0 +1,24 @@ |
|||||
|
import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common'; |
||||
|
import { isNil } from '@nestjs/common/utils/shared.utils'; |
||||
|
import { validate } from './validation.util'; |
||||
|
|
||||
|
@Injectable() |
||||
|
export class ValidatorPipe implements PipeTransform<any> { |
||||
|
public async transform(value: any, metadata: ArgumentMetadata): Promise<any> { |
||||
|
const { metatype } = metadata; |
||||
|
if (!metatype || !this.toValidate(metadata)) { |
||||
|
return value; |
||||
|
} |
||||
|
|
||||
|
return await validate(metatype, value); |
||||
|
} |
||||
|
|
||||
|
private toValidate(metadata: ArgumentMetadata): boolean { |
||||
|
const { metatype, type } = metadata; |
||||
|
if (type === 'custom') { |
||||
|
return false; |
||||
|
} |
||||
|
const types = [String, Boolean, Number, Array, Object]; |
||||
|
return !types.find(typeIn => metatype === typeIn) && !isNil(metatype); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,2 @@ |
|||||
|
export const USER_ROLE = 'USER_ROLE'; |
||||
|
export const ADMIN_ROLE = 'ADMIN_ROLE'; |
||||
@ -0,0 +1,43 @@ |
|||||
|
import { |
||||
|
Controller, |
||||
|
Get, |
||||
|
NotFoundException, |
||||
|
Param, |
||||
|
ParseIntPipe, |
||||
|
UseGuards, |
||||
|
} from '@nestjs/common'; |
||||
|
import { Role } from './roles.entity'; |
||||
|
import { RolesService } from './roles.service'; |
||||
|
import { ApiBearerAuth, ApiResponse, ApiUseTags } from '@nestjs/swagger'; |
||||
|
import { AuthGuard } from '@nestjs/passport'; |
||||
|
|
||||
|
@ApiUseTags('Role') |
||||
|
@Controller() |
||||
|
@ApiBearerAuth() |
||||
|
export class RolesController { |
||||
|
constructor(private readonly rolesService: RolesService) {} |
||||
|
|
||||
|
@Get() |
||||
|
@ApiResponse({ |
||||
|
status: 200, |
||||
|
description: 'Get a list of all Role.', |
||||
|
type: Role, |
||||
|
isArray: true, |
||||
|
}) |
||||
|
getAll(): Promise<Role[]> { |
||||
|
return this.rolesService.getAll(); |
||||
|
} |
||||
|
|
||||
|
@Get(':id') |
||||
|
@ApiResponse({ |
||||
|
status: 200, |
||||
|
description: 'The Role with the matching id', |
||||
|
type: Role, |
||||
|
}) |
||||
|
@ApiResponse({ status: 404, description: 'Not found.' }) |
||||
|
async findOne(@Param('id', new ParseIntPipe()) id: number): Promise<Role> { |
||||
|
return (await this.rolesService.getOneById(id)).orElseThrow( |
||||
|
() => new NotFoundException(), |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,15 @@ |
|||||
|
import { DbAuditModel } from '../../utils/dbmodel.model'; |
||||
|
import { Column, Entity, ManyToOne, OneToMany } from 'typeorm'; |
||||
|
import { ApiModelProperty } from '@nestjs/swagger'; |
||||
|
|
||||
|
@Entity() |
||||
|
export class Role extends DbAuditModel { |
||||
|
constructor(name: string) { |
||||
|
super(); |
||||
|
this.name = name; |
||||
|
} |
||||
|
|
||||
|
@ApiModelProperty({ required: true, readOnly: true }) |
||||
|
@Column({ unique: true }) |
||||
|
name: string; |
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
import { Module } from '@nestjs/common'; |
||||
|
import { TypeOrmModule } from '@nestjs/typeorm'; |
||||
|
import { RolesController } from './roles.controller'; |
||||
|
import { RolesService } from './roles.service'; |
||||
|
import { Role } from './roles.entity'; |
||||
|
import { RolesRepository } from './roles.repository'; |
||||
|
|
||||
|
@Module({ |
||||
|
imports: [TypeOrmModule.forFeature([Role, RolesRepository])], |
||||
|
controllers: [RolesController], |
||||
|
providers: [RolesService], |
||||
|
exports: [RolesService], |
||||
|
}) |
||||
|
export class RolesModule {} |
||||
@ -0,0 +1,14 @@ |
|||||
|
import { EntityRepository, Repository } from 'typeorm'; |
||||
|
import { Role } from './roles.entity'; |
||||
|
import { Optional } from 'typescript-optional'; |
||||
|
|
||||
|
@EntityRepository(Role) |
||||
|
export class RolesRepository extends Repository<Role> { |
||||
|
async findRoleByName(name: string): Promise<Optional<Role>> { |
||||
|
return Optional.ofNullable(await this.findOne({ name })); |
||||
|
} |
||||
|
|
||||
|
async findOneById(id: number): Promise<Optional<Role>> { |
||||
|
return Optional.ofNullable(await this.findOne(id, {})); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,41 @@ |
|||||
|
import { Role } from './roles.entity'; |
||||
|
import { Injectable } from '@nestjs/common'; |
||||
|
import { InjectRepository } from '@nestjs/typeorm'; |
||||
|
import { RolesRepository } from './roles.repository'; |
||||
|
import { USER_ROLE, ADMIN_ROLE } from './roles.constants'; |
||||
|
import { Optional } from 'typescript-optional'; |
||||
|
|
||||
|
@Injectable() |
||||
|
export class RolesService { |
||||
|
constructor( |
||||
|
@InjectRepository(RolesRepository) |
||||
|
private readonly rolesRepository: RolesRepository, |
||||
|
) { |
||||
|
this.init(); |
||||
|
} |
||||
|
|
||||
|
async init(): Promise<void> { |
||||
|
for (const role of [USER_ROLE, ADMIN_ROLE]) { |
||||
|
if ((await this.rolesRepository.findRoleByName(role)).isEmpty()) { |
||||
|
const roleDb = new Role(role); |
||||
|
await this.rolesRepository.save(roleDb); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async getAll(): Promise<Role[]> { |
||||
|
return this.rolesRepository.find({}); |
||||
|
} |
||||
|
|
||||
|
async getOneById(id: number): Promise<Optional<Role>> { |
||||
|
return this.rolesRepository.findOneById(id); |
||||
|
} |
||||
|
|
||||
|
async getUserRole(): Promise<Role> { |
||||
|
return (await this.rolesRepository.findRoleByName(USER_ROLE)).get(); |
||||
|
} |
||||
|
|
||||
|
async getAdminRole(): Promise<Role> { |
||||
|
return (await this.rolesRepository.findRoleByName(ADMIN_ROLE)).get(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,132 @@ |
|||||
|
import { |
||||
|
Body, |
||||
|
Controller, |
||||
|
Delete, |
||||
|
ForbiddenException, |
||||
|
Get, |
||||
|
NotFoundException, |
||||
|
Param, |
||||
|
ParseIntPipe, |
||||
|
Post, |
||||
|
Put, |
||||
|
Query, |
||||
|
UseGuards, |
||||
|
} from '@nestjs/common'; |
||||
|
import { User } from './user.entity'; |
||||
|
import { UserService } from './user.service'; |
||||
|
import { |
||||
|
ApiBearerAuth, |
||||
|
ApiImplicitParam, |
||||
|
ApiResponse, |
||||
|
ApiUseTags, |
||||
|
} from '@nestjs/swagger'; |
||||
|
import { |
||||
|
UserDtoRegister, |
||||
|
UserDtoUpdateInfo, |
||||
|
UserDtoUpdatePassword, |
||||
|
} from './user.dto'; |
||||
|
import { AuthGuard } from '@nestjs/passport'; |
||||
|
import { Roles } from '../../decorators/roles.decorator'; |
||||
|
import { ADMIN_ROLE } from '../roles/roles.constants'; |
||||
|
import { CurrentUser } from '../../decorators/currentUser.decorator'; |
||||
|
|
||||
|
@ApiUseTags('User') |
||||
|
@Controller() |
||||
|
@ApiBearerAuth() |
||||
|
export class UserController { |
||||
|
constructor(private readonly userService: UserService) {} |
||||
|
|
||||
|
@Get() |
||||
|
@ApiResponse({ |
||||
|
status: 200, |
||||
|
description: 'Get a list of all User.', |
||||
|
type: User, |
||||
|
isArray: true, |
||||
|
}) |
||||
|
@Roles(ADMIN_ROLE) |
||||
|
getAll(): Promise<User[]> { |
||||
|
return this.userService.getAll(); |
||||
|
} |
||||
|
|
||||
|
@Post() |
||||
|
@ApiResponse({ |
||||
|
status: 201, |
||||
|
description: 'The User has been created.', |
||||
|
type: User, |
||||
|
}) |
||||
|
@Roles(ADMIN_ROLE) |
||||
|
saveNew(@Body() userDto: UserDtoRegister): Promise<User> { |
||||
|
return this.userService.saveNew(userDto); |
||||
|
} |
||||
|
|
||||
|
@Get(':id') |
||||
|
@ApiResponse({ |
||||
|
status: 200, |
||||
|
description: 'The User with the matching id', |
||||
|
type: User, |
||||
|
}) |
||||
|
@ApiResponse({ status: 404, description: 'Not found.' }) |
||||
|
async findOne( |
||||
|
@Param('id', new ParseIntPipe()) id: number, |
||||
|
@CurrentUser() user: User, |
||||
|
): Promise<User> { |
||||
|
if (!user.isAdmin() && user.id !== id) { |
||||
|
throw new ForbiddenException(); |
||||
|
} |
||||
|
return (await this.userService.getOneById(id)).orElseThrow( |
||||
|
() => new NotFoundException(), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
@Put(':id') |
||||
|
@ApiResponse({ |
||||
|
status: 200, |
||||
|
description: 'The updated User with the matching id', |
||||
|
type: User, |
||||
|
}) |
||||
|
@ApiResponse({ status: 404, description: 'Not found.' }) |
||||
|
async updateOne( |
||||
|
@Param('id', new ParseIntPipe()) id: number, |
||||
|
@Body() userDto: UserDtoUpdateInfo, |
||||
|
@CurrentUser() user: User, |
||||
|
): Promise<User> { |
||||
|
if (!user.isAdmin() && user.id !== id) { |
||||
|
throw new ForbiddenException(); |
||||
|
} |
||||
|
return this.userService.update(id, userDto); |
||||
|
} |
||||
|
|
||||
|
@Put(':id/password') |
||||
|
@ApiResponse({ |
||||
|
status: 200, |
||||
|
description: 'The updated User with the matching id', |
||||
|
type: User, |
||||
|
}) |
||||
|
@ApiResponse({ status: 404, description: 'Not found.' }) |
||||
|
async updateOnePassword( |
||||
|
@Param('id', new ParseIntPipe()) id: number, |
||||
|
@Body() userDto: UserDtoUpdatePassword, |
||||
|
@CurrentUser() user: User, |
||||
|
): Promise<User> { |
||||
|
if (!user.isAdmin() && user.id !== id) { |
||||
|
throw new ForbiddenException(); |
||||
|
} |
||||
|
return this.userService.updatePassword(id, userDto); |
||||
|
} |
||||
|
|
||||
|
@Delete(':id') |
||||
|
@ApiResponse({ |
||||
|
status: 200, |
||||
|
description: 'The User with the matching id was deleted', |
||||
|
}) |
||||
|
@ApiResponse({ status: 404, description: 'Not found.' }) |
||||
|
async deleteOne( |
||||
|
@Param('id', new ParseIntPipe()) id: number, |
||||
|
@CurrentUser() user: User, |
||||
|
): Promise<void> { |
||||
|
if (!user.isAdmin() && user.id !== id) { |
||||
|
throw new ForbiddenException(); |
||||
|
} |
||||
|
await this.userService.deleteById(id); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,54 @@ |
|||||
|
import { |
||||
|
IsArray, |
||||
|
IsEmail, |
||||
|
IsOptional, |
||||
|
IsString, |
||||
|
Min, |
||||
|
MinLength, |
||||
|
} from 'class-validator'; |
||||
|
import { Type } from 'class-transformer'; |
||||
|
import { ApiModelProperty } from '@nestjs/swagger'; |
||||
|
|
||||
|
export class UserDtoRegister { |
||||
|
@IsEmail() |
||||
|
@ApiModelProperty({ example: 'foo@bar.fr' }) |
||||
|
email: string; |
||||
|
|
||||
|
@IsString() |
||||
|
@ApiModelProperty({ example: 'foo' }) |
||||
|
firstName: string; |
||||
|
|
||||
|
@IsString() |
||||
|
@ApiModelProperty({ example: 'bar' }) |
||||
|
lastName: string; |
||||
|
|
||||
|
@IsString() |
||||
|
@MinLength(6) |
||||
|
@ApiModelProperty({ example: 'azerty', minLength: 6 }) |
||||
|
password: string; |
||||
|
} |
||||
|
|
||||
|
export class UserDtoUpdateInfo { |
||||
|
@IsString() |
||||
|
firstName: string; |
||||
|
|
||||
|
@IsString() |
||||
|
lastName: string; |
||||
|
} |
||||
|
|
||||
|
export class UserDtoUpdatePassword { |
||||
|
@IsString() |
||||
|
@MinLength(6) |
||||
|
@ApiModelProperty({ example: 'azerty', minLength: 6 }) |
||||
|
oldPassword: string; |
||||
|
|
||||
|
@IsString() |
||||
|
@MinLength(6) |
||||
|
@ApiModelProperty({ example: 'azerty', minLength: 6 }) |
||||
|
newPassword: string; |
||||
|
|
||||
|
@IsString() |
||||
|
@MinLength(6) |
||||
|
@ApiModelProperty({ example: 'azerty', minLength: 6 }) |
||||
|
newPasswordBis: string; |
||||
|
} |
||||
@ -0,0 +1,33 @@ |
|||||
|
import { DbAuditModel } from '../../utils/dbmodel.model'; |
||||
|
import { Column, Entity, JoinTable, ManyToMany } from 'typeorm'; |
||||
|
import { ApiModelProperty } from '@nestjs/swagger'; |
||||
|
import { Exclude } from 'class-transformer'; |
||||
|
import { Role } from '../roles/roles.entity'; |
||||
|
import { ADMIN_ROLE } from '../roles/roles.constants'; |
||||
|
|
||||
|
@Entity() |
||||
|
export class User extends DbAuditModel { |
||||
|
@Column() |
||||
|
@ApiModelProperty({ example: 'foo@bar.fr' }) |
||||
|
email: string; |
||||
|
|
||||
|
@Column() |
||||
|
@ApiModelProperty() |
||||
|
firstName: string; |
||||
|
|
||||
|
@Column() |
||||
|
@ApiModelProperty() |
||||
|
lastName: string; |
||||
|
|
||||
|
@Column() |
||||
|
@Exclude() |
||||
|
password: string; |
||||
|
|
||||
|
@ManyToMany(type => Role) |
||||
|
@JoinTable() |
||||
|
roles: Role[]; |
||||
|
|
||||
|
isAdmin(): boolean { |
||||
|
return !!(this.roles || []).find(role => role.name === ADMIN_ROLE); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
import { Module } from '@nestjs/common'; |
||||
|
import { TypeOrmModule } from '@nestjs/typeorm'; |
||||
|
import { UserController } from './user.controller'; |
||||
|
import { UserService } from './user.service'; |
||||
|
import { User } from './user.entity'; |
||||
|
import { UserRepository } from './user.repository'; |
||||
|
import { CryptoModule } from '../core/crypto/crypto.module'; |
||||
|
import { RolesModule } from '../roles/roles.module'; |
||||
|
|
||||
|
@Module({ |
||||
|
imports: [ |
||||
|
TypeOrmModule.forFeature([User, UserRepository]), |
||||
|
CryptoModule, |
||||
|
RolesModule, |
||||
|
], |
||||
|
controllers: [UserController], |
||||
|
providers: [UserService], |
||||
|
exports: [UserService], |
||||
|
}) |
||||
|
export class UserModule {} |
||||
@ -0,0 +1,22 @@ |
|||||
|
import { EntityRepository, Repository } from 'typeorm'; |
||||
|
import { User } from './user.entity'; |
||||
|
import { Optional } from 'typescript-optional'; |
||||
|
|
||||
|
@EntityRepository(User) |
||||
|
export class UserRepository extends Repository<User> { |
||||
|
async findOneById(id: number): Promise<Optional<User>> { |
||||
|
return Optional.ofNullable( |
||||
|
await this.findOne(id, { relations: ['roles'] }), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
async hasUserWithMatchingEmail(email: string): Promise<boolean> { |
||||
|
return (await this.count({ where: { email } })) === 1; |
||||
|
} |
||||
|
|
||||
|
async findOneWithEmail(email: string): Promise<Optional<User>> { |
||||
|
return Optional.ofNullable( |
||||
|
await this.findOne({ email: email.toLowerCase() }), |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,112 @@ |
|||||
|
import { User } from './user.entity'; |
||||
|
import { |
||||
|
BadRequestException, |
||||
|
ConflictException, |
||||
|
Inject, |
||||
|
Injectable, |
||||
|
NotFoundException, |
||||
|
} from '@nestjs/common'; |
||||
|
import { InjectRepository } from '@nestjs/typeorm'; |
||||
|
import { UserRepository } from './user.repository'; |
||||
|
// import { } from './user.constants';
|
||||
|
import { |
||||
|
UserDtoRegister, |
||||
|
UserDtoUpdateInfo, |
||||
|
UserDtoUpdatePassword, |
||||
|
} from './user.dto'; |
||||
|
import { Optional } from 'typescript-optional'; |
||||
|
import { CryptoService } from '../core/crypto/crypto.service'; |
||||
|
import { Role } from '../roles/roles.entity'; |
||||
|
import { Roles } from '../../decorators/roles.decorator'; |
||||
|
import { USER_ROLE } from '../roles/roles.constants'; |
||||
|
import { RolesService } from '../roles/roles.service'; |
||||
|
|
||||
|
@Injectable() |
||||
|
export class UserService { |
||||
|
constructor( |
||||
|
@InjectRepository(UserRepository) |
||||
|
private readonly userRepository: UserRepository, |
||||
|
private readonly cryptoService: CryptoService, |
||||
|
private readonly rolesSercie: RolesService, |
||||
|
) {} |
||||
|
|
||||
|
async getAll(): Promise<User[]> { |
||||
|
return this.userRepository.find({}); |
||||
|
} |
||||
|
|
||||
|
async getOneById(id: number): Promise<Optional<User>> { |
||||
|
return this.userRepository.findOneById(id); |
||||
|
} |
||||
|
|
||||
|
async getOnWithEmail(email: string): Promise<Optional<User>> { |
||||
|
return await this.userRepository.findOneWithEmail(email); |
||||
|
} |
||||
|
|
||||
|
async doPasswordMatch(user: User, password: string): Promise<boolean> { |
||||
|
return this.cryptoService.compare(password, user.password); |
||||
|
} |
||||
|
|
||||
|
async saveNew(userRegister: UserDtoRegister): Promise<User> { |
||||
|
if ( |
||||
|
await this.userRepository.hasUserWithMatchingEmail( |
||||
|
userRegister.email.toLowerCase(), |
||||
|
) |
||||
|
) { |
||||
|
throw new ConflictException('Email already taken'); |
||||
|
} |
||||
|
|
||||
|
let userNew = new User(); |
||||
|
|
||||
|
const userRole = await this.rolesSercie.getUserRole(); |
||||
|
|
||||
|
userNew.password = await this.cryptoService.hash(userRegister.password); |
||||
|
userNew.email = userRegister.email.toLowerCase(); |
||||
|
userNew.lastName = userRegister.lastName; |
||||
|
userNew.firstName = userRegister.firstName; |
||||
|
userNew.roles = [userRole]; |
||||
|
|
||||
|
userNew = await this.userRepository.save(userNew); |
||||
|
|
||||
|
return userNew; |
||||
|
} |
||||
|
|
||||
|
async update(id: number, body: UserDtoUpdateInfo): Promise<User> { |
||||
|
let userFound = (await this.userRepository.findOneById(id)).orElseThrow( |
||||
|
() => new NotFoundException(), |
||||
|
); |
||||
|
|
||||
|
userFound.firstName = body.firstName; |
||||
|
userFound.lastName = body.lastName; |
||||
|
|
||||
|
userFound = await this.userRepository.save(userFound); |
||||
|
|
||||
|
return userFound; |
||||
|
} |
||||
|
|
||||
|
async updatePassword(id: number, body: UserDtoUpdatePassword): Promise<User> { |
||||
|
let userFound = (await this.userRepository.findOneById(id)).orElseThrow( |
||||
|
() => new NotFoundException(), |
||||
|
); |
||||
|
|
||||
|
if (!this.doPasswordMatch(userFound, body.oldPassword)) { |
||||
|
throw new BadRequestException('Old password do not match'); |
||||
|
} |
||||
|
|
||||
|
if (body.newPassword !== body.newPasswordBis) { |
||||
|
throw new BadRequestException('New passwords are not the same'); |
||||
|
} |
||||
|
|
||||
|
userFound.password = await this.cryptoService.hash(body.newPassword); |
||||
|
|
||||
|
userFound = await this.userRepository.save(userFound); |
||||
|
|
||||
|
return userFound; |
||||
|
} |
||||
|
|
||||
|
async deleteById(id: number): Promise<void> { |
||||
|
const userFound = (await this.userRepository.findOneById(id)).orElseThrow( |
||||
|
() => new NotFoundException(), |
||||
|
); |
||||
|
await this.userRepository.remove(userFound); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
import { ApiModelProperty } from '@nestjs/swagger'; |
||||
|
import { |
||||
|
CreateDateColumn, |
||||
|
PrimaryGeneratedColumn, |
||||
|
UpdateDateColumn, |
||||
|
VersionColumn, |
||||
|
} from 'typeorm'; |
||||
|
import { Exclude } from 'class-transformer'; |
||||
|
|
||||
|
/** |
||||
|
* This model helps to have audits metrics to db models |
||||
|
*/ |
||||
|
export abstract class DbAuditModel { |
||||
|
@ApiModelProperty() |
||||
|
@PrimaryGeneratedColumn() |
||||
|
id: number; |
||||
|
|
||||
|
@CreateDateColumn() |
||||
|
@Exclude() |
||||
|
creationDate: Date; |
||||
|
|
||||
|
@UpdateDateColumn() |
||||
|
@Exclude() |
||||
|
updateDate: Date; |
||||
|
|
||||
|
@VersionColumn() |
||||
|
@Exclude() |
||||
|
version: number; |
||||
|
} |
||||
@ -0,0 +1,28 @@ |
|||||
|
import { INestApplication } from '@nestjs/common'; |
||||
|
import { Test } from '@nestjs/testing'; |
||||
|
import * as request from 'supertest'; |
||||
|
import { AppModule } from './../src/app.module'; |
||||
|
|
||||
|
describe('AppController (e2e)', () => { |
||||
|
let app: INestApplication; |
||||
|
|
||||
|
beforeAll(async () => { |
||||
|
const moduleFixture = await Test.createTestingModule({ |
||||
|
imports: [AppModule], |
||||
|
}).compile(); |
||||
|
|
||||
|
app = moduleFixture.createNestApplication(); |
||||
|
await app.init(); |
||||
|
}); |
||||
|
|
||||
|
it('/ping (GET)', () => { |
||||
|
return request(app.getHttpServer()) |
||||
|
.get('/ping') |
||||
|
.expect(200) |
||||
|
.expect('pong'); |
||||
|
}); |
||||
|
|
||||
|
afterAll(async () => { |
||||
|
await app.close(); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,9 @@ |
|||||
|
{ |
||||
|
"moduleFileExtensions": ["js", "json", "ts"], |
||||
|
"rootDir": ".", |
||||
|
"testEnvironment": "node", |
||||
|
"testRegex": ".e2e-spec.ts$", |
||||
|
"transform": { |
||||
|
"^.+\\.(t|j)s$": "ts-jest" |
||||
|
} |
||||
|
} |
||||
Some files were not shown because too many files changed in this diff
Write
Preview
Loading…
Cancel
Save
Reference in new issue