commit
4404b47ec8
84 changed files with 12230 additions and 0 deletions
-
175.circleci/config.yml
-
7.editorconfig
-
243.gitignore
-
4.prettierrc
-
10Dockerfile
-
29README.md
-
6compodoc.server.json
-
11docker/postgresql.yml
-
13exemple.env
-
24internals/generator/index.js
-
0internals/generator/module/constants.ts.hbs
-
0internals/generator/module/controller.spec.ts.hbs
-
89internals/generator/module/controller.ts.hbs
-
7internals/generator/module/dto.ts.hbs
-
9internals/generator/module/entity.ts.hbs
-
203internals/generator/module/index.js
-
16internals/generator/module/module.ts.hbs
-
12internals/generator/module/repository.ts.hbs
-
5internals/generator/module/routing.ts.hbs
-
0internals/generator/module/service.spec.ts.hbs
-
54internals/generator/module/service.ts.hbs
-
20internals/generator/utils/moduleExists.js
-
5nest-cli.json
-
6nodemon-debug.json
-
6nodemon.json
-
109package.json
-
22src/app.controller.spec.ts
-
12src/app.controller.ts
-
43src/app.module.ts
-
22src/app.routes.ts
-
8src/app.service.ts
-
8src/decorators/currentUser.decorator.ts
-
3src/decorators/roles.decorator.ts
-
0src/exceptions/.gitkeep
-
0src/filters/.gitkeep
-
22src/guards/roles.guard.ts
-
14src/interceptors/transform.interceptor.ts
-
0src/interfaces/.gitkeep
-
62src/main.ts
-
0src/middlewares/.gitkeep
-
39src/modules/core/auth/auth.controller.ts
-
17src/modules/core/auth/auth.dto.ts
-
33src/modules/core/auth/auth.module.ts
-
30src/modules/core/auth/auth.service.ts
-
22src/modules/core/auth/jwt-auth.guard.ts
-
26src/modules/core/auth/jwt.strategy.ts
-
9src/modules/core/config/config.module.ts
-
75src/modules/core/config/config.service.ts
-
8src/modules/core/crypto/crypto.module.ts
-
15src/modules/core/crypto/crypto.service.spec.ts
-
28src/modules/core/crypto/crypto.service.ts
-
46src/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
-
18src/modules/core/logger/logger.service.spec.ts
-
46src/modules/core/logger/logger.service.ts
-
10src/modules/core/pagination/pagination.decorator.ts
-
14src/modules/core/pagination/pagination.validation.ts
-
15src/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
-
52src/modules/roles/roles.controller.ts
-
5src/modules/roles/roles.dto.ts
-
10src/modules/roles/roles.entity.ts
-
14src/modules/roles/roles.module.ts
-
15src/modules/roles/roles.repository.ts
-
64src/modules/roles/roles.service.ts
-
0src/modules/user/user.constants.ts
-
111src/modules/user/user.controller.ts
-
54src/modules/user/user.dto.ts
-
22src/modules/user/user.entity.ts
-
15src/modules/user/user.module.ts
-
20src/modules/user/user.repository.ts
-
104src/modules/user/user.service.ts
-
27src/utils/dbmodel.model.ts
-
28test/app.e2e-spec.ts
-
9test/jest-e2e.json
-
19tsconfig.json
-
7tsconfig.spec.json
-
29tslint.json
-
34webpack.config.js
-
9723yarn.lock
@ -0,0 +1,175 @@ |
|||||
|
version: 2 |
||||
|
jobs: |
||||
|
build: |
||||
|
docker: |
||||
|
- image: circleci/node:dubnium-browsers |
||||
|
working_directory: ~/repo |
||||
|
steps: |
||||
|
- checkout |
||||
|
- restore_cache: |
||||
|
keys: |
||||
|
- dependencies-{{ checksum "package.json" }}-{{ .Environment.CACHE_VERSION }} |
||||
|
# fallback to using the latest cache if no exact match is found |
||||
|
- dependencies- |
||||
|
- run: yarn global add node-gyp && yarn install |
||||
|
- save_cache: |
||||
|
paths: |
||||
|
- node_modules |
||||
|
key: dependencies-{{ checksum "package.json" }}-{{ .Environment.CACHE_VERSION }} |
||||
|
lint: |
||||
|
docker: |
||||
|
- image: circleci/node:dubnium-browsers |
||||
|
working_directory: ~/repo |
||||
|
steps: |
||||
|
- checkout |
||||
|
- restore_cache: |
||||
|
key: dependencies-{{ checksum "package.json" }}-{{ .Environment.CACHE_VERSION }} |
||||
|
- run: yarn lint |
||||
|
- run: yarn format:check |
||||
|
server-doc-build: |
||||
|
docker: |
||||
|
- image: circleci/node:dubnium-browsers |
||||
|
working_directory: ~/repo |
||||
|
steps: |
||||
|
- checkout |
||||
|
- restore_cache: |
||||
|
key: dependencies-{{ checksum "package.json" }}-{{ .Environment.CACHE_VERSION }} |
||||
|
- run: yarn doc:build |
||||
|
- run: |
||||
|
name: Copy deployment artifacts to workspace |
||||
|
command: | |
||||
|
cp doc-server/ /tmp/server-doc -r |
||||
|
- store_artifacts: |
||||
|
path: /tmp/server-doc |
||||
|
- persist_to_workspace: |
||||
|
root: /tmp |
||||
|
paths: |
||||
|
- server-doc |
||||
|
server-doc-deploy: |
||||
|
docker: |
||||
|
- image: circleci/node:dubnium-browsers |
||||
|
working_directory: ~/deploy-doc-server |
||||
|
steps: |
||||
|
- attach_workspace: |
||||
|
at: /tmp |
||||
|
- run: |
||||
|
name: Deploy app |
||||
|
command: echo todo && false |
||||
|
back-test-unit: |
||||
|
docker: |
||||
|
- image: circleci/node:dubnium-browsers |
||||
|
working_directory: ~/repo |
||||
|
steps: |
||||
|
- checkout |
||||
|
- restore_cache: |
||||
|
key: dependencies-{{ checksum "package.json" }}-{{ .Environment.CACHE_VERSION }} |
||||
|
- run: |
||||
|
command: yarn test:ci |
||||
|
environment: |
||||
|
DATABASE_URL: postgres://psqluer:psqlpassword@localhost:5432/psqluer |
||||
|
JWT_SECRET: aaaa |
||||
|
API_PORT: 3000 |
||||
|
API_HOST: localhost |
||||
|
API_PROTOCOL: http |
||||
|
- run: yarn add codecov && yarn codecov |
||||
|
back-test-e2e: |
||||
|
docker: |
||||
|
- image: circleci/node:dubnium-browsers |
||||
|
- image: circleci/postgres:9.6.5 |
||||
|
environment: |
||||
|
POSTGRES_DB: psqluer |
||||
|
POSTGRES_USER: psqluer |
||||
|
POSTGRES_PASSWORD: psqlpassword |
||||
|
working_directory: ~/repo |
||||
|
steps: |
||||
|
- checkout |
||||
|
- restore_cache: |
||||
|
key: dependencies-{{ checksum "package.json" }}-{{ .Environment.CACHE_VERSION }} |
||||
|
- run: |
||||
|
command: yarn test:e2e |
||||
|
environment: |
||||
|
DATABASE_URL: postgres://psqluer:psqlpassword@localhost:5432/psqluer |
||||
|
JWT_SECRET: aaaa |
||||
|
API_PORT: 3000 |
||||
|
API_HOST: localhost |
||||
|
API_PROTOCOL: http |
||||
|
|
||||
|
docker-build-and-push: |
||||
|
working_directory: /dockerapp |
||||
|
docker: |
||||
|
- image: docker:17.05.0-ce-git |
||||
|
steps: |
||||
|
- checkout |
||||
|
- setup_remote_docker |
||||
|
- run: |
||||
|
name: Build application Docker image |
||||
|
command: | |
||||
|
docker build --cache-from=app -t app . |
||||
|
- deploy: |
||||
|
name: Publish application to docker hub |
||||
|
command: | |
||||
|
docker login -e $DOCKER_HUB_EMAIL -u $DOCKER_HUB_USER_ID -p $DOCKER_HUB_PWD |
||||
|
docker tag app $DOCKER_HUB_USER_ID/my-awesome-ci-expr:$CIRCLE_BUILD_NUM |
||||
|
docker tag app $DOCKER_HUB_USER_ID/my-awesome-ci-expr:latest |
||||
|
docker push $DOCKER_HUB_USER_ID/my-awesome-ci-expr:$CIRCLE_BUILD_NUM |
||||
|
docker push $DOCKER_HUB_USER_ID/my-awesome-ci-expr:latest |
||||
|
|
||||
|
back-deploy-heroku: |
||||
|
docker: |
||||
|
- image: buildpack-deps:trusty |
||||
|
steps: |
||||
|
- checkout |
||||
|
- run: |
||||
|
name: Heroku Deploy |
||||
|
command: echo todo && false |
||||
|
- run: |
||||
|
name: Smoke Test |
||||
|
command: echo todo && false |
||||
|
|
||||
|
workflows: |
||||
|
version: 2 |
||||
|
build-test-and-lint: |
||||
|
jobs: |
||||
|
- build |
||||
|
- back-test-unit: |
||||
|
requires: |
||||
|
- build |
||||
|
- back-test-e2e: |
||||
|
requires: |
||||
|
- build |
||||
|
- lint: |
||||
|
requires: |
||||
|
- build |
||||
|
- docker-build-and-push: |
||||
|
requires: |
||||
|
- build |
||||
|
- lint |
||||
|
- back-test-e2e |
||||
|
- back-test-unit |
||||
|
filters: |
||||
|
branches: |
||||
|
only: main |
||||
|
- back-deploy-heroku: |
||||
|
requires: |
||||
|
- build |
||||
|
- lint |
||||
|
- back-test-e2e |
||||
|
- back-test-unit |
||||
|
filters: |
||||
|
branches: |
||||
|
only: main |
||||
|
- server-doc-build: |
||||
|
requires: |
||||
|
- build |
||||
|
- lint |
||||
|
- back-test-e2e |
||||
|
- back-test-unit |
||||
|
filters: |
||||
|
branches: |
||||
|
only: main |
||||
|
- server-doc-deploy: |
||||
|
requires: |
||||
|
- server-doc-build |
||||
|
filters: |
||||
|
branches: |
||||
|
only: main |
||||
@ -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,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 |
||||
|
doc-server |
||||
|
.netlify |
||||
@ -0,0 +1,4 @@ |
|||||
|
{ |
||||
|
"singleQuote": true, |
||||
|
"trailingComma": "all" |
||||
|
} |
||||
@ -0,0 +1,10 @@ |
|||||
|
FROM node:dubnium |
||||
|
|
||||
|
WORKDIR /nest-server |
||||
|
|
||||
|
COPY . . |
||||
|
|
||||
|
RUN yarn install |
||||
|
|
||||
|
|
||||
|
CMD ["yarn", "start"] |
||||
@ -0,0 +1,29 @@ |
|||||
|
## Description |
||||
|
|
||||
|
Ce projet est fait avec [Nest](https://github.com/nestjs/nest). |
||||
|
|
||||
|
## Installation |
||||
|
|
||||
|
```bash |
||||
|
$ yarn install |
||||
|
``` |
||||
|
|
||||
|
## Démarer l'application |
||||
|
|
||||
|
```bash |
||||
|
# development |
||||
|
$ docker-compose -f docker/postgresql.yml up -d |
||||
|
$ yarn start |
||||
|
``` |
||||
|
|
||||
|
## Test |
||||
|
|
||||
|
```bash |
||||
|
# unit tests |
||||
|
$ npm run test |
||||
|
|
||||
|
# e2e tests |
||||
|
$ npm run test:e2e |
||||
|
``` |
||||
|
|
||||
|
Il y a un dockerfile pour builder en docker, pour lancer la stack avec docker, un docker-compose est présent. |
||||
@ -0,0 +1,6 @@ |
|||||
|
{ |
||||
|
"port": 9911, |
||||
|
"name": "Nuit info server", |
||||
|
"output": "doc-server", |
||||
|
"tsconfig": "./tsconfig.json" |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
version: '2' |
||||
|
services: |
||||
|
ognion-boilerplate-postgresql: |
||||
|
image: postgres:9.6.5 |
||||
|
# volumes: |
||||
|
# - ~/volumes/dashy/postgresql/:/var/lib/postgresql/data/ |
||||
|
environment: |
||||
|
- POSTGRES_USER=onionBoilerplate |
||||
|
- POSTGRES_PASSWORD=someNotSecurePassword |
||||
|
ports: |
||||
|
- 5432:5432 |
||||
@ -0,0 +1,13 @@ |
|||||
|
## API |
||||
|
PORT=3000 |
||||
|
API_HOST=localhost |
||||
|
API_PROTOCOL=http |
||||
|
|
||||
|
## LOGGER |
||||
|
LOG_LEVEL=debug |
||||
|
|
||||
|
## DB [TypeORM] |
||||
|
DATABASE_URL=postgres://onionBoilerplate:someNotSecurePassword@localhost:5432/onionBoilerplate |
||||
|
|
||||
|
## AUTHENTICATION [JWT] |
||||
|
JWT_SECRET=bananana |
||||
@ -0,0 +1,24 @@ |
|||||
|
/** |
||||
|
* generator/index.js |
||||
|
* |
||||
|
* Exports the generators so plop knows them |
||||
|
*/ |
||||
|
|
||||
|
const fs = require('fs'); |
||||
|
const path = require('path'); |
||||
|
const moduleGenerator = require('./module/index.js'); |
||||
|
|
||||
|
module.exports = plop => { |
||||
|
plop.setGenerator('module', moduleGenerator); |
||||
|
/* |
||||
|
plop.addHelper('directory', (comp) => { |
||||
|
try { |
||||
|
fs.accessSync(path.join(__dirname, `../../app/containers/${comp}`), fs.F_OK); |
||||
|
return `containers/${comp}`; |
||||
|
} catch (e) { |
||||
|
return `components/${comp}`; |
||||
|
} |
||||
|
}); |
||||
|
*/ |
||||
|
plop.addHelper('curly', (object, open) => (open ? '{' : '}')); |
||||
|
}; |
||||
@ -0,0 +1,89 @@ |
|||||
|
import { |
||||
|
Body, |
||||
|
Controller, |
||||
|
Delete, |
||||
|
Get, |
||||
|
NotFoundException, |
||||
|
Param, |
||||
|
ParseIntPipe, |
||||
|
Post, |
||||
|
Put, |
||||
|
Query, |
||||
|
} from '@nestjs/common'; |
||||
|
import { {{ properCase name }} } from './{{ kebabCase name }}.entity'; |
||||
|
import { {{ properCase name }}Service } from './{{ kebabCase name }}.service'; |
||||
|
import { |
||||
|
ApiBearerAuth, |
||||
|
ApiImplicitParam, |
||||
|
ApiResponse, |
||||
|
ApiUseTags, |
||||
|
} from '@nestjs/swagger'; |
||||
|
import { {{ properCase name }}Dto } from './{{ kebabCase name }}.dto'; |
||||
|
|
||||
|
|
||||
|
@ApiUseTags('{{ sentenceCase name }}') |
||||
|
@Controller() |
||||
|
// @ApiBearerAuth() |
||||
|
export class {{ properCase name }}Controller { |
||||
|
constructor(private readonly {{ camelCase name }}Service: {{ properCase name }}Service) {} |
||||
|
|
||||
|
@Get() |
||||
|
@ApiResponse({ |
||||
|
status: 200, |
||||
|
description: 'Get a list of all {{ sentenceCase name }}.', |
||||
|
type: {{ properCase name }}, |
||||
|
isArray: true, |
||||
|
}) |
||||
|
getAll(): Promise<{{ properCase name }}[]> { |
||||
|
return this.{{ camelCase name }}Service.getAll(); |
||||
|
} |
||||
|
|
||||
|
@Post() |
||||
|
@ApiResponse({ |
||||
|
status: 201, |
||||
|
description: 'The {{ sentenceCase name }} has been created.', |
||||
|
type: {{ properCase name }}, |
||||
|
}) |
||||
|
saveNew(@Body() {{ camelCase name }}Dto: {{ properCase name }}Dto): Promise<{{ properCase name }}> { |
||||
|
return this.{{ camelCase name }}Service.saveNew({{ camelCase name }}Dto); |
||||
|
} |
||||
|
|
||||
|
@Get(':id') |
||||
|
@ApiResponse({ |
||||
|
status: 200, |
||||
|
description: 'The {{ sentenceCase name }} with the matching id', |
||||
|
type: {{ properCase name }}, |
||||
|
}) |
||||
|
@ApiResponse({ status: 404, description: 'Not found.' }) |
||||
|
async findOne( |
||||
|
@Param('id', new ParseIntPipe()) id: number, |
||||
|
): Promise<{{ properCase name }}> { |
||||
|
return (await this.{{ camelCase name }}Service.getOneById(id)).orElseThrow( |
||||
|
() => new NotFoundException(), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
@Put(':id') |
||||
|
@ApiResponse({ |
||||
|
status: 200, |
||||
|
description: 'The updated {{ sentenceCase name }} with the matching id', |
||||
|
type: {{ properCase name }}, |
||||
|
}) |
||||
|
@ApiResponse({ status: 404, description: 'Not found.' }) |
||||
|
async updateOne( |
||||
|
@Param('id', new ParseIntPipe()) id: number, |
||||
|
@Body() {{ camelCase name }}Dto: {{ properCase name }}Dto, |
||||
|
): Promise<{{ properCase name }}> { |
||||
|
return this.{{ camelCase name }}Service.update(id, {{ camelCase name }}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 name }}Service.deleteById(id); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,7 @@ |
|||||
|
import { IsArray, IsOptional, IsString, Min, MinLength } from 'class-validator'; |
||||
|
import { Type } from 'class-transformer'; |
||||
|
import { ApiModelProperty } from '@nestjs/swagger'; |
||||
|
|
||||
|
export class {{ properCase name }}Dto { |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
import { DbAuditModel } from '../../utils/dbmodel.model'; |
||||
|
import { Column, Entity, ManyToOne, OneToMany } from 'typeorm'; |
||||
|
import { ApiModelProperty } from '@nestjs/swagger'; |
||||
|
|
||||
|
|
||||
|
@Entity() |
||||
|
export class {{ properCase name }} extends DbAuditModel { |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,203 @@ |
|||||
|
/** |
||||
|
* Component Generator |
||||
|
*/ |
||||
|
'use strict'; |
||||
|
|
||||
|
const { moduleExists } = require('../utils/moduleExists'); |
||||
|
|
||||
|
module.exports = { |
||||
|
description: 'Add an unconnected component', |
||||
|
prompts: [ |
||||
|
/* |
||||
|
{ |
||||
|
type: 'list', |
||||
|
name: 'type', |
||||
|
message: 'Select the type of component', |
||||
|
default: 'Stateless Function', |
||||
|
choices: () => [ |
||||
|
'Stateless Function', |
||||
|
'React.PureComponent', |
||||
|
'React.Component', |
||||
|
'Styled component', |
||||
|
], |
||||
|
}, |
||||
|
*/ |
||||
|
{ |
||||
|
type: 'input', |
||||
|
name: 'name', |
||||
|
message: 'What is the name of the module ?', |
||||
|
default: 'User', |
||||
|
validate: value => { |
||||
|
if (/.+/.test(value)) { |
||||
|
return moduleExists(value) |
||||
|
? 'A component or container with this name already exists' |
||||
|
: true; |
||||
|
} |
||||
|
|
||||
|
return 'The name is required'; |
||||
|
}, |
||||
|
}, |
||||
|
/* |
||||
|
{ |
||||
|
type: 'list', |
||||
|
name: 'sizeType', |
||||
|
message: 'Select the type of component for the atomic design', |
||||
|
default: 'atom', |
||||
|
choices: () => ['atom', 'molecule', 'organism', 'template', 'page', 'chart'], |
||||
|
}, |
||||
|
{ |
||||
|
type: 'confirm', |
||||
|
name: 'wantMessages', |
||||
|
default: true, |
||||
|
message: 'Do you want i18n messages (i.e. will this component use text)?', |
||||
|
}, |
||||
|
{ |
||||
|
type: 'confirm', |
||||
|
name: 'wantLoadable', |
||||
|
default: false, |
||||
|
message: 'Do you want to load the component asynchronously?', |
||||
|
}, |
||||
|
*/ |
||||
|
], |
||||
|
actions: data => { |
||||
|
// Generate index.js
|
||||
|
let componentTemplate; |
||||
|
let paternImport; |
||||
|
/* |
||||
|
switch (data.type) { |
||||
|
case 'Stateless Function': { |
||||
|
componentTemplate = './component/stateless.js.hbs'; |
||||
|
break; |
||||
|
} |
||||
|
case 'Styled component': { |
||||
|
componentTemplate = './component/styled.js.hbs'; |
||||
|
break; |
||||
|
} |
||||
|
default: { |
||||
|
componentTemplate = './component/class.js.hbs'; |
||||
|
} |
||||
|
} |
||||
|
switch (data.sizeType) { |
||||
|
// 'atom', 'molecule', 'organism', 'template', 'page'
|
||||
|
case 'atom': |
||||
|
paternImport = /(\/\/ needle-import-atoms)/g; |
||||
|
break; |
||||
|
case 'molecule': |
||||
|
paternImport = /(\/\/ needle-import-molecules)/g; |
||||
|
break; |
||||
|
case 'organism': |
||||
|
paternImport = /(\/\/ needle-import-organisms)/g; |
||||
|
break; |
||||
|
case 'template': |
||||
|
paternImport = /(\/\/ needle-import-templates)/g; |
||||
|
break; |
||||
|
case 'chart': |
||||
|
paternImport = /(\/\/ needle-import-charts)/g; |
||||
|
break; |
||||
|
default: |
||||
|
paternImport = /(\/\/ needle-import-pages)/g; |
||||
|
} |
||||
|
*/ |
||||
|
const actions = [ |
||||
|
{ |
||||
|
type: 'add', |
||||
|
path: |
||||
|
'../../src/modules/{{ kebabCase name }}/{{ kebabCase name }}.module.ts', |
||||
|
templateFile: './module/module.ts.hbs', |
||||
|
abortOnFail: true, |
||||
|
}, |
||||
|
{ |
||||
|
type: 'add', |
||||
|
path: |
||||
|
'../../src/modules/{{ kebabCase name }}/{{ kebabCase name }}.constants.ts', |
||||
|
templateFile: './module/constants.ts.hbs', |
||||
|
abortOnFail: true, |
||||
|
}, |
||||
|
{ |
||||
|
type: 'add', |
||||
|
path: |
||||
|
'../../src/modules/{{ kebabCase name }}/{{ kebabCase name }}.controller.ts', |
||||
|
templateFile: './module/controller.ts.hbs', |
||||
|
abortOnFail: true, |
||||
|
}, |
||||
|
{ |
||||
|
type: 'add', |
||||
|
path: |
||||
|
'../../src/modules/{{ kebabCase name }}/{{ kebabCase name }}.dto.ts', |
||||
|
templateFile: './module/dto.ts.hbs', |
||||
|
abortOnFail: true, |
||||
|
}, |
||||
|
{ |
||||
|
type: 'add', |
||||
|
path: |
||||
|
'../../src/modules/{{ kebabCase name }}/{{ kebabCase name }}.entity.ts', |
||||
|
templateFile: './module/entity.ts.hbs', |
||||
|
abortOnFail: true, |
||||
|
}, |
||||
|
{ |
||||
|
type: 'add', |
||||
|
path: |
||||
|
'../../src/modules/{{ kebabCase name }}/{{ kebabCase name }}.repository.ts', |
||||
|
templateFile: './module/repository.ts.hbs', |
||||
|
abortOnFail: true, |
||||
|
}, |
||||
|
{ |
||||
|
type: 'add', |
||||
|
path: |
||||
|
'../../src/modules/{{ kebabCase name }}/{{ kebabCase name }}.service.ts', |
||||
|
templateFile: './module/service.ts.hbs', |
||||
|
abortOnFail: true, |
||||
|
}, |
||||
|
{ |
||||
|
type: 'modify', |
||||
|
path: '../../src/app.module.ts', |
||||
|
pattern: /(\/\/ needle-module-import)/g, |
||||
|
template: |
||||
|
"import { {{properCase name}}Module } from './modules/{{ kebabCase name }}/{{ kebabCase name }}.module';\n$1", |
||||
|
}, |
||||
|
{ |
||||
|
type: 'modify', |
||||
|
path: '../../src/app.module.ts', |
||||
|
pattern: /(\/\/ needle-module-includes)/g, |
||||
|
template: '{{properCase name}}Module,\n$1', |
||||
|
}, |
||||
|
{ |
||||
|
type: 'modify', |
||||
|
path: '../../src/app.routes.ts', |
||||
|
pattern: /(\/\/ needle-module-import)/g, |
||||
|
template: |
||||
|
"import { {{properCase name}}Module } from './modules/{{ kebabCase name }}/{{ kebabCase name }}.module';\n$1", |
||||
|
}, |
||||
|
{ |
||||
|
type: 'modify', |
||||
|
path: '../../src/app.routes.ts', |
||||
|
pattern: /(\/\/ needle-modules-routes)/g, |
||||
|
templateFile: './module/routing.ts.hbs', |
||||
|
}, |
||||
|
]; |
||||
|
|
||||
|
// If they want a i18n messages file
|
||||
|
if (data.wantMessages) { |
||||
|
actions.push({ |
||||
|
type: 'add', |
||||
|
path: |
||||
|
'../../src/components//{{sizeType}}s/{{properCase name}}/messages.js', |
||||
|
templateFile: './component/messages.js.hbs', |
||||
|
abortOnFail: true, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// If want Loadable.js to load the component asynchronously
|
||||
|
if (data.wantLoadable) { |
||||
|
actions.push({ |
||||
|
type: 'add', |
||||
|
path: |
||||
|
'../../src/components//{{sizeType}}s/{{properCase name}}/Loadable.js', |
||||
|
templateFile: './component/loadable.js.hbs', |
||||
|
abortOnFail: true, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
return actions; |
||||
|
}, |
||||
|
}; |
||||
@ -0,0 +1,16 @@ |
|||||
|
import { Module } from '@nestjs/common'; |
||||
|
import { TypeOrmModule } from '@nestjs/typeorm'; |
||||
|
import { {{ properCase name }}Controller } from './{{ kebabCase name }}.controller'; |
||||
|
import { {{ properCase name }}Service } from './{{ kebabCase name }}.service'; |
||||
|
import { {{ properCase name }} } from './{{ kebabCase name }}.entity'; |
||||
|
import { {{ properCase name }}Repository } from './{{ kebabCase name }}.repository'; |
||||
|
|
||||
|
@Module({ |
||||
|
imports: [ |
||||
|
TypeOrmModule.forFeature([{{ properCase name }}, {{ properCase name }}Repository]), |
||||
|
], |
||||
|
controllers: [{{ properCase name }}Controller], |
||||
|
providers: [{{ properCase name }}Service], |
||||
|
exports: [{{ properCase name }}Service], |
||||
|
}) |
||||
|
export class {{ properCase name }}Module {} |
||||
@ -0,0 +1,12 @@ |
|||||
|
import { EntityRepository, Repository } from 'typeorm'; |
||||
|
import { {{ properCase name }} } from './{{ kebabCase name }}.entity'; |
||||
|
import Optional from 'typescript-optional'; |
||||
|
|
||||
|
@EntityRepository({{ properCase name }}) |
||||
|
export class {{ properCase name }}Repository extends Repository<{{ properCase name }}> { |
||||
|
async findOneById(id: number): Promise<Optional<{{ properCase name }}>> { |
||||
|
return Optional.ofNullable( |
||||
|
await this.findOne(id, {}), |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,5 @@ |
|||||
|
{ |
||||
|
path: '/{{ kebabCase name }}s', |
||||
|
module: {{properCase name}}Module, |
||||
|
}, |
||||
|
$1 |
||||
@ -0,0 +1,54 @@ |
|||||
|
import { {{ properCase name }} } from './{{ kebabCase name }}.entity'; |
||||
|
import { Inject, Injectable, NotFoundException } from '@nestjs/common'; |
||||
|
import { InjectRepository } from '@nestjs/typeorm'; |
||||
|
import { {{ properCase name }}Repository } from './{{ kebabCase name }}.repository'; |
||||
|
import { } from './{{ kebabCase name }}.constants'; |
||||
|
import { {{ properCase name }}Dto } from './{{ kebabCase name }}.dto'; |
||||
|
import Optional from 'typescript-optional'; |
||||
|
|
||||
|
|
||||
|
@Injectable() |
||||
|
export class {{ properCase name }}Service { |
||||
|
|
||||
|
constructor( |
||||
|
@InjectRepository({{ properCase name }}Repository) |
||||
|
private readonly {{ camelCase name }}Repository: {{ properCase name }}Repository, |
||||
|
) { } |
||||
|
|
||||
|
async getAll(): Promise<{{ properCase name }}[]> { |
||||
|
return this.{{ camelCase name }}Repository.find({}); |
||||
|
} |
||||
|
|
||||
|
async getOneById(id: number): Promise<Optional<{{ properCase name }}>> { |
||||
|
return this.{{ camelCase name }}Repository.findOneById(id); |
||||
|
} |
||||
|
|
||||
|
async saveNew(body: {{ properCase name }}Dto): Promise<{{ properCase name }}> { |
||||
|
let {{ camelCase name }}New = new {{ properCase name }}(); |
||||
|
|
||||
|
// Complete with the mappings |
||||
|
|
||||
|
{{ camelCase name }}New = await this.{{ camelCase name }}Repository.save({{ camelCase name }}New); |
||||
|
|
||||
|
return {{ camelCase name }}New; |
||||
|
} |
||||
|
|
||||
|
async update(id: number, body: {{ properCase name }}Dto): Promise<{{ properCase name }}> { |
||||
|
let {{ camelCase name }}Found = (await this.{{ camelCase name }}Repository.findOneById(id)).orElseThrow( |
||||
|
() => new NotFoundException(), |
||||
|
); |
||||
|
|
||||
|
// Complete with the mappings |
||||
|
|
||||
|
{{ camelCase name }}Found = await this.{{ camelCase name }}Repository.save({{ camelCase name }}Found); |
||||
|
|
||||
|
return {{ camelCase name }}Found; |
||||
|
} |
||||
|
|
||||
|
async deleteById(id: number): Promise<void> { |
||||
|
const {{ camelCase name }}Found = (await this.{{ camelCase name }}Repository.findOneById( |
||||
|
id, |
||||
|
)).orElseThrow(() => new NotFoundException()); |
||||
|
await this.{{ camelCase name }}Repository.remove({{ camelCase name }}Found); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
/** |
||||
|
* componentExists |
||||
|
* |
||||
|
* Check whether the given component exist in either the components or containers directory |
||||
|
*/ |
||||
|
|
||||
|
const fs = require('fs'); |
||||
|
const path = require('path'); |
||||
|
|
||||
|
const pathModules = path.join(__dirname, '../../../src/modules'); |
||||
|
|
||||
|
const modules = fs.readdirSync(pathModules); |
||||
|
|
||||
|
function moduleExists(store) { |
||||
|
return modules.indexOf(store) >= 0; |
||||
|
} |
||||
|
|
||||
|
module.exports = { |
||||
|
moduleExists, |
||||
|
}; |
||||
@ -0,0 +1,5 @@ |
|||||
|
{ |
||||
|
"language": "ts", |
||||
|
"collection": "@nestjs/schematics", |
||||
|
"sourceRoot": "src" |
||||
|
} |
||||
@ -0,0 +1,6 @@ |
|||||
|
{ |
||||
|
"watch": ["src"], |
||||
|
"ext": "ts", |
||||
|
"ignore": ["src/**/*.spec.ts"], |
||||
|
"exec": "node --inspect-brk -r ts-node/register src/main.ts" |
||||
|
} |
||||
@ -0,0 +1,6 @@ |
|||||
|
{ |
||||
|
"watch": ["src"], |
||||
|
"ext": "ts", |
||||
|
"ignore": ["src/**/*.spec.ts"], |
||||
|
"exec": "ts-node -r tsconfig-paths/register src/main.ts" |
||||
|
} |
||||
@ -0,0 +1,109 @@ |
|||||
|
{ |
||||
|
"name": "nest-onion-boilerplate", |
||||
|
"version": "0.0.1", |
||||
|
"description": "a boilerplate for my projects", |
||||
|
"author": "Nicolas Beaussart <nic.beaussart@gmail.com>", |
||||
|
"license": "MIT", |
||||
|
"scripts": { |
||||
|
"format": "prettier --write \"src/**/*.ts\"", |
||||
|
"format:check": "prettier --list-different \"src/**/*{.ts,.scss,.html}\"", |
||||
|
"start": "ts-node -r tsconfig-paths/register src/main.ts", |
||||
|
"start:dev": "nodemon", |
||||
|
"start:debug": "nodemon --config nodemon-debug.json", |
||||
|
"prestart:prod": "rimraf dist && tsc", |
||||
|
"start:prod": "node dist/main.js", |
||||
|
"start:hmr": "node dist/server", |
||||
|
"lint": "tslint -p tsconfig.json -c tslint.json", |
||||
|
"doc:build": "./node_modules/.bin/compodoc -c compodoc.server.json", |
||||
|
"doc": "./node_modules/.bin/compodoc -c compodoc.server.json -s -o -w", |
||||
|
"test": "jest", |
||||
|
"test:ci": "jest --runInBand --coverage", |
||||
|
"test:watch": "jest --watch", |
||||
|
"test:cov": "jest --coverage", |
||||
|
"test:e2e": "jest --config ./test/jest-e2e.json", |
||||
|
"generate": "plop --plopfile internals/generator/index.js", |
||||
|
"webpack": "webpack --config webpack.config.js" |
||||
|
}, |
||||
|
"dependencies": { |
||||
|
"@nestjs/common": "^5.1.0", |
||||
|
"@nestjs/core": "^5.1.0", |
||||
|
"@nestjs/jwt": "^0.2.0", |
||||
|
"@nestjs/passport": "^5.1.0", |
||||
|
"@nestjs/swagger": "^2.5.1", |
||||
|
"@nestjs/typeorm": "^5.2.2", |
||||
|
"argon2": "^0.20.0", |
||||
|
"class-sanitizer": "^0.0.5", |
||||
|
"class-transformer": "^0.3.1", |
||||
|
"class-validator": "^0.9.1", |
||||
|
"dotenv": "^6.1.0", |
||||
|
"joi": "^14.1.0", |
||||
|
"nest-router": "^1.0.7", |
||||
|
"passport": "^0.4.0", |
||||
|
"passport-jwt": "^4.0.0", |
||||
|
"pg": "^7.6.1", |
||||
|
"reflect-metadata": "^0.1.12", |
||||
|
"rxjs": "^6.2.2", |
||||
|
"typeorm": "^0.2.29", |
||||
|
"typescript": "^3.0.1", |
||||
|
"typescript-optional": "^1.8.0", |
||||
|
"winston": "^3.1.0" |
||||
|
}, |
||||
|
"devDependencies": { |
||||
|
"@compodoc/compodoc": "^1.1.11", |
||||
|
"@nestjs/testing": "^5.1.0", |
||||
|
"@types/express": "^4.16.0", |
||||
|
"@types/jest": "^23.3.1", |
||||
|
"@types/joi": "^14.0.0", |
||||
|
"@types/node": "^10.7.1", |
||||
|
"@types/supertest": "^2.0.5", |
||||
|
"husky": "^1.0.0-rc.15", |
||||
|
"jest": "^23.5.0", |
||||
|
"lint-staged": "^8.1.0", |
||||
|
"lodash": "^4.17.20", |
||||
|
"node-plop": "^0.16.0", |
||||
|
"nodemon": "^1.18.3", |
||||
|
"plop": "^2.1.0", |
||||
|
"prettier": "^1.14.2", |
||||
|
"rimraf": "^2.6.2", |
||||
|
"supertest": "^3.1.0", |
||||
|
"ts-jest": "^23.1.3", |
||||
|
"ts-loader": "^5.3.1", |
||||
|
"ts-node": "^7.0.1", |
||||
|
"tsconfig-paths": "^3.5.0", |
||||
|
"tslint": "5.11.0", |
||||
|
"tslint-config-prettier": "^1.15.0", |
||||
|
"webpack": "^4.16.5", |
||||
|
"webpack-cli": "^3.1.0", |
||||
|
"webpack-node-externals": "^1.7.2" |
||||
|
}, |
||||
|
"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" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,22 @@ |
|||||
|
import { Test, TestingModule } from '@nestjs/testing'; |
||||
|
import { INestApplication } from '@nestjs/common'; |
||||
|
import { AppController } from './app.controller'; |
||||
|
import { AppService } from './app.service'; |
||||
|
|
||||
|
describe('AppController', () => { |
||||
|
let app: TestingModule; |
||||
|
|
||||
|
beforeAll(async () => { |
||||
|
app = await Test.createTestingModule({ |
||||
|
controllers: [AppController], |
||||
|
providers: [AppService], |
||||
|
}).compile(); |
||||
|
}); |
||||
|
|
||||
|
describe('root', () => { |
||||
|
it('should return "Hello World!"', () => { |
||||
|
const appController = app.get<AppController>(AppController); |
||||
|
expect(appController.root()).toBe('Hello World!'); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,12 @@ |
|||||
|
import { Get, Controller } from '@nestjs/common'; |
||||
|
import { AppService } from './app.service'; |
||||
|
|
||||
|
@Controller() |
||||
|
export class AppController { |
||||
|
constructor(private readonly appService: AppService) {} |
||||
|
|
||||
|
@Get() |
||||
|
root(): string { |
||||
|
return this.appService.root(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,43 @@ |
|||||
|
import { Module } from '@nestjs/common'; |
||||
|
import { AppController } from './app.controller'; |
||||
|
import { AppService } from './app.service'; |
||||
|
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'; |
||||
|
// 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], |
||||
|
}), |
||||
|
LoggerModule, // Global
|
||||
|
RouterModule.forRoutes(appRoutes), |
||||
|
AuthModule, |
||||
|
UserModule, |
||||
|
RolesModule, |
||||
|
// needle-module-includes
|
||||
|
], |
||||
|
controllers: [AppController], |
||||
|
providers: [AppService, RolesGuard], |
||||
|
}) |
||||
|
export class AppModule {} |
||||
@ -0,0 +1,22 @@ |
|||||
|
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'; |
||||
|
// needle-module-import
|
||||
|
|
||||
|
export const appRoutes: Routes = [ |
||||
|
{ |
||||
|
path: '/users', |
||||
|
module: UserModule, |
||||
|
}, |
||||
|
{ |
||||
|
path: '/roles', |
||||
|
module: RolesModule, |
||||
|
}, |
||||
|
{ |
||||
|
path: '/auth', |
||||
|
module: AuthController, |
||||
|
}, |
||||
|
// needle-modules-routes
|
||||
|
]; |
||||
@ -0,0 +1,8 @@ |
|||||
|
import { Injectable } from '@nestjs/common'; |
||||
|
|
||||
|
@Injectable() |
||||
|
export class AppService { |
||||
|
root(): string { |
||||
|
return 'Hello World!'; |
||||
|
} |
||||
|
} |
||||
@ -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.payload; |
||||
|
}, |
||||
|
); |
||||
@ -0,0 +1,3 @@ |
|||||
|
import { ReflectMetadata } from '@nestjs/common'; |
||||
|
|
||||
|
export const Roles = (...roles: string[]) => ReflectMetadata('roles', roles); |
||||
@ -0,0 +1,22 @@ |
|||||
|
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; |
||||
|
import { Reflector } from '@nestjs/core'; |
||||
|
|
||||
|
@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 = request.user; |
||||
|
const hasRole = () => |
||||
|
user.roles.some((role: string) => roles.includes(role)); |
||||
|
|
||||
|
return user && user.roles && hasRole(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
import { Injectable, NestInterceptor, ExecutionContext } from '@nestjs/common'; |
||||
|
import { Observable } from 'rxjs'; |
||||
|
import { map } from 'rxjs/operators'; |
||||
|
import { classToPlain } from 'class-transformer'; |
||||
|
|
||||
|
@Injectable() |
||||
|
export class TransformInterceptor implements NestInterceptor<any, any> { |
||||
|
intercept( |
||||
|
context: ExecutionContext, |
||||
|
call$: Observable<any>, |
||||
|
): Observable<any> { |
||||
|
return call$.pipe(map(data => classToPlain(data))); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,62 @@ |
|||||
|
import { NestFactory } 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 { TransformInterceptor } from './interceptors/transform.interceptor'; |
||||
|
|
||||
|
async function bootstrap() { |
||||
|
// Use .env to configure environment variables (process.env)
|
||||
|
config(); |
||||
|
|
||||
|
const app = await NestFactory.create(AppModule); |
||||
|
|
||||
|
// Set logger
|
||||
|
app.useLogger(app.get(LoggerService)); |
||||
|
|
||||
|
// Enable cors
|
||||
|
app.enableCors(); |
||||
|
|
||||
|
// Interceptors
|
||||
|
const loggerInterceptor = app |
||||
|
.select(LoggerModule) |
||||
|
.get(LoggerExceptionInterceptor); |
||||
|
app.useGlobalInterceptors( |
||||
|
loggerInterceptor, // Log exceptions
|
||||
|
new TransformInterceptor(), |
||||
|
); |
||||
|
|
||||
|
const connfigService = app.select(ConfigModule).get(ConfigService); |
||||
|
|
||||
|
// Guards
|
||||
|
const rolesGuard = app.select(AppModule).get(RolesGuard); |
||||
|
app.useGlobalGuards(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,39 @@ |
|||||
|
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 { AuthGuard } from '@nestjs/passport'; |
||||
|
|
||||
|
@ApiUseTags('Auth') |
||||
|
@Controller() |
||||
|
export class AuthController { |
||||
|
constructor( |
||||
|
private readonly authService: AuthService, |
||||
|
private readonly userService: UserService, |
||||
|
) {} |
||||
|
|
||||
|
@Post('login') |
||||
|
async signIn(@Body() userLogin: LoginDto): Promise<TokenDto> { |
||||
|
const token = await this.authService.signIn( |
||||
|
userLogin.email, |
||||
|
userLogin.password, |
||||
|
); |
||||
|
return { token }; |
||||
|
} |
||||
|
|
||||
|
@Post('register') |
||||
|
async registerUser(@Body() userRegister: UserDtoRegister): Promise<User> { |
||||
|
return this.userService.saveNew(userRegister); |
||||
|
} |
||||
|
|
||||
|
@Get('me') |
||||
|
@ApiBearerAuth() |
||||
|
@UseGuards(AuthGuard()) |
||||
|
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,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,30 @@ |
|||||
|
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 (!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); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,22 @@ |
|||||
|
import { |
||||
|
ExecutionContext, |
||||
|
Injectable, |
||||
|
UnauthorizedException, |
||||
|
} from '@nestjs/common'; |
||||
|
import { AuthGuard } from '@nestjs/passport'; |
||||
|
|
||||
|
@Injectable() |
||||
|
export class JwtAuthGuard extends AuthGuard('jwt') { |
||||
|
canActivate(context: ExecutionContext) { |
||||
|
// Add your custom authentication logic here
|
||||
|
// for example, call super.logIn(request) to establish a session.
|
||||
|
return super.canActivate(context); |
||||
|
} |
||||
|
|
||||
|
handleRequest(err, user, info) { |
||||
|
if (err || !user) { |
||||
|
throw err || new UnauthorizedException(); |
||||
|
} |
||||
|
return user; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,26 @@ |
|||||
|
import { Injectable, UnauthorizedException } 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) { |
||||
|
const user = await this.authService.validateUser(payload); |
||||
|
if (!user) { |
||||
|
throw new UnauthorizedException(); |
||||
|
} |
||||
|
return user; |
||||
|
} |
||||
|
} |
||||
@ -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,75 @@ |
|||||
|
import { Inject, Injectable } from '@nestjs/common'; |
||||
|
import * as Joi from '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'), |
||||
|
PORT: Joi.number().default(3000), |
||||
|
API_HOST: Joi.string().default('localhost'), |
||||
|
API_PROTOCOL: Joi.string().default('http'), |
||||
|
LOG_LEVEL: Joi.string() |
||||
|
.valid(['error', 'warning', 'info', 'debug', 'silly']) |
||||
|
.default('debug'), |
||||
|
DATABASE_URL: Joi.string().required(), |
||||
|
JWT_SECRET: Joi.string() |
||||
|
.required() |
||||
|
.default('some_default_jwt_bad_practice_!!'), |
||||
|
LOG_SQL_REQUEST: Joi.boolean().default(false), |
||||
|
}); |
||||
|
|
||||
|
const { error, value: validatedEnvConfig } = Joi.validate( |
||||
|
envConfig, |
||||
|
envVarsSchema, |
||||
|
{ |
||||
|
stripUnknown: true, |
||||
|
}, |
||||
|
); |
||||
|
if (error) { |
||||
|
throw new Error(`Config validation error: ${error.message}`); |
||||
|
} |
||||
|
return validatedEnvConfig; |
||||
|
} |
||||
|
|
||||
|
get databaseUrl(): string { |
||||
|
return this.envConfig.DATABASE_URL; |
||||
|
} |
||||
|
|
||||
|
get jwtSecret(): string { |
||||
|
return this.envConfig.JWT_SECRET; |
||||
|
} |
||||
|
|
||||
|
get isLoggingDb(): boolean { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
get loggerLevel(): string { |
||||
|
return this.envConfig.LOG_LEVEL; |
||||
|
} |
||||
|
|
||||
|
get port(): number { |
||||
|
return this.envConfig.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,15 @@ |
|||||
|
import { Test, TestingModule } from '@nestjs/testing'; |
||||
|
import { CryptoService } from './crypto.service'; |
||||
|
|
||||
|
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(); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,28 @@ |
|||||
|
import { Injectable } from '@nestjs/common'; |
||||
|
import * as argon2 from 'argon2'; |
||||
|
|
||||
|
@Injectable() |
||||
|
export class CryptoService { |
||||
|
private readonly type = argon2.argon2id; |
||||
|
|
||||
|
constructor() {} |
||||
|
|
||||
|
/** |
||||
|
* Compare hash |
||||
|
* @param {string} plain |
||||
|
* @param {string} hash |
||||
|
* @returns {Promise<boolean>} |
||||
|
*/ |
||||
|
public async compare(plain: string, hash: string): Promise<boolean> { |
||||
|
return await argon2.verify(hash, plain); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Generate hash |
||||
|
* @param {string} plain |
||||
|
* @returns {Promise<string>} |
||||
|
*/ |
||||
|
public async hash(plain: string): Promise<string> { |
||||
|
return await argon2.hash(plain, { type: this.type }); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,46 @@ |
|||||
|
import { |
||||
|
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, |
||||
|
call$: Observable<any>, |
||||
|
): Observable<any> { |
||||
|
const request = context.switchToHttp().getRequest(); |
||||
|
|
||||
|
return call$.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,18 @@ |
|||||
|
import { Test, TestingModule } from '@nestjs/testing'; |
||||
|
import { LoggerService } from './logger.service'; |
||||
|
import { ConfigModule } from '../config/config.module'; |
||||
|
import { loggerProviders } from './logger.providers'; |
||||
|
|
||||
|
describe('LoggerService', () => { |
||||
|
let service: LoggerService; |
||||
|
beforeAll(async () => { |
||||
|
const module: TestingModule = await Test.createTestingModule({ |
||||
|
providers: [LoggerService, ...loggerProviders], |
||||
|
imports: [ConfigModule], |
||||
|
}).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,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,15 @@ |
|||||
|
import { HttpException, HttpStatus } from '@nestjs/common'; |
||||
|
import { createHttpExceptionBody } from '@nestjs/common/utils/http-exception-body.util'; |
||||
|
|
||||
|
export class ValidationException extends HttpException { |
||||
|
constructor(errors: string | object | any) { |
||||
|
super( |
||||
|
createHttpExceptionBody( |
||||
|
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,52 @@ |
|||||
|
import { |
||||
|
Body, |
||||
|
Controller, |
||||
|
Delete, |
||||
|
Get, |
||||
|
NotFoundException, |
||||
|
Param, |
||||
|
ParseIntPipe, |
||||
|
Post, |
||||
|
Put, |
||||
|
Query, |
||||
|
} from '@nestjs/common'; |
||||
|
import { Role } from './roles.entity'; |
||||
|
import { RolesService } from './roles.service'; |
||||
|
import { |
||||
|
ApiBearerAuth, |
||||
|
ApiImplicitParam, |
||||
|
ApiResponse, |
||||
|
ApiUseTags, |
||||
|
} from '@nestjs/swagger'; |
||||
|
import { RolesDto } from './roles.dto'; |
||||
|
|
||||
|
@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,5 @@ |
|||||
|
import { IsArray, IsOptional, IsString, Min, MinLength } from 'class-validator'; |
||||
|
import { Type } from 'class-transformer'; |
||||
|
import { ApiModelProperty } from '@nestjs/swagger'; |
||||
|
|
||||
|
export class RolesDto {} |
||||
@ -0,0 +1,10 @@ |
|||||
|
import { DbAuditModel } from '../../utils/dbmodel.model'; |
||||
|
import { Column, Entity, ManyToOne, OneToMany } from 'typeorm'; |
||||
|
import { ApiModelProperty } from '@nestjs/swagger'; |
||||
|
|
||||
|
@Entity() |
||||
|
export class Role extends DbAuditModel { |
||||
|
@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,15 @@ |
|||||
|
import { EntityRepository, Repository } from 'typeorm'; |
||||
|
import { Role } from './roles.entity'; |
||||
|
import Optional from 'typescript-optional'; |
||||
|
import { ADMIN_ROLE, USER_ROLE } from './roles.constants'; |
||||
|
|
||||
|
@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,64 @@ |
|||||
|
import { Role } from './roles.entity'; |
||||
|
import { Inject, Injectable, NotFoundException } from '@nestjs/common'; |
||||
|
import { InjectRepository } from '@nestjs/typeorm'; |
||||
|
import { RolesRepository } from './roles.repository'; |
||||
|
import { USER_ROLE, ADMIN_ROLE } from './roles.constants'; |
||||
|
import { RolesDto } from './roles.dto'; |
||||
|
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 in [USER_ROLE, ADMIN_ROLE]) { |
||||
|
if ((await this.rolesRepository.findRoleByName(role)).isEmpty) { |
||||
|
const roleDb = new Role(); |
||||
|
roleDb.name = 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 saveNew(body: RolesDto): Promise<Role> { |
||||
|
let rolesNew = new Role(); |
||||
|
|
||||
|
// Complete with the mappings
|
||||
|
|
||||
|
rolesNew = await this.rolesRepository.save(rolesNew); |
||||
|
|
||||
|
return rolesNew; |
||||
|
} |
||||
|
|
||||
|
async update(id: number, body: RolesDto): Promise<Role> { |
||||
|
let rolesFound = (await this.rolesRepository.findOneById(id)).orElseThrow( |
||||
|
() => new NotFoundException(), |
||||
|
); |
||||
|
|
||||
|
// Complete with the mappings
|
||||
|
|
||||
|
rolesFound = await this.rolesRepository.save(rolesFound); |
||||
|
|
||||
|
return rolesFound; |
||||
|
} |
||||
|
|
||||
|
async deleteById(id: number): Promise<void> { |
||||
|
const rolesFound = (await this.rolesRepository.findOneById(id)).orElseThrow( |
||||
|
() => new NotFoundException(), |
||||
|
); |
||||
|
await this.rolesRepository.remove(rolesFound); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,111 @@ |
|||||
|
import { |
||||
|
Body, |
||||
|
Controller, |
||||
|
Delete, |
||||
|
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'; |
||||
|
|
||||
|
@ApiUseTags('User') |
||||
|
@Controller() |
||||
|
@ApiBearerAuth() |
||||
|
@UseGuards(AuthGuard()) |
||||
|
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): Promise<User> { |
||||
|
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, |
||||
|
): Promise<User> { |
||||
|
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, |
||||
|
): Promise<User> { |
||||
|
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): Promise<void> { |
||||
|
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,22 @@ |
|||||
|
import { DbAuditModel } from '../../utils/dbmodel.model'; |
||||
|
import { Column, Entity, ManyToOne, OneToMany } from 'typeorm'; |
||||
|
import { ApiModelProperty } from '@nestjs/swagger'; |
||||
|
import { Exclude } from 'class-transformer'; |
||||
|
@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; |
||||
|
} |
||||
@ -0,0 +1,15 @@ |
|||||
|
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'; |
||||
|
|
||||
|
@Module({ |
||||
|
imports: [TypeOrmModule.forFeature([User, UserRepository]), CryptoModule], |
||||
|
controllers: [UserController], |
||||
|
providers: [UserService], |
||||
|
exports: [UserService], |
||||
|
}) |
||||
|
export class UserModule {} |
||||
@ -0,0 +1,20 @@ |
|||||
|
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, {})); |
||||
|
} |
||||
|
|
||||
|
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,104 @@ |
|||||
|
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'; |
||||
|
|
||||
|
@Injectable() |
||||
|
export class UserService { |
||||
|
constructor( |
||||
|
@InjectRepository(UserRepository) |
||||
|
private readonly userRepository: UserRepository, |
||||
|
private readonly cryptoService: CryptoService, |
||||
|
) {} |
||||
|
|
||||
|
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(); |
||||
|
|
||||
|
userNew.password = await this.cryptoService.hash(userRegister.password); |
||||
|
userNew.email = userRegister.email.toLowerCase(); |
||||
|
userNew.lastName = userRegister.lastName; |
||||
|
userNew.firstName = userRegister.firstName; |
||||
|
|
||||
|
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,27 @@ |
|||||
|
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() |
||||
|
creationDate: Date; |
||||
|
|
||||
|
@UpdateDateColumn() |
||||
|
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('/ (GET)', () => { |
||||
|
return request(app.getHttpServer()) |
||||
|
.get('/') |
||||
|
.expect(200) |
||||
|
.expect('Hello World!'); |
||||
|
}); |
||||
|
|
||||
|
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" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
{ |
||||
|
"compilerOptions": { |
||||
|
"module": "commonjs", |
||||
|
"declaration": true, |
||||
|
"noImplicitAny": false, |
||||
|
"removeComments": true, |
||||
|
"noLib": false, |
||||
|
"allowSyntheticDefaultImports": true, |
||||
|
"emitDecoratorMetadata": true, |
||||
|
"experimentalDecorators": true, |
||||
|
"target": "es6", |
||||
|
"sourceMap": true, |
||||
|
"outDir": "./dist", |
||||
|
"lib": ["es2017", "es2015"], |
||||
|
"baseUrl": "./src" |
||||
|
}, |
||||
|
"include": ["src/**/*"], |
||||
|
"exclude": ["node_modules", "**/*.spec.ts"] |
||||
|
} |
||||
@ -0,0 +1,7 @@ |
|||||
|
{ |
||||
|
"extends": "tsconfig.json", |
||||
|
"compilerOptions": { |
||||
|
"types": ["jest", "node"] |
||||
|
}, |
||||
|
"include": ["**/*.spec.ts", "**/*.d.ts"] |
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
{ |
||||
|
"defaultSeverity": "error", |
||||
|
"extends": ["tslint:recommended", "tslint-config-prettier"], |
||||
|
"jsRules": { |
||||
|
"no-unused-expression": true |
||||
|
}, |
||||
|
"rules": { |
||||
|
"eofline": false, |
||||
|
"quotemark": [true, "single"], |
||||
|
"indent": false, |
||||
|
"member-access": [false], |
||||
|
"ordered-imports": [false], |
||||
|
"max-line-length": [true, 150], |
||||
|
"member-ordering": [false], |
||||
|
"curly": false, |
||||
|
"interface-name": [false], |
||||
|
"array-type": [false], |
||||
|
"no-empty-interface": false, |
||||
|
"no-empty": false, |
||||
|
"arrow-parens": false, |
||||
|
"object-literal-sort-keys": false, |
||||
|
"no-unused-expression": false, |
||||
|
"max-classes-per-file": [false], |
||||
|
"variable-name": [false], |
||||
|
"one-line": [false], |
||||
|
"one-variable-per-declaration": [false] |
||||
|
}, |
||||
|
"rulesDirectory": [] |
||||
|
} |
||||
@ -0,0 +1,34 @@ |
|||||
|
const webpack = require('webpack'); |
||||
|
const path = require('path'); |
||||
|
const nodeExternals = require('webpack-node-externals'); |
||||
|
|
||||
|
module.exports = { |
||||
|
entry: ['webpack/hot/poll?1000', './src/main.hmr.ts'], |
||||
|
watch: true, |
||||
|
target: 'node', |
||||
|
externals: [ |
||||
|
nodeExternals({ |
||||
|
whitelist: ['webpack/hot/poll?1000'], |
||||
|
}), |
||||
|
], |
||||
|
module: { |
||||
|
rules: [ |
||||
|
{ |
||||
|
test: /\.tsx?$/, |
||||
|
use: 'ts-loader', |
||||
|
exclude: /node_modules/, |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
mode: "development", |
||||
|
resolve: { |
||||
|
extensions: ['.tsx', '.ts', '.js'], |
||||
|
}, |
||||
|
plugins: [ |
||||
|
new webpack.HotModuleReplacementPlugin(), |
||||
|
], |
||||
|
output: { |
||||
|
path: path.join(__dirname, 'dist'), |
||||
|
filename: 'server.js', |
||||
|
}, |
||||
|
}; |
||||
9723
yarn.lock
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
Write
Preview
Loading…
Cancel
Save
Reference in new issue