├── .gitignore
├── .nestcli.json
├── .prettierrc
├── README.md
├── config
└── default.ts
├── nodemon.json
├── package.json
├── src
├── app.controller.spec.ts
├── app.controller.ts
├── app.module.ts
├── app.service.ts
├── main.ts
├── shared
│ ├── api-exception.model.ts
│ ├── auth
│ │ ├── auth.service.spec.ts
│ │ ├── auth.service.ts
│ │ ├── jwt-payload.interface.ts
│ │ └── strategies
│ │ │ ├── jwt-strategy.service.spec.ts
│ │ │ └── jwt-strategy.service.ts
│ ├── base.model.ts
│ ├── base.service.ts
│ ├── configuration
│ │ ├── configuration.enum.ts
│ │ ├── configuration.service.spec.ts
│ │ └── configuration.service.ts
│ ├── decorators
│ │ └── roles.decorator.ts
│ ├── filters
│ │ └── http-exception.filter.ts
│ ├── guards
│ │ └── roles.guard.ts
│ ├── mapper
│ │ ├── mapper.service.spec.ts
│ │ └── mapper.service.ts
│ ├── pipes
│ │ └── to-boolean.pipe.ts
│ ├── shared.module.ts
│ └── utilities
│ │ ├── enum-to-array.ts
│ │ └── get-operation-id.ts
├── todo
│ ├── models
│ │ ├── todo-level.enum.ts
│ │ ├── todo.models.ts
│ │ └── veiw-models
│ │ │ ├── todo-params.model.ts
│ │ │ └── todo-vm.model.ts
│ ├── todo.controller.spec.ts
│ ├── todo.controller.ts
│ ├── todo.module.ts
│ ├── todo.service.spec.ts
│ └── todo.service.ts
└── user
│ ├── models
│ ├── user-role.enum.ts
│ ├── user.model.ts
│ └── view-models
│ │ ├── login-response-vm.model.ts
│ │ ├── login-vm.model.ts
│ │ ├── register-vm.model.ts
│ │ └── user-vm.model.ts
│ ├── user.controller.spec.ts
│ ├── user.controller.ts
│ ├── user.module.ts
│ ├── user.service.spec.ts
│ └── user.service.ts
├── test
├── app.e2e-spec.ts
└── jest-e2e.json
├── tsconfig.json
├── tslint.json
└── webpack.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | node_modules
3 | .idea
4 | yarn.lock
5 |
--------------------------------------------------------------------------------
/.nestcli.json:
--------------------------------------------------------------------------------
1 | {
2 | "language": "ts",
3 | "collection": "@nestjs/schematics"
4 | }
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all"
4 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Node JS REST API starter pack
2 |
3 | This is a starter pack REST API NodeJS based for fast develop advanced API services with Enterprise architecture.
4 |
5 | ## Installation
6 |
7 | npm install
8 |
9 | ## Usage
10 |
11 | Development mode
12 |
13 | npm run start
14 | Production
15 |
16 | npm run start:prod
17 | Run test
18 |
19 | npm run test
20 |
21 |
22 | ## Features:
23 | - Typescript 3+
24 | - NestJS 5 (Express powered)
25 | - JWT
26 | - Mongoose - typegoose
27 | - Automapper
28 | - Swagger
29 | - Jest
30 | - RxJS
31 |
32 | All modules has last versions and regular updated
33 |
34 | # Architecture
35 |
36 | Inspired by Angular. Great thanks to [NESTJS](https://nestjs.com/)
37 | Go to docs for more information about framework and best practices
38 |
39 | - **config** - contains `default.ts` with env parameters
40 | - **src**
41 | - **shared** - complete features for any purposes
42 | - **todo** - example implementation secure routes
43 | - **user** - auth routes
44 |
45 | ## Create modules/services and folders
46 |
47 | Nest provides an out-of-the-box application architecture which allows for effortless creation of highly testable, scalable, loosely coupled, and easily maintainable applications.
48 | Use Nest [CLI](https://docs.nestjs.com/cli/usages) for correct creating new modules/services & controllers
49 |
50 |
51 | ## JWT Authorization
52 |
53 | ```mermaid
54 | sequenceDiagram
55 | User ->> Service: Login & Password
56 |
57 | Note right of Service: if one of creadentials
not valid
58 | Service--x User: 404 Bad request
59 | Note right of Service: if all creadentials
valid
60 | Service ->> User: 200 Ok with token
61 | ```
62 |
63 | ## Secure routes
64 | Get more about [Guards](https://docs.nestjs.com/guards)
65 | Two protection schemes have been implemented:
66 |
67 | 1. JWT Guard - protect access by validation token
68 | 2. Role Guard - protect access by user role
69 |
70 |
71 | ## Stay in touch
72 |
73 | [Alex Kotovsky](https://github.com/Kotovskyart)
74 |
75 | *2.11.2018*
76 |
77 |
--------------------------------------------------------------------------------
/config/default.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | PORT: 8080,
3 | HOST: 'http://localhost',
4 | MONGO_URI: '', // add mongo URI
5 | JWT_KEY: '', //add JWT secret key
6 | }
7 |
--------------------------------------------------------------------------------
/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": [
3 | "src"
4 | ],
5 | "ext": "ts",
6 | "ignore": [
7 | "src/**/*.spec.ts"
8 | ],
9 | "exec": "ts-node -r tsconfig-paths/register src/main.ts"
10 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nodejs-rest-api",
3 | "version": "1.2.0",
4 | "description": "description",
5 | "author": "",
6 | "license": "MIT",
7 | "scripts": {
8 | "format": "prettier --write \"**/*.ts\"",
9 | "start": "ts-node -r tsconfig-paths/register src/main.ts",
10 | "start:dev": "nodemon",
11 | "prestart:prod": "rm -rf dist && tsc",
12 | "start:prod": "node dist/main.js",
13 | "start:hmr": "node dist/server",
14 | "test": "jest",
15 | "test:cov": "jest --coverage",
16 | "test:e2e": "jest --config ./test/jest-e2e.json",
17 | "webpack": "webpack --config webpack.config.js"
18 | },
19 | "dependencies": {
20 | "@nestjs/common": "^5.4.0",
21 | "@nestjs/core": "^5.4.0",
22 | "@nestjs/mongoose": "^5.2.2",
23 | "@nestjs/passport": "^5.1.0",
24 | "@nestjs/swagger": "^2.5.1",
25 | "automapper-ts": "^1.9.0",
26 | "bcryptjs": "^2.4.3",
27 | "config": "^2.0.1",
28 | "fastify-formbody": "^2.0.0",
29 | "jsonwebtoken": "^8.3.0",
30 | "lodash": "^4.17.10",
31 | "mongoose": "^5.3.8",
32 | "passport": "^0.4.0",
33 | "passport-jwt": "^4.0.0",
34 | "reflect-metadata": "^0.1.12",
35 | "rxjs": "^6.3.3",
36 | "typegoose": "^5.4.1",
37 | "typescript": "^3.1.6"
38 | },
39 | "devDependencies": {
40 | "@nestjs/testing": "^5.4.0",
41 | "@types/bcryptjs": "^2.4.2",
42 | "@types/config": "0.0.34",
43 | "@types/express": "^4.16.0",
44 | "@types/jest": "^23.3.9",
45 | "@types/jsonwebtoken": "^8.3.0",
46 | "@types/lodash": "^4.14.117",
47 | "@types/mongoose": "^5.3.0",
48 | "@types/node": "^10.12.2",
49 | "@types/passport": "^0.4.7",
50 | "@types/passport-jwt": "^3.0.1",
51 | "@types/supertest": "^2.0.6",
52 | "jest": "^23.6.0",
53 | "nodemon": "^1.18.5",
54 | "prettier": "^1.14.3",
55 | "supertest": "^3.3.0",
56 | "ts-jest": "^23.10.4",
57 | "ts-loader": "^5.3.0",
58 | "ts-node": "^7.0.1",
59 | "tsconfig-paths": "^3.6.0",
60 | "tslint": "^5.11.0",
61 | "webpack": "^4.24.0",
62 | "webpack-cli": "^3.1.2",
63 | "webpack-node-externals": "^1.7.2"
64 | },
65 | "jest": {
66 | "moduleFileExtensions": [
67 | "js",
68 | "json",
69 | "ts"
70 | ],
71 | "rootDir": "src",
72 | "testRegex": ".spec.ts$",
73 | "transform": {
74 | "^.+\\.(t|j)s$": "ts-jest"
75 | },
76 | "coverageDirectory": "../coverage"
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/app.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { AppController } from './app.controller';
3 | import { AppService } from './app.service';
4 |
5 | describe('AppController', () => {
6 | let app: TestingModule;
7 |
8 | beforeAll(async () => {
9 | app = await Test.createTestingModule({
10 | controllers: [AppController],
11 | providers: [AppService],
12 | }).compile();
13 | });
14 |
15 | describe('root', () => {
16 | it('should return "Hello World!"', () => {
17 | const appController = app.get(AppController);
18 | expect(appController.root()).toBe('Hello World!');
19 | });
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/src/app.controller.ts:
--------------------------------------------------------------------------------
1 | import { Get, Controller } from '@nestjs/common';
2 | import { AppService } from './app.service';
3 |
4 | @Controller('root')
5 | export class AppController {
6 | constructor(private readonly appService: AppService) {}
7 | }
8 |
--------------------------------------------------------------------------------
/src/app.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common'
2 | import { AppController } from './app.controller'
3 | import { MongooseModule } from '@nestjs/mongoose'
4 | import { AppService } from './app.service'
5 | import { SharedModule } from './shared/shared.module'
6 | import { ConfigurationService } from './shared/configuration/configuration.service'
7 | import { Configuration } from './shared/configuration/configuration.enum'
8 | import { UserModule } from './user/user.module';
9 | import { TodoModule } from './todo/todo.module';
10 |
11 | @Module({
12 | imports: [
13 | SharedModule,
14 | MongooseModule.forRoot(ConfigurationService.connectionDB, {
15 | useNewUrlParser: true,
16 | autoIndex: false,
17 | }),
18 | UserModule,
19 | TodoModule
20 | ],
21 | controllers: [AppController],
22 | providers: [AppService],
23 | })
24 | export class AppModule {
25 | static host: string
26 | static port: string | number
27 | static isDev: boolean
28 |
29 | constructor(private readonly _configurationService: ConfigurationService) {
30 | AppModule.port = AppModule.normalizePort(_configurationService.get(Configuration.PORT))
31 | AppModule.host = _configurationService.get(Configuration.HOST)
32 | AppModule.isDev = _configurationService.isDevelopment
33 | }
34 |
35 | private static normalizePort(param: string | number): string | number {
36 | const portNumber: number = typeof param === 'string' ? parseInt(param, 10) : param
37 | if (isNaN(portNumber)) return param
38 | else if (portNumber >= 0) return portNumber
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/app.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 |
3 | @Injectable()
4 | export class AppService {}
5 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { NestFactory } from '@nestjs/core'
2 | import { AppModule } from './app.module'
3 | import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'
4 | import { HttpExceptionFilter } from './shared/filters/http-exception.filter'
5 |
6 | declare const module: any
7 |
8 | async function bootstrap() {
9 | const app = await NestFactory.create(AppModule)
10 | const hostDomain = AppModule.isDev ? `${AppModule.host}:${AppModule.port}` : AppModule.host
11 | const swaggerOptions = new DocumentBuilder()
12 | .setTitle('Nest API')
13 | .setDescription('API documentation')
14 | .setVersion('1.0.0')
15 | .setHost(hostDomain.split('//')[1])
16 | .setSchemes(AppModule.isDev ? 'http' : 'https')
17 | .setBasePath('/api') //localhost:8080/api
18 | .addBearerAuth('Autorization', 'header')
19 | .build()
20 | const swaggerDoc = SwaggerModule.createDocument(app, swaggerOptions)
21 |
22 | app.use('/api/docs/swagger.json', (req, res) => {
23 | res.send(swaggerDoc)
24 | })
25 |
26 | SwaggerModule.setup('/api/docs', app, null, {
27 | swaggerUrl: `${hostDomain}/api/docs/swagger.json`,
28 | explorer: true,
29 | swaggerOptions: {
30 | docExpansion: 'list',
31 | filter: true,
32 | showRequestDuration: true,
33 | },
34 | })
35 |
36 | if (module.hot) {
37 | module.hot.accept()
38 | module.hot.dispose(() => app.close())
39 | }
40 |
41 | app.setGlobalPrefix('api')
42 | app.useGlobalFilters(new HttpExceptionFilter())
43 | await app.listen(AppModule.port)
44 | }
45 |
46 | bootstrap()
47 |
--------------------------------------------------------------------------------
/src/shared/api-exception.model.ts:
--------------------------------------------------------------------------------
1 | import { ApiModelPropertyOptional } from '@nestjs/swagger'
2 |
3 | export class ApiException {
4 | @ApiModelPropertyOptional() statusCode?: number
5 | @ApiModelPropertyOptional() message?: string
6 | @ApiModelPropertyOptional() status?: string
7 | @ApiModelPropertyOptional() error?: string
8 | @ApiModelPropertyOptional() errors?: any
9 | @ApiModelPropertyOptional() timestamp?: string
10 | @ApiModelPropertyOptional() path?: string
11 | }
12 |
--------------------------------------------------------------------------------
/src/shared/auth/auth.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { AuthService } from './auth.service';
3 |
4 | describe('AuthService', () => {
5 | let service: AuthService;
6 | beforeAll(async () => {
7 | const module: TestingModule = await Test.createTestingModule({
8 | providers: [AuthService],
9 | }).compile();
10 | service = module.get(AuthService);
11 | });
12 | it('should be defined', () => {
13 | expect(service).toBeDefined();
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/src/shared/auth/auth.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, Inject, forwardRef } from '@nestjs/common'
2 | import { SignOptions, sign } from 'jsonwebtoken'
3 | import { UserService } from '../../user/user.service'
4 | import { ConfigurationService } from '../configuration/configuration.service'
5 | import { Configuration } from '../configuration/configuration.enum'
6 | import { JwtPayload } from './jwt-payload.interface'
7 | import { InstanceType } from 'typegoose'
8 | import { User } from '../../user/models/user.model'
9 |
10 | @Injectable()
11 | export class AuthService {
12 | private readonly jwtOptions: SignOptions
13 | private readonly jwtKey: string
14 |
15 | constructor(
16 | @Inject(forwardRef(() => UserService)) private readonly _userService: UserService,
17 | private readonly _configurationService: ConfigurationService,
18 | ) {
19 | this.jwtOptions = { expiresIn: '12h' }
20 | this.jwtKey = _configurationService.get(Configuration.JWT_KEY)
21 | }
22 |
23 | async signPayload(payload: JwtPayload): Promise {
24 | return await sign(payload, this.jwtKey, this.jwtOptions)
25 | }
26 |
27 | async validatePayload(payload: JwtPayload): Promise> {
28 | return await this._userService.findOne({ username: payload.username.toLowerCase() })
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/shared/auth/jwt-payload.interface.ts:
--------------------------------------------------------------------------------
1 | import { UserRole } from '../../user/models/user-role.enum'
2 |
3 | export interface JwtPayload {
4 | username: string
5 | role: UserRole
6 | iat?: Date
7 | }
8 |
--------------------------------------------------------------------------------
/src/shared/auth/strategies/jwt-strategy.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { JwtStrategyService } from './jwt-strategy.service';
3 |
4 | describe('JwtStrategyService', () => {
5 | let service: JwtStrategyService;
6 | beforeAll(async () => {
7 | const module: TestingModule = await Test.createTestingModule({
8 | providers: [JwtStrategyService],
9 | }).compile();
10 | service = module.get(JwtStrategyService);
11 | });
12 | it('should be defined', () => {
13 | expect(service).toBeDefined();
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/src/shared/auth/strategies/jwt-strategy.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, HttpException, HttpStatus } from '@nestjs/common'
2 | import { PassportStrategy } from '@nestjs/passport'
3 | import { ExtractJwt, Strategy, VerifiedCallback } from 'passport-jwt'
4 | import { AuthService } from '../auth.service'
5 | import { ConfigurationService } from '../../configuration/configuration.service'
6 | import { Configuration } from '../../configuration/configuration.enum'
7 | import { JwtPayload } from '../jwt-payload.interface'
8 |
9 | @Injectable()
10 | export class JwtStrategyService extends PassportStrategy(Strategy) {
11 | constructor(
12 | private readonly _authService: AuthService,
13 | private readonly _configurationService: ConfigurationService,
14 | ) {
15 | super({
16 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
17 | secretOrKey: _configurationService.get(Configuration.JWT_KEY),
18 | })
19 | }
20 |
21 | async validate(payload: JwtPayload, done: VerifiedCallback) {
22 | const user = this._authService.validatePayload(payload)
23 |
24 | if (!user) {
25 | return done(new HttpException({}, HttpStatus.UNAUTHORIZED ), false )
26 | } else {
27 | done(null, user, payload.iat)
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/shared/base.model.ts:
--------------------------------------------------------------------------------
1 | import { SchemaOptions } from 'mongoose'
2 | import { ApiModelPropertyOptional } from '@nestjs/swagger'
3 | import { pre, prop, Typegoose } from 'typegoose'
4 |
5 | @pre('findOneAndUpdate', function(next) {
6 | this._update.updatedAt = new Date(Date.now())
7 | next()
8 | })
9 | export class BaseModel extends Typegoose {
10 |
11 | @prop({ default: Date.now() })
12 | createdAt?: Date
13 | @prop({ default: Date.now() })
14 | updatedAt?: Date
15 |
16 | id?: string
17 | }
18 |
19 | export class BaseModelVM {
20 | @ApiModelPropertyOptional({ type: String, format: 'date-time' })
21 | createdAt?: Date
22 |
23 | @ApiModelPropertyOptional({ type: String, format: 'date-time' })
24 | updatedAt?: Date
25 |
26 | @ApiModelPropertyOptional()
27 | id?: string
28 | }
29 |
30 | export const schemaOptions: SchemaOptions = {
31 | toJSON: {
32 | virtuals: true,
33 | getters: true,
34 | },
35 | }
36 |
--------------------------------------------------------------------------------
/src/shared/base.service.ts:
--------------------------------------------------------------------------------
1 | import 'automapper-ts/dist/automapper'
2 | import { Types } from 'mongoose'
3 | import { ModelType, InstanceType, Typegoose } from 'typegoose'
4 |
5 | export abstract class BaseService {
6 |
7 | protected _model: ModelType
8 | protected _mapper: AutoMapperJs.AutoMapper
9 |
10 | private get modelName(): string {
11 | return this._model.modelName
12 | }
13 |
14 | private get viewModelName(): string {
15 | return `${this._model.modelName}Vm`
16 | }
17 |
18 | async map(
19 | object: Partial> | Partial>[],
20 | isArray: boolean = false,
21 | sourceKey?: string,
22 | destinationKey?: string,
23 | ): Promise {
24 |
25 | const _sourceKey = isArray
26 | ? `${ sourceKey || this.modelName }[]`
27 | : sourceKey || this.modelName
28 |
29 | const _destinationKey = isArray
30 | ? `${ destinationKey || this.viewModelName }[]`
31 | : destinationKey || this.viewModelName
32 |
33 | return this._mapper.map(_sourceKey, _destinationKey, object)
34 | }
35 |
36 | async findAll(filter = {}): Promise[]> {
37 | return this._model.find(filter).exec()
38 | }
39 |
40 | async findOne(filter = {}): Promise> {
41 | return this._model.findOne(filter).exec()
42 | }
43 |
44 | async findById(id: string): Promise> {
45 | return this._model.findById(this.toObjectId(id)).exec()
46 | }
47 |
48 | async create(item: InstanceType): Promise> {
49 | return this._model.create(item)
50 | }
51 |
52 | async delete(id: string): Promise> {
53 | return this._model.findByIdAndRemove(this.toObjectId(id))
54 | }
55 |
56 | async update(id: string, item: InstanceType) {
57 | return this._model.findByIdAndUpdate(this.toObjectId(id), item, {new: true}).exec()
58 | }
59 |
60 | async clearCollection(filter = {}) {
61 | return this._model.deleteMany(filter).exec()
62 | }
63 |
64 | private toObjectId(id: string): Types.ObjectId {
65 | return Types.ObjectId(id)
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/shared/configuration/configuration.enum.ts:
--------------------------------------------------------------------------------
1 | export enum Configuration {
2 | HOST = 'HOST',
3 | PORT = 'PORT',
4 | MONGO_URI = 'MONGO_URI',
5 | JWT_KEY = 'JWT_KEY',
6 | }
7 |
--------------------------------------------------------------------------------
/src/shared/configuration/configuration.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { ConfigurationService } from './configuration.service';
3 |
4 | describe('ConfigurationService', () => {
5 | let service: ConfigurationService;
6 | beforeAll(async () => {
7 | const module: TestingModule = await Test.createTestingModule({
8 | providers: [ConfigurationService],
9 | }).compile();
10 | service = module.get(ConfigurationService);
11 | });
12 | it('should be defined', () => {
13 | expect(service).toBeDefined();
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/src/shared/configuration/configuration.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common'
2 | import { Configuration } from './configuration.enum'
3 | import { get } from 'config'
4 |
5 | @Injectable()
6 | export class ConfigurationService {
7 |
8 | static connectionDB: string = process.env[Configuration.MONGO_URI] || get(Configuration.MONGO_URI)
9 | private environmentHosting: string = process.env.NODE_ENV || 'development'
10 |
11 | get(name: string): string {
12 | return process.env[name] || get(name)
13 | }
14 |
15 | get isDevelopment(): boolean {
16 | return this.environmentHosting === 'development'
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/shared/decorators/roles.decorator.ts:
--------------------------------------------------------------------------------
1 | import { UserRole } from '../../user/models/user-role.enum'
2 | import { ReflectMetadata } from '@nestjs/common'
3 |
4 | export const Roles = (...roles: UserRole[]) => ReflectMetadata('roles', roles)
5 |
--------------------------------------------------------------------------------
/src/shared/filters/http-exception.filter.ts:
--------------------------------------------------------------------------------
1 | import { ArgumentsHost, Catch, ExceptionFilter, HttpStatus } from '@nestjs/common'
2 |
3 | @Catch()
4 | export class HttpExceptionFilter implements ExceptionFilter {
5 | catch(error: any, host: ArgumentsHost): any {
6 | const ctx = host.switchToHttp()
7 | const req = ctx.getRequest()
8 | const res = ctx.getResponse()
9 |
10 | if (error.getStatus() === HttpStatus.UNAUTHORIZED) {
11 | if (typeof error.response !== 'string') {
12 | error.response['message'] =
13 | error.response.message || 'You do not have permission to access this resource'
14 | }
15 | }
16 |
17 | res
18 | .status(error.getStatus())
19 | .json({
20 | statusCode: error.getStatus(),
21 | error: error.response.name || error.name,
22 | message: error.response.message || error.message,
23 | errors: error.response.errors || null,
24 | timestamp: new Date().toISOString(),
25 | path: req ? req.url : null
26 | })
27 |
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/src/shared/guards/roles.guard.ts:
--------------------------------------------------------------------------------
1 | import { CanActivate, ExecutionContext, HttpException, HttpStatus, Injectable } from '@nestjs/common'
2 | import { Reflector } from '@nestjs/core'
3 | import { InstanceType } from 'typegoose'
4 | import { User } from '../../user/models/user.model'
5 |
6 | @Injectable()
7 | export class RolesGuard implements CanActivate {
8 | constructor(private readonly _reflector: Reflector) {}
9 |
10 | canActivate(context: ExecutionContext): boolean {
11 | const roles = this._reflector.get('roles', context.getHandler())
12 |
13 | // @ts-ignore
14 | if (!roles || roles.length === 0) {
15 | return true
16 | }
17 |
18 | console.log(roles)
19 |
20 | const request = context.switchToHttp().getRequest()
21 | const user: InstanceType = request.user
22 |
23 | // @ts-ignore
24 | const hasRole = () => roles.includes(user.role);
25 |
26 | if (user && user.role && hasRole())
27 | return true
28 |
29 | throw new HttpException(`Operation is permitted with your role - ${user.role}`, HttpStatus.UNAUTHORIZED)
30 |
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/shared/mapper/mapper.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { MapperService } from './mapper.service';
3 |
4 | describe('MapperService', () => {
5 | let service: MapperService;
6 | beforeAll(async () => {
7 | const module: TestingModule = await Test.createTestingModule({
8 | providers: [MapperService],
9 | }).compile();
10 | service = module.get(MapperService);
11 | });
12 | it('should be defined', () => {
13 | expect(service).toBeDefined();
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/src/shared/mapper/mapper.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import 'automapper-ts/dist/automapper'
3 |
4 | @Injectable()
5 | export class MapperService {
6 |
7 | mapper: AutoMapperJs.AutoMapper
8 |
9 | constructor() {
10 | this.mapper = automapper
11 | this.initializeMapper()
12 | }
13 |
14 | private initializeMapper(): void {
15 | this.mapper.initialize(MapperService.configure)
16 | }
17 |
18 | private static configure(config: AutoMapperJs.IConfiguration): void {
19 | config
20 | .createMap('User', 'UserVm')
21 | .forSourceMember('_id', opts => opts.ignore())
22 | .forSourceMember('password', opts => opts.ignore())
23 |
24 | config
25 | .createMap('Todo', 'TodoVm')
26 | .forSourceMember('_id', opts => opts.ignore())
27 |
28 | config
29 | .createMap('Todo[]', 'TodoVm[]')
30 | .forSourceMember('_id', opts => opts.ignore())
31 |
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/shared/pipes/to-boolean.pipe.ts:
--------------------------------------------------------------------------------
1 | import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common'
2 |
3 | @Injectable()
4 | export class ToBooleanPipe implements PipeTransform {
5 | transform(value: any, { type, metatype }: ArgumentMetadata) {
6 | if (type === 'query' && metatype === Boolean)
7 | return value ? value === 'true' : null
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/shared/shared.module.ts:
--------------------------------------------------------------------------------
1 | import { Global, Module } from '@nestjs/common';
2 | import { ConfigurationService } from './configuration/configuration.service';
3 | import { MapperService } from './mapper/mapper.service';
4 | import { AuthService } from './auth/auth.service';
5 | import { JwtStrategyService } from './auth/strategies/jwt-strategy.service';
6 | import { UserModule } from '../user/user.module'
7 |
8 | @Global()
9 | @Module({
10 | imports: [UserModule],
11 | providers: [ConfigurationService, MapperService, AuthService, JwtStrategyService],
12 | exports: [ConfigurationService, MapperService, AuthService],
13 | })
14 | export class SharedModule {}
15 |
--------------------------------------------------------------------------------
/src/shared/utilities/enum-to-array.ts:
--------------------------------------------------------------------------------
1 | export function EnumToArray(enumVariable: any): string[] {
2 | return Object.keys(enumVariable).map(key => enumVariable[key])
3 | }
4 |
--------------------------------------------------------------------------------
/src/shared/utilities/get-operation-id.ts:
--------------------------------------------------------------------------------
1 | export function GetOperationId(model:string, operation: string) {
2 | const _model = ToTitleCase(model).replace(/\s/g, '')
3 | const _operation = ToTitleCase(operation).replace(/\s/g, '')
4 |
5 | return {
6 | title: '',
7 | operation: `${_model}_${_operation}`
8 | }
9 | }
10 |
11 | function ToTitleCase(str: string): string {
12 | return str.toLowerCase().split(' ').map(word => word.replace(word[0], word[0].toUpperCase())).join(' ')
13 | }
14 |
--------------------------------------------------------------------------------
/src/todo/models/todo-level.enum.ts:
--------------------------------------------------------------------------------
1 | export enum TodoLevel {
2 | Low = 'Low',
3 | Normal = 'Normal',
4 | High = 'High',
5 | }
6 |
--------------------------------------------------------------------------------
/src/todo/models/todo.models.ts:
--------------------------------------------------------------------------------
1 | import { BaseModel, schemaOptions } from '../../shared/base.model'
2 | import { TodoLevel } from './todo-level.enum'
3 | import { ModelType, prop } from 'typegoose'
4 | import { TodoParams } from './veiw-models/todo-params.model'
5 |
6 | export class Todo extends BaseModel{
7 | @prop({ required: [true, 'Content is required'] })
8 | content: string
9 |
10 | @prop({ enum: TodoLevel, default: TodoLevel.Normal })
11 | level: TodoLevel
12 |
13 | @prop({ default: false })
14 | isCompleted: boolean
15 |
16 | static get model(): ModelType {
17 | return new Todo().getModelForClass(Todo, { schemaOptions })
18 | }
19 |
20 | static get modelName(): string {
21 | return this.model.modelName
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/todo/models/veiw-models/todo-params.model.ts:
--------------------------------------------------------------------------------
1 | import { TodoLevel } from '../todo-level.enum'
2 | import { ApiModelProperty, ApiModelPropertyOptional } from '@nestjs/swagger'
3 |
4 | export class TodoParams {
5 | @ApiModelProperty() content: string
6 | @ApiModelPropertyOptional({ enum: TodoLevel, example: TodoLevel.Normal })
7 | level?: TodoLevel
8 | }
9 |
--------------------------------------------------------------------------------
/src/todo/models/veiw-models/todo-vm.model.ts:
--------------------------------------------------------------------------------
1 | import { BaseModelVM } from '../../../shared/base.model'
2 | import { TodoLevel } from '../todo-level.enum'
3 | import { ApiModelProperty } from '@nestjs/swagger'
4 | import { EnumToArray } from '../../../shared/utilities/enum-to-array'
5 |
6 | export class TodoVm extends BaseModelVM{
7 | @ApiModelProperty() content: string
8 | @ApiModelProperty({ enum: EnumToArray(TodoLevel) }) level: TodoLevel
9 | @ApiModelProperty() isCompleted: boolean
10 | }
11 |
--------------------------------------------------------------------------------
/src/todo/todo.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { TodoController } from './todo.controller';
3 |
4 | describe('Todo Controller', () => {
5 | let module: TestingModule;
6 | beforeAll(async () => {
7 | module = await Test.createTestingModule({
8 | controllers: [TodoController],
9 | }).compile();
10 | });
11 | it('should be defined', () => {
12 | const controller: TodoController = module.get(TodoController);
13 | expect(controller).toBeDefined();
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/src/todo/todo.controller.ts:
--------------------------------------------------------------------------------
1 | import { Body, Controller, Delete, Get, HttpException, HttpStatus, Param, Post, Put, Query, UseGuards } from '@nestjs/common'
2 | import { ApiBearerAuth, ApiImplicitQuery, ApiOperation, ApiResponse, ApiUseTags } from '@nestjs/swagger'
3 | import { Todo } from './models/todo.models'
4 | import { TodoService } from './todo.service'
5 | import { TodoVm } from './models/veiw-models/todo-vm.model'
6 | import { TodoParams } from './models/veiw-models/todo-params.model'
7 | import { ApiException } from '../shared/api-exception.model'
8 | import { GetOperationId } from '../shared/utilities/get-operation-id'
9 | import * as _ from 'lodash'
10 | import { TodoLevel } from './models/todo-level.enum'
11 | import { isArray } from 'util'
12 | import { ToBooleanPipe } from '../shared/pipes/to-boolean.pipe'
13 | import { RolesGuard } from '../shared/guards/roles.guard'
14 | import { Roles } from '../shared/decorators/roles.decorator'
15 | import { UserRole } from '../user/models/user-role.enum'
16 | import { AuthGuard } from '@nestjs/passport'
17 |
18 | @ApiUseTags(Todo.modelName)
19 | @ApiBearerAuth()
20 | @Controller('todos')
21 | export class TodoController {
22 | constructor(
23 | private readonly _todoService: TodoService,
24 | ) {}
25 |
26 | @Post()
27 | @Roles(UserRole.Admin)
28 | @UseGuards(AuthGuard('jwt'), RolesGuard)
29 | @ApiResponse({status: HttpStatus.CREATED, type: TodoVm})
30 | @ApiResponse({status: HttpStatus.BAD_REQUEST, type: ApiException})
31 | @ApiOperation(GetOperationId(Todo.modelName, 'Create'))
32 | async create(@Body() params: TodoParams): Promise {
33 | const { content } = params
34 |
35 | if (!content) {
36 | throw new HttpException('Content is required', HttpStatus.BAD_REQUEST)
37 | }
38 |
39 | try {
40 | const newTodo = await this._todoService.createTodo(params)
41 | return this._todoService.map(newTodo)
42 | } catch (e) {
43 | throw new HttpException(e, HttpStatus.INTERNAL_SERVER_ERROR)
44 | }
45 | }
46 |
47 | @Get()
48 | @Roles(UserRole.Admin, UserRole.User)
49 | @UseGuards(AuthGuard('jwt'), RolesGuard)
50 | @ApiResponse({status: HttpStatus.OK, type: TodoVm, isArray: true })
51 | @ApiResponse({status: HttpStatus.BAD_REQUEST, type: ApiException})
52 | @ApiOperation(GetOperationId(Todo.modelName, 'Get todos'))
53 | @ApiImplicitQuery({ name: 'level', required: false, isArray: true, collectionFormat: 'multi', description: 'Filter by level option' })
54 | @ApiImplicitQuery({ name: 'isCompleted', required: false, description: 'Filter by isCompleted option' })
55 | async get(
56 | @Query('level') level?: TodoLevel,
57 | @Query('isCompleted', new ToBooleanPipe()) isCompleted?: boolean,
58 | ): Promise {
59 | let filter = {}
60 |
61 | if (level)
62 | filter['level'] = { $in: isArray(level) ? [...level] : [level]}
63 |
64 | if (isCompleted !== null) {
65 | if (filter['level']) {
66 | filter = { $and: [{ level: filter['level'] }, { isCompleted }] }
67 | } else {
68 | filter['isCompleted'] = isCompleted
69 | }
70 | }
71 |
72 | try {
73 | const todos = await this._todoService.findAll(filter)
74 | return this._todoService.map(_.map(todos, todo => todo.toJSON()), true)
75 | } catch (e) {
76 | throw new HttpException(e, HttpStatus.INTERNAL_SERVER_ERROR)
77 | }
78 | }
79 |
80 | @Put()
81 | @Roles(UserRole.Admin)
82 | @UseGuards(AuthGuard('jwt'), RolesGuard)
83 | @ApiResponse({status: HttpStatus.CREATED, type: TodoVm})
84 | @ApiResponse({status: HttpStatus.BAD_REQUEST, type: ApiException})
85 | @ApiOperation(GetOperationId(Todo.modelName, 'Update'))
86 | async update(@Body() todoVm: TodoVm): Promise {
87 | const { id, content, level, isCompleted } = todoVm
88 |
89 | if (!todoVm || !id)
90 | throw new HttpException('Missing parameters', HttpStatus.BAD_REQUEST)
91 |
92 | const exists = await this._todoService.findById(id)
93 |
94 | if (!exists)
95 | throw new HttpException('Todo record does not exist', HttpStatus.NOT_FOUND)
96 |
97 | if (exists.isCompleted)
98 | throw new HttpException('Already completed', HttpStatus.CONFLICT)
99 |
100 | exists.content = content
101 | exists.isCompleted = isCompleted
102 | exists.level = level
103 |
104 | try {
105 | const updated = await this._todoService.update(id, exists)
106 | return this._todoService.map(updated.toJSON())
107 | } catch (e) {
108 | throw new HttpException(e, HttpStatus.INTERNAL_SERVER_ERROR)
109 | }
110 | }
111 |
112 | @Delete(':id')
113 | @Roles(UserRole.Admin)
114 | @UseGuards(AuthGuard('jwt'), RolesGuard)
115 | @ApiResponse({status: HttpStatus.OK, type: TodoVm})
116 | @ApiResponse({status: HttpStatus.BAD_REQUEST, type: ApiException})
117 | @ApiOperation(GetOperationId(Todo.modelName, 'Delete'))
118 | async delete(@Param('id') id: string): Promise {
119 |
120 | const exists = await this._todoService.findById(id)
121 |
122 | if (!exists)
123 | throw new HttpException('Todo\'s id not found', HttpStatus.NOT_FOUND)
124 |
125 | try {
126 | const deleted = await this._todoService.delete(id)
127 |
128 | return this._todoService.map(deleted.toJSON())
129 | } catch (e) {
130 | throw new HttpException(e, HttpStatus.INTERNAL_SERVER_ERROR)
131 | }
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/src/todo/todo.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { MongooseModule } from '@nestjs/mongoose'
3 | import { TodoService } from './todo.service';
4 | import { TodoController } from './todo.controller';
5 | import { Todo } from './models/todo.models'
6 |
7 | @Module({
8 | imports: [
9 | MongooseModule.forFeature([{name: Todo.modelName, schema: Todo.model.schema}]),
10 | ],
11 | providers: [TodoService],
12 | controllers: [TodoController]
13 | })
14 | export class TodoModule {}
15 |
--------------------------------------------------------------------------------
/src/todo/todo.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { TodoService } from './todo.service';
3 |
4 | describe('TodoService', () => {
5 | let service: TodoService;
6 | beforeAll(async () => {
7 | const module: TestingModule = await Test.createTestingModule({
8 | providers: [TodoService],
9 | }).compile();
10 | service = module.get(TodoService);
11 | });
12 | it('should be defined', () => {
13 | expect(service).toBeDefined();
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/src/todo/todo.service.ts:
--------------------------------------------------------------------------------
1 | import { HttpException, HttpStatus, Injectable } from '@nestjs/common'
2 | import { BaseService } from '../shared/base.service'
3 | import { Todo } from './models/todo.models'
4 | import { InjectModel } from '@nestjs/mongoose'
5 | import { ModelType } from 'typegoose'
6 | import { MapperService } from '../shared/mapper/mapper.service'
7 | import { TodoParams } from './models/veiw-models/todo-params.model'
8 |
9 | @Injectable()
10 | export class TodoService extends BaseService{
11 | constructor(
12 | @InjectModel(Todo.modelName) private readonly _todoModel: ModelType,
13 | private readonly _mapperService: MapperService
14 | ) {
15 | super()
16 | this._model = _todoModel
17 | this._mapper = _mapperService.mapper
18 | }
19 |
20 | async createTodo(params: TodoParams): Promise {
21 | const { content, level } = params
22 |
23 | const newTodo = new this._model()
24 |
25 | newTodo.content = content
26 |
27 | if (level) {
28 | newTodo.level = level
29 | }
30 |
31 | try {
32 | const result = await this.create(newTodo)
33 | return result.toJSON() as Todo
34 | } catch (e) {
35 | throw new HttpException(e, HttpStatus.INTERNAL_SERVER_ERROR)
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/user/models/user-role.enum.ts:
--------------------------------------------------------------------------------
1 | export enum UserRole {
2 | Admin = 'Admin',
3 | User = 'User',
4 | }
5 |
--------------------------------------------------------------------------------
/src/user/models/user.model.ts:
--------------------------------------------------------------------------------
1 | import { BaseModel, schemaOptions } from '../../shared/base.model'
2 | import { UserRole } from './user-role.enum'
3 | import { ModelType, prop } from 'typegoose'
4 |
5 | export class User extends BaseModel {
6 | @prop({
7 | required: [true, 'Username is required'],
8 | minlength: [6, 'Username must be at least 6 characters'],
9 | unique: true,
10 | })
11 | username: string
12 |
13 | @prop({
14 | required: [true, 'Password is required'],
15 | minlength: [6, 'Password must be at least 6 characters']
16 | })
17 | password: string
18 |
19 | @prop({ enum: UserRole, default: UserRole.User })
20 | role?: UserRole
21 |
22 | @prop() firstName?: string
23 | @prop() lastName?: string
24 |
25 | @prop() get fullName(): string {
26 | return `${this.firstName} ${this.lastName}`
27 | }
28 |
29 | static get model(): ModelType {
30 | return new User().getModelForClass(User, { schemaOptions })
31 | }
32 |
33 | static get modelName(): string {
34 | return this.model.modelName
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/user/models/view-models/login-response-vm.model.ts:
--------------------------------------------------------------------------------
1 | import { UserVm } from './user-vm.model'
2 | import { ApiModelProperty } from '@nestjs/swagger'
3 |
4 | export class LoginResponseVm {
5 | @ApiModelProperty() token: string
6 | @ApiModelProperty() user: UserVm
7 | }
8 |
--------------------------------------------------------------------------------
/src/user/models/view-models/login-vm.model.ts:
--------------------------------------------------------------------------------
1 | import { ApiModelProperty } from '@nestjs/swagger'
2 |
3 | export class LoginVm {
4 | @ApiModelProperty() username: string
5 | @ApiModelProperty() password: string
6 | }
7 |
--------------------------------------------------------------------------------
/src/user/models/view-models/register-vm.model.ts:
--------------------------------------------------------------------------------
1 | import { LoginVm } from './login-vm.model'
2 | import { ApiModelPropertyOptional } from '@nestjs/swagger'
3 |
4 | export class RegisterVm extends LoginVm {
5 | @ApiModelPropertyOptional() firstName: string
6 | @ApiModelPropertyOptional() lastName: string
7 | }
8 |
--------------------------------------------------------------------------------
/src/user/models/view-models/user-vm.model.ts:
--------------------------------------------------------------------------------
1 | import { BaseModel } from '../../../shared/base.model'
2 | import { UserRole } from '../user-role.enum'
3 | import { ApiModelProperty, ApiModelPropertyOptional } from '@nestjs/swagger'
4 | import { EnumToArray } from '../../../shared/utilities/enum-to-array'
5 | import { User } from '../user.model'
6 |
7 | export class UserVm extends BaseModel {
8 | @ApiModelProperty() username: string
9 | @ApiModelPropertyOptional() firstName?: string
10 | @ApiModelPropertyOptional() lastName?: string
11 | @ApiModelPropertyOptional() fullName?: string
12 | @ApiModelPropertyOptional({ enum: EnumToArray(UserRole)}) role?: UserRole
13 | }
14 |
--------------------------------------------------------------------------------
/src/user/user.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { UserController } from './user.controller';
3 |
4 | describe('User Controller', () => {
5 | let module: TestingModule;
6 | beforeAll(async () => {
7 | module = await Test.createTestingModule({
8 | controllers: [UserController],
9 | }).compile();
10 | });
11 | it('should be defined', () => {
12 | const controller: UserController = module.get(UserController);
13 | expect(controller).toBeDefined();
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/src/user/user.controller.ts:
--------------------------------------------------------------------------------
1 | import { Body, Controller, HttpException, HttpStatus, Post } from '@nestjs/common'
2 | import { ApiUseTags, ApiResponse, ApiOperation } from '@nestjs/swagger'
3 | import { User } from './models/user.model'
4 | import { UserService } from './user.service'
5 | import { UserVm } from './models/view-models/user-vm.model'
6 | import { ApiException } from '../shared/api-exception.model'
7 | import { GetOperationId } from '../shared/utilities/get-operation-id'
8 | import { RegisterVm } from './models/view-models/register-vm.model'
9 | import { LoginResponseVm } from './models/view-models/login-response-vm.model'
10 | import { LoginVm } from './models/view-models/login-vm.model'
11 |
12 | @ApiUseTags(User.modelName)
13 | @Controller('users')
14 | export class UserController {
15 | constructor(
16 | private readonly _userService: UserService,
17 | ) {}
18 |
19 | @Post('register')
20 | @ApiResponse({ status: HttpStatus.CREATED, type: UserVm })
21 | @ApiResponse({ status: HttpStatus.BAD_REQUEST, type: ApiException })
22 | @ApiOperation(GetOperationId(User.modelName, 'Register'))
23 | async register(@Body() registerVm: RegisterVm): Promise {
24 | const { username, password } = registerVm
25 |
26 | if (!username) {
27 | throw new HttpException('Username is required', HttpStatus.BAD_REQUEST)
28 | }
29 |
30 | if (!password) {
31 | throw new HttpException('Password is required', HttpStatus.BAD_REQUEST)
32 | }
33 |
34 | let exists
35 |
36 | try {
37 | exists = await this._userService.findOne({ username })
38 | } catch (e) {
39 | throw new HttpException(e, HttpStatus.INTERNAL_SERVER_ERROR)
40 | }
41 |
42 | if (exists) {
43 | throw new HttpException(`${username} exists yet`, HttpStatus.BAD_REQUEST)
44 | }
45 |
46 | const newUser = await this._userService.register(registerVm)
47 |
48 | return this._userService.map(newUser)
49 | }
50 |
51 | @Post('login')
52 | @ApiResponse({ status: HttpStatus.OK, type: LoginResponseVm })
53 | @ApiResponse({ status: HttpStatus.BAD_REQUEST, type: ApiException })
54 | @ApiOperation(GetOperationId(User.modelName, 'Login'))
55 | async login(@Body() loginVm: LoginVm): Promise {
56 | const fields = Object.keys(loginVm)
57 |
58 | fields.forEach(field => {
59 | if (!loginVm[field]) {
60 | throw new HttpException(`${field} is required`, HttpStatus.BAD_REQUEST)
61 | }
62 | })
63 |
64 | return this._userService.login(loginVm)
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/user/user.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { MongooseModule } from '@nestjs/mongoose'
3 | import { UserController } from './user.controller';
4 | import { UserService } from './user.service';
5 | import { User } from './models/user.model'
6 | import { SharedModule } from '../shared/shared.module'
7 |
8 | @Module({
9 | imports: [
10 | MongooseModule.forFeature([{name: User.modelName, schema: User.model.schema}]),
11 | ],
12 | controllers: [UserController],
13 | providers: [UserService],
14 | exports: [UserService],
15 | })
16 | export class UserModule {}
17 |
--------------------------------------------------------------------------------
/src/user/user.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { UserService } from './user.service';
3 |
4 | describe('UserService', () => {
5 | let service: UserService;
6 | beforeAll(async () => {
7 | const module: TestingModule = await Test.createTestingModule({
8 | providers: [UserService],
9 | }).compile();
10 | service = module.get(UserService);
11 | });
12 | it('should be defined', () => {
13 | expect(service).toBeDefined();
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/src/user/user.service.ts:
--------------------------------------------------------------------------------
1 | import { HttpException, HttpStatus, Inject, Injectable, forwardRef } from '@nestjs/common'
2 | import { BaseService } from '../shared/base.service'
3 | import { User } from './models/user.model'
4 | import { ModelType } from 'typegoose'
5 | import { InjectModel } from '@nestjs/mongoose'
6 | import { MapperService } from '../shared/mapper/mapper.service'
7 | import { RegisterVm } from './models/view-models/register-vm.model'
8 | import { compare, genSalt, hash } from 'bcryptjs'
9 | import { LoginVm } from './models/view-models/login-vm.model'
10 | import { LoginResponseVm } from './models/view-models/login-response-vm.model'
11 | import { JwtPayload } from '../shared/auth/jwt-payload.interface'
12 | import { AuthService } from '../shared/auth/auth.service'
13 | import { UserVm } from './models/view-models/user-vm.model'
14 |
15 | @Injectable()
16 | export class UserService extends BaseService{
17 | constructor(
18 | @InjectModel(User.modelName) private readonly _userModel: ModelType,
19 | private readonly _mapperService: MapperService,
20 | @Inject(forwardRef(() => AuthService)) private readonly _authService: AuthService,
21 | ) {
22 | super()
23 | this._model = _userModel
24 | this._mapper = _mapperService.mapper
25 | }
26 |
27 | async login(loginVm: LoginVm): Promise {
28 | const { username, password } = loginVm
29 |
30 | const user = await this.findOne({ username })
31 |
32 | if (!user) {
33 | throw new HttpException('Bad credentials', HttpStatus.BAD_REQUEST)
34 | }
35 |
36 | const isPasswordMatch = await compare(password, user.password)
37 |
38 | if (!isPasswordMatch) {
39 | throw new HttpException('Bad credentials', HttpStatus.BAD_REQUEST)
40 | }
41 |
42 | const payload: JwtPayload = {
43 | username: user.username,
44 | role: user.role,
45 | }
46 |
47 | const token = await this._authService.signPayload(payload)
48 | const userVm = await this.map(user.toJSON())
49 |
50 | return {
51 | token: token,
52 | user: userVm
53 | }
54 | }
55 |
56 | async register(registerVm: RegisterVm): Promise {
57 | const { username, password, firstName, lastName } = registerVm
58 |
59 | const newUser = new this._model()
60 | newUser.username = username
61 | newUser.firstName = firstName
62 | newUser.lastName = lastName
63 |
64 | const salt = await genSalt(16)
65 |
66 | newUser.password = await hash(password, salt)
67 |
68 | try {
69 | const result = await this.create(newUser)
70 | return result.toJSON() as User
71 | } catch (e) {
72 | throw new HttpException(e, HttpStatus.INTERNAL_SERVER_ERROR)
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/test/app.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import request from 'supertest';
2 | import { Test } from '@nestjs/testing';
3 | import { AppModule } from './../src/app.module';
4 | import { INestApplication } from '@nestjs/common';
5 |
6 | describe('AppController (e2e)', () => {
7 | let app: INestApplication;
8 |
9 | beforeAll(async () => {
10 | const moduleFixture = await Test.createTestingModule({
11 | imports: [AppModule],
12 | }).compile();
13 |
14 | app = moduleFixture.createNestApplication();
15 | await app.init();
16 | });
17 |
18 | it('/GET /', () => {
19 | return request(app.getHttpServer())
20 | .get('/')
21 | .expect(200)
22 | .expect('Hello World!');
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/test/jest-e2e.json:
--------------------------------------------------------------------------------
1 | {
2 | "moduleFileExtensions": ["js", "json", "ts"],
3 | "rootDir": ".",
4 | "testRegex": ".e2e-spec.ts$",
5 | "transform": {
6 | "^.+\\.(t|j)s$": "ts-jest"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "declaration": false,
5 | "noImplicitAny": false,
6 | "removeComments": true,
7 | "noLib": false,
8 | "allowSyntheticDefaultImports": true,
9 | "emitDecoratorMetadata": true,
10 | "experimentalDecorators": true,
11 | "target": "es6",
12 | "sourceMap": true,
13 | "allowJs": true,
14 | "outDir": "./dist",
15 | "baseUrl": "./src"
16 | },
17 | "include": [
18 | "src/**/*"
19 | ],
20 | "exclude": [
21 | "node_modules",
22 | "**/*.spec.ts"
23 | ]
24 | }
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "defaultSeverity": "error",
3 | "extends": [
4 | "tslint:recommended"
5 | ],
6 | "jsRules": {
7 | "no-unused-expression": true
8 | },
9 | "rules": {
10 | "eofline": false,
11 | "quotemark": [
12 | true,
13 | "single"
14 | ],
15 | "semicolon": false,
16 | "indent": false,
17 | "member-access": [
18 | false
19 | ],
20 | "ordered-imports": [
21 | false
22 | ],
23 | "max-line-length": [
24 | true,
25 | 150
26 | ],
27 | "member-ordering": [
28 | false
29 | ],
30 | "curly": false,
31 | "interface-name": [
32 | false
33 | ],
34 | "array-type": [
35 | false
36 | ],
37 | "comment-format": false,
38 | "no-empty-interface": false,
39 | "no-empty": false,
40 | "arrow-parens": false,
41 | "trailing-comma": true,
42 | "object-literal-sort-keys": false,
43 | "no-unused-expression": false,
44 | "variable-name": [
45 | false
46 | ],
47 | "one-line": [
48 | false
49 | ],
50 | "one-variable-per-declaration": [
51 | false
52 | ]
53 | },
54 | "rulesDirectory": []
55 | }
56 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const path = require('path');
3 | const nodeExternals = require('webpack-node-externals');
4 |
5 | module.exports = {
6 | entry: ['webpack/hot/poll?1000', './src/main.ts'],
7 | watch: true,
8 | target: 'node',
9 | externals: [
10 | nodeExternals({
11 | whitelist: ['webpack/hot/poll?1000'],
12 | }),
13 | ],
14 | module: {
15 | rules: [
16 | {
17 | test: /\.tsx?$/,
18 | use: 'ts-loader',
19 | exclude: /node_modules/,
20 | },
21 | ],
22 | },
23 | mode: "development",
24 | resolve: {
25 | extensions: ['.tsx', '.ts', '.js'],
26 | },
27 | plugins: [
28 | new webpack.HotModuleReplacementPlugin(),
29 | ],
30 | output: {
31 | path: path.join(__dirname, 'dist'),
32 | filename: 'server.js',
33 | },
34 | };
35 |
--------------------------------------------------------------------------------