Browse Source

Initial commit

pull/1/head
hallouma875 6 years ago
commit
f5d0deb73a
  1. 7
      .editorconfig
  2. 76
      .github/CODE_OF_CONDUCT.md
  3. 49
      .github/CONTRIBUTING.md
  4. 243
      .gitignore
  5. 4
      .prettierrc
  6. 21
      LICENSE.md
  7. 69
      README.md
  8. 5
      _templates/generator/help/index.ejs
  9. 18
      _templates/generator/new/hello.ejs
  10. 18
      _templates/generator/with-prompt/hello.ejs
  11. 14
      _templates/generator/with-prompt/prompt.ejs
  12. 17
      _templates/module/empty-module/module.ts.ejs
  13. 10
      _templates/module/empty-module/prompt.js
  14. 6
      _templates/module/help/index.ejs
  15. 3
      _templates/module/with-crud/constants.ts.ejs
  16. 3
      _templates/module/with-crud/controller.spec.ts.ejs
  17. 96
      _templates/module/with-crud/controller.ts.ejs
  18. 15
      _templates/module/with-crud/dto.ts.ejs
  19. 16
      _templates/module/with-crud/entity.ts.ejs
  20. 24
      _templates/module/with-crud/module.ts.ejs
  21. 10
      _templates/module/with-crud/prompt.js
  22. 19
      _templates/module/with-crud/repository.ts.ejs
  23. 13
      _templates/module/with-crud/routing-body.ts.ejs
  24. 10
      _templates/module/with-crud/routing-head.ts.ejs
  25. 3
      _templates/module/with-crud/service.spec.ts.ejs
  26. 62
      _templates/module/with-crud/service.ts.ejs
  27. 18
      _templates/module/with-routing/module.ts.ejs
  28. 10
      _templates/module/with-routing/prompt.js
  29. 13
      _templates/module/with-routing/routing-body.ts.ejs
  30. 10
      _templates/module/with-routing/routing-head.ts.ejs
  31. 6
      compodoc.server.json
  32. 9
      docker/postgresql.yml
  33. 14
      exemple.env
  34. 5
      nest-cli.json
  35. 122
      package.json
  36. 21
      src/app.controller.spec.ts
  37. 13
      src/app.controller.ts
  38. 58
      src/app.module.ts
  39. 19
      src/app.routes.ts
  40. 8
      src/decorators/currentUser.decorator.ts
  41. 3
      src/decorators/public.decorator.ts
  42. 3
      src/decorators/roles.decorator.ts
  43. 0
      src/exceptions/.gitkeep
  44. 0
      src/filters/.gitkeep
  45. 24
      src/guards/roles.guard.ts
  46. 0
      src/interceptors/.gitkeep
  47. 0
      src/interfaces/.gitkeep
  48. 71
      src/main.ts
  49. 0
      src/middlewares/.gitkeep
  50. 40
      src/modules/core/auth/auth.controller.ts
  51. 17
      src/modules/core/auth/auth.dto.ts
  52. 29
      src/modules/core/auth/auth.guard.ts
  53. 33
      src/modules/core/auth/auth.module.ts
  54. 32
      src/modules/core/auth/auth.service.ts
  55. 22
      src/modules/core/auth/jwt.strategy.ts
  56. 9
      src/modules/core/config/config.module.ts
  57. 118
      src/modules/core/config/config.service.spec.ts
  58. 75
      src/modules/core/config/config.service.ts
  59. 8
      src/modules/core/crypto/crypto.module.ts
  60. 32
      src/modules/core/crypto/crypto.service.spec.ts
  61. 28
      src/modules/core/crypto/crypto.service.ts
  62. 43
      src/modules/core/logger/logger-exception.interceptor.ts
  63. 9
      src/modules/core/logger/logger.constants.ts
  64. 11
      src/modules/core/logger/logger.module.ts
  65. 33
      src/modules/core/logger/logger.providers.ts
  66. 21
      src/modules/core/logger/logger.service.spec.ts
  67. 46
      src/modules/core/logger/logger.service.ts
  68. 11
      src/modules/core/metrics/common/prom.decorators.ts
  69. 11
      src/modules/core/metrics/common/prom.utils.ts
  70. 66
      src/modules/core/metrics/interceptors/metrics.interceptor.ts
  71. 33
      src/modules/core/metrics/interfaces/metric.type.ts
  72. 27
      src/modules/core/metrics/interfaces/prom-options.interface.ts
  73. 90
      src/modules/core/metrics/metrics-core.module.ts
  74. 3
      src/modules/core/metrics/metrics.constants.ts
  75. 15
      src/modules/core/metrics/metrics.constroller.ts
  76. 147
      src/modules/core/metrics/metrics.module.ts
  77. 98
      src/modules/core/metrics/metrics.providers.ts
  78. 32
      src/modules/core/metrics/middleware/inbound.middleware.ts
  79. 10
      src/modules/core/pagination/pagination.decorator.ts
  80. 14
      src/modules/core/pagination/pagination.validation.ts
  81. 14
      src/modules/core/validation/validation.exception.ts
  82. 59
      src/modules/core/validation/validation.util.ts
  83. 24
      src/modules/core/validation/validator.pipe.ts
  84. 2
      src/modules/roles/roles.constants.ts
  85. 43
      src/modules/roles/roles.controller.ts
  86. 15
      src/modules/roles/roles.entity.ts
  87. 14
      src/modules/roles/roles.module.ts
  88. 14
      src/modules/roles/roles.repository.ts
  89. 41
      src/modules/roles/roles.service.ts
  90. 0
      src/modules/user/user.constants.ts
  91. 132
      src/modules/user/user.controller.ts
  92. 54
      src/modules/user/user.dto.ts
  93. 33
      src/modules/user/user.entity.ts
  94. 20
      src/modules/user/user.module.ts
  95. 22
      src/modules/user/user.repository.ts
  96. 112
      src/modules/user/user.service.ts
  97. 0
      src/pipes/.gitkeep
  98. 29
      src/utils/dbmodel.model.ts
  99. 28
      test/app.e2e-spec.ts
  100. 9
      test/jest-e2e.json

7
.editorconfig

@ -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

76
.github/CODE_OF_CONDUCT.md

@ -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

49
.github/CONTRIBUTING.md

@ -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].

243
.gitignore

@ -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

4
.prettierrc

@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}

21
LICENSE.md

@ -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.

69
README.md

@ -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)_

5
_templates/generator/help/index.ejs

@ -0,0 +1,5 @@
---
message: |
hygen {bold generator new} --name [NAME] --action [ACTION]
hygen {bold generator with-prompt} --name [NAME] --action [ACTION]
---

18
_templates/generator/new/hello.ejs

@ -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)

18
_templates/generator/with-prompt/hello.ejs

@ -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)

14
_templates/generator/with-prompt/prompt.ejs

@ -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?"
}
]

17
_templates/module/empty-module/module.ts.ejs

@ -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 {}

10
_templates/module/empty-module/prompt.js

@ -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?',
},
];

6
_templates/module/help/index.ejs

@ -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]
---

3
_templates/module/with-crud/constants.ts.ejs

@ -0,0 +1,3 @@
---
to: src/modules/<%=h.changeCase.param(name)%>/<%=h.changeCase.param(name)%>.constants.ts
---

3
_templates/module/with-crud/controller.spec.ts.ejs

@ -0,0 +1,3 @@
---
to: src/modules/<%=h.changeCase.param(name)%>/<%=h.changeCase.param(name)%>.controller.spec.ts
---

96
_templates/module/with-crud/controller.ts.ejs

@ -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);
}
}

15
_templates/module/with-crud/dto.ts.ejs

@ -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 {
}

16
_templates/module/with-crud/entity.ts.ejs

@ -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 {
}

24
_templates/module/with-crud/module.ts.ejs

@ -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 {}

10
_templates/module/with-crud/prompt.js

@ -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?',
},
];

19
_templates/module/with-crud/repository.ts.ejs

@ -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, {}),
);
}
}

13
_templates/module/with-crud/routing-body.ts.ejs

@ -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,
},

10
_templates/module/with-crud/routing-head.ts.ejs

@ -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';

3
_templates/module/with-crud/service.spec.ts.ejs

@ -0,0 +1,3 @@
---
to: src/modules/<%=h.changeCase.param(name)%>/<%=h.changeCase.param(name)%>.service.spec.ts
---

62
_templates/module/with-crud/service.ts.ejs

@ -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);
}
}

18
_templates/module/with-routing/module.ts.ejs

@ -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 {}

10
_templates/module/with-routing/prompt.js

@ -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?',
},
];

13
_templates/module/with-routing/routing-body.ts.ejs

@ -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,
},

10
_templates/module/with-routing/routing-head.ts.ejs

@ -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';

6
compodoc.server.json

@ -0,0 +1,6 @@
{
"port": 9911,
"name": "TP_CI SERVER",
"output": "doc-server",
"tsconfig": "./tsconfig.json"
}

9
docker/postgresql.yml

@ -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

14
exemple.env

@ -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

5
nest-cli.json

@ -0,0 +1,5 @@
{
"language": "ts",
"collection": "@nestjs/schematics",
"sourceRoot": "src"
}

122
package.json

@ -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}"
}
}
}

21
src/app.controller.spec.ts

@ -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');
});
});
});

13
src/app.controller.ts

@ -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';
}
}

58
src/app.module.ts

@ -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('*');
}
}

19
src/app.routes.ts

@ -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,
},
];

8
src/decorators/currentUser.decorator.ts

@ -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;
},
);

3
src/decorators/public.decorator.ts

@ -0,0 +1,3 @@
import { SetMetadata } from '@nestjs/common';
export const Public = () => SetMetadata('isPublic', true);

3
src/decorators/roles.decorator.ts

@ -0,0 +1,3 @@
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

0
src/exceptions/.gitkeep

0
src/filters/.gitkeep

24
src/guards/roles.guard.ts

@ -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
src/interceptors/.gitkeep

0
src/interfaces/.gitkeep

71
src/main.ts

@ -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
src/middlewares/.gitkeep

40
src/modules/core/auth/auth.controller.ts

@ -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;
}
}

17
src/modules/core/auth/auth.dto.ts

@ -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;
}

29
src/modules/core/auth/auth.guard.ts

@ -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);
}
}

33
src/modules/core/auth/auth.module.ts

@ -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 {}

32
src/modules/core/auth/auth.service.ts

@ -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(),
);
}
}

22
src/modules/core/auth/jwt.strategy.ts

@ -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);
}
}

9
src/modules/core/config/config.module.ts

@ -0,0 +1,9 @@
import { Global, Module } from '@nestjs/common';
import { ConfigService } from './config.service';
@Global()
@Module({
providers: [ConfigService],
exports: [ConfigService],
})
export class ConfigModule {}

118
src/modules/core/config/config.service.spec.ts

@ -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);
});
});

75
src/modules/core/config/config.service.ts

@ -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;
}
}

8
src/modules/core/crypto/crypto.module.ts

@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { CryptoService } from './crypto.service';
@Module({
providers: [CryptoService],
exports: [CryptoService],
})
export class CryptoModule {}

32
src/modules/core/crypto/crypto.service.spec.ts

@ -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();
});
});

28
src/modules/core/crypto/crypto.service.ts

@ -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 });
}
}

43
src/modules/core/logger/logger-exception.interceptor.ts

@ -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;
}),
);
}
}

9
src/modules/core/logger/logger.constants.ts

@ -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',
}

11
src/modules/core/logger/logger.module.ts

@ -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 {}

33
src/modules/core/logger/logger.providers.ts

@ -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,
});
},
},
];

21
src/modules/core/logger/logger.service.spec.ts

@ -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();
});
});

46
src/modules/core/logger/logger.service.ts

@ -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);
}
}

11
src/modules/core/metrics/common/prom.decorators.ts

@ -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));

11
src/modules/core/metrics/common/prom.utils.ts

@ -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`;
}

66
src/modules/core/metrics/interceptors/metrics.interceptor.ts

@ -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);
}
}

33
src/modules/core/metrics/interfaces/metric.type.ts

@ -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;
}

27
src/modules/core/metrics/interfaces/prom-options.interface.ts

@ -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;
};
}

90
src/modules/core/metrics/metrics-core.module.ts

@ -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() {}
}

3
src/modules/core/metrics/metrics.constants.ts

@ -0,0 +1,3 @@
export const DEFAULT_PROM_REGISTRY = 'PromRegistry';
export const DEFAULT_PROM_OPTIONS = 'PromOptions';
export const PROM_REGISTRY_NAME = 'PromRegistryName';

15
src/modules/core/metrics/metrics.constroller.ts

@ -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());
}
}

147
src/modules/core/metrics/metrics.module.ts

@ -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],
};
}
}

98
src/modules/core/metrics/metrics.providers.ts

@ -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),
],
};
}

32
src/modules/core/metrics/middleware/inbound.middleware.ts

@ -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();
}
}

10
src/modules/core/pagination/pagination.decorator.ts

@ -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);
},
);

14
src/modules/core/pagination/pagination.validation.ts

@ -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;
}

14
src/modules/core/validation/validation.exception.ts

@ -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,
);
}
}

59
src/modules/core/validation/validation.util.ts

@ -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;
};

24
src/modules/core/validation/validator.pipe.ts

@ -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);
}
}

2
src/modules/roles/roles.constants.ts

@ -0,0 +1,2 @@
export const USER_ROLE = 'USER_ROLE';
export const ADMIN_ROLE = 'ADMIN_ROLE';

43
src/modules/roles/roles.controller.ts

@ -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(),
);
}
}

15
src/modules/roles/roles.entity.ts

@ -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;
}

14
src/modules/roles/roles.module.ts

@ -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 {}

14
src/modules/roles/roles.repository.ts

@ -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, {}));
}
}

41
src/modules/roles/roles.service.ts

@ -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
src/modules/user/user.constants.ts

132
src/modules/user/user.controller.ts

@ -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);
}
}

54
src/modules/user/user.dto.ts

@ -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;
}

33
src/modules/user/user.entity.ts

@ -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);
}
}

20
src/modules/user/user.module.ts

@ -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 {}

22
src/modules/user/user.repository.ts

@ -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() }),
);
}
}

112
src/modules/user/user.service.ts

@ -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
src/pipes/.gitkeep

29
src/utils/dbmodel.model.ts

@ -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;
}

28
test/app.e2e-spec.ts

@ -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();
});
});

9
test/jest-e2e.json

@ -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

Loading…
Cancel
Save