├── .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 | --------------------------------------------------------------------------------