Repository: chnirt/nestjs-restful-best-practice Branch: master Commit: 39fa1770ab67 Files: 120 Total size: 145.6 KB Directory structure: gitextract_xs5wvu5j/ ├── .gitignore ├── .prettierrc ├── .well-known/ │ └── acme-challenge/ │ └── 3fEzNe2klZ1GLASfExbFL6LPbdmqZf2YmefYhRT-kwk ├── LICENSE ├── Procfile ├── README.md ├── nest-cli.json ├── package.json ├── src/ │ ├── app.controller.spec.ts │ ├── app.controller.ts │ ├── app.module.ts │ ├── app.service.ts │ ├── assets/ │ │ └── templates/ │ │ └── udacity-index.html │ ├── auth/ │ │ ├── auth.module.ts │ │ ├── auth.service.ts │ │ ├── facebook.strategy.ts │ │ ├── google.strategy.ts │ │ ├── jwt.strategy.ts │ │ └── local.strategy.ts │ ├── common/ │ │ ├── filters/ │ │ │ └── http-exception.filter.ts │ │ ├── index.ts │ │ ├── interceptors/ │ │ │ ├── exception.interceptor.ts │ │ │ ├── http-cache.interceptor.ts │ │ │ ├── logging.interceptor.ts │ │ │ ├── timeout.interceptor.ts │ │ │ └── transform.interceptor.ts │ │ ├── middleware/ │ │ │ └── logger.middleware.ts │ │ ├── pipes/ │ │ │ └── validation.pipe.ts │ │ └── wiston/ │ │ └── index.ts │ ├── config/ │ │ ├── cache/ │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── logger/ │ │ │ └── index.ts │ │ └── typeorm/ │ │ └── index.ts │ ├── config.orm.ts │ ├── environments/ │ │ └── index.ts │ ├── main.ts │ ├── modules/ │ │ ├── addresses/ │ │ │ ├── address.entity.ts │ │ │ ├── addresses.controller.ts │ │ │ ├── addresses.module.ts │ │ │ ├── addresses.service.ts │ │ │ ├── dto/ │ │ │ │ ├── create-address.dto.ts │ │ │ │ └── query-address.dto.ts │ │ │ └── enum/ │ │ │ └── address.enum.ts │ │ ├── attendance/ │ │ │ ├── attendance.controller.ts │ │ │ ├── attendance.module.ts │ │ │ ├── attendance.service.ts │ │ │ ├── dto/ │ │ │ │ ├── create-attendance.dto.ts │ │ │ │ └── replace-attendance.dto.ts │ │ │ └── entity/ │ │ │ └── attendance.entity.ts │ │ ├── banners/ │ │ │ ├── banner.entity.ts │ │ │ ├── banners.controller.ts │ │ │ ├── banners.module.ts │ │ │ ├── banners.service.ts │ │ │ └── dto/ │ │ │ ├── create-banner.dto.ts │ │ │ └── replace-banner.dto.ts │ │ ├── bidders/ │ │ │ ├── bidders.controller.ts │ │ │ ├── bidders.module.ts │ │ │ └── bidders.service.ts │ │ ├── chats/ │ │ │ ├── chats.gateway.ts │ │ │ └── chats.module.ts │ │ ├── classes/ │ │ │ ├── classes.controller.ts │ │ │ ├── classes.module.ts │ │ │ ├── classes.service.ts │ │ │ ├── dto/ │ │ │ │ ├── create-class.dto.ts │ │ │ │ └── replace-class.dto.ts │ │ │ └── entity/ │ │ │ └── class.entity.ts │ │ ├── connections/ │ │ │ ├── connection.entity.ts │ │ │ ├── connections.controller.ts │ │ │ ├── connections.module.ts │ │ │ ├── connections.service.ts │ │ │ ├── dto/ │ │ │ │ └── create-connection.dto.ts │ │ │ └── enum/ │ │ │ └── connection.enum.ts │ │ ├── deals/ │ │ │ ├── deal.entity.ts │ │ │ ├── deals.controller.ts │ │ │ ├── deals.module.ts │ │ │ ├── deals.service.ts │ │ │ ├── dto/ │ │ │ │ ├── create-deal.dto.ts │ │ │ │ └── deal-response.dto.ts │ │ │ ├── entity/ │ │ │ │ └── position.entity.ts │ │ │ └── enum/ │ │ │ └── deal.enum.ts │ │ ├── events/ │ │ │ ├── events.gateway.ts │ │ │ └── events.module.ts │ │ ├── students/ │ │ │ ├── dto/ │ │ │ │ ├── create-student.dto.ts │ │ │ │ └── replace-student.dto.ts │ │ │ ├── entity/ │ │ │ │ └── student.entity.ts │ │ │ ├── students.controller.ts │ │ │ ├── students.module.ts │ │ │ └── students.service.ts │ │ └── users/ │ │ ├── dto/ │ │ │ ├── create-user.dto.ts │ │ │ ├── created-by.dto.ts │ │ │ ├── error-response.dto.ts │ │ │ ├── login-response.dto.ts │ │ │ ├── login-user.dto.ts │ │ │ ├── otp-response.dto.ts │ │ │ ├── replace-user.dto.ts │ │ │ ├── update-user.dto.ts │ │ │ └── upload-response.dto.ts │ │ ├── index.ts │ │ ├── user.entity.ts │ │ ├── users.controller.ts │ │ ├── users.module.ts │ │ └── users.service.ts │ ├── shared/ │ │ ├── index.ts │ │ └── upload/ │ │ └── index.ts │ ├── terminus-options.service.ts │ └── utils/ │ ├── index.ts │ ├── password/ │ │ └── index.ts │ └── uuid/ │ └── index.ts ├── ssl/ │ ├── ca_bundle.crt │ ├── certificate.crt │ └── private.key ├── static/ │ ├── index.html │ ├── main.js │ └── style.css ├── test/ │ ├── app.e2e-spec.ts │ └── jest-e2e.json ├── tsconfig.build.json ├── tsconfig.json ├── tslint.json └── webpack.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # compiled output /dist /node_modules # others package-lock.json yarn.lock # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* # OS .DS_Store # Tests /coverage /.nyc_output # IDEs and editors /.idea .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # IDE - VSCode .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # nyc test coverage .nyc_output # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # TypeScript v1 declaration files typings/ # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variables file .env # next.js build output .next ================================================ FILE: .prettierrc ================================================ { "printWidth": 80, "tabWidth": 2, "useTabs": true, "semi": false, "singleQuote": true, "trailingComma": "none", "bracketSpacing": true, "jsxBracketSameLine": false, "fluid": false } ================================================ FILE: .well-known/acme-challenge/3fEzNe2klZ1GLASfExbFL6LPbdmqZf2YmefYhRT-kwk ================================================ 3fEzNe2klZ1GLASfExbFL6LPbdmqZf2YmefYhRT-kwk.G8p-XYVA4Y6MBmzrD23IQJ2p48OttBbjGc68r6pxqWo ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2019 Chnirt Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Procfile ================================================ web: npm run start:prod ================================================ FILE: README.md ================================================ # nestjs-restful-best-practice ================================================ FILE: nest-cli.json ================================================ { "collection": "@nestjs/schematics", "sourceRoot": "src" } ================================================ FILE: package.json ================================================ { "name": "nestjs-restful-best-practice", "version": "0.0.1", "description": "", "author": "", "license": "MIT", "scripts": { "prebuild": "rimraf dist", "build": "NODE_ENV=production nest build", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "start": "nest start", "start:dev": "nest start --watch", "start:debug": "nest start --debug --watch", "start:prod": "NODE_ENV=production node dist/main", "lint": "tslint -p tsconfig.json -c tslint.json", "test": "jest", "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:e2e": "jest --config ./test/jest-e2e.json", "webpack": "NODE_ENV=development nest build --watch --webpack", "start:hmr": "node dist/main", "heroku-postbuild": "npm i --only=dev --no-shrinkwrap && npm run build", "doc": "npx compodoc --port 11045 -p tsconfig.json -s", "doc:serve": "npm run doc && npx compodoc -s" }, "dependencies": { "@godaddy/terminus": "^4.2.1", "@nestjs/common": "^6.7.2", "@nestjs/core": "^6.7.2", "@nestjs/jwt": "^6.1.1", "@nestjs/passport": "^6.1.0", "@nestjs/platform-express": "^6.9.0", "@nestjs/platform-socket.io": "^6.9.0", "@nestjs/serve-static": "^1.0.1", "@nestjs/swagger": "^3.1.0", "@nestjs/terminus": "^6.5.2", "@nestjs/typeorm": "^6.2.0", "@nestjs/websockets": "^6.9.0", "@nestjsx/crud": "^4.2.0", "@nestjsx/crud-typeorm": "^4.2.0", "@types/mongodb": "^3.3.8", "bcrypt": "^3.0.6", "cache-manager": "^2.10.0", "class-transformer": "^0.2.3", "class-validator": "^0.11.0", "cloudinary": "1.16.0", "dotenv": "^8.2.0", "express-rate-limit": "^5.0.0", "geolib": "^3.1.0", "googleapis": "^45.0.0", "handlebars": "^4.5.2", "helmet": "^3.21.2", "http": "^0.0.0", "https": "^1.0.0", "mongodb": "^3.3.3", "nodemailer": "^6.3.1", "passport": "^0.4.0", "passport-facebook-token": "^3.3.0", "passport-google-oauth20": "^2.0.0", "passport-jwt": "^4.0.0", "passport-local": "^1.0.0", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.0", "rxjs": "^6.5.3", "speakeasy": "^2.0.0", "swagger-ui-express": "^4.1.2", "typeorm": "^0.2.20", "uuid": "^3.3.3", "webpack-bundle-analyzer": "^3.6.0", "winston": "^3.2.1" }, "devDependencies": { "@compodoc/compodoc": "^1.1.11", "@nestjs/cli": "^6.9.0", "@nestjs/schematics": "^6.7.0", "@nestjs/testing": "^6.7.1", "@types/bcrypt": "^3.0.0", "@types/dotenv": "^8.2.0", "@types/express": "^4.17.1", "@types/jest": "^24.0.18", "@types/node": "^12.7.5", "@types/passport-facebook-token": "^0.4.33", "@types/passport-jwt": "^3.0.2", "@types/passport-local": "^1.0.33", "@types/socket.io": "^2.1.4", "@types/speakeasy": "^2.0.5", "@types/supertest": "^2.0.8", "jest": "^24.9.0", "prettier": "^1.18.2", "progress-bar-webpack-plugin": "^1.12.1", "supertest": "^4.0.2", "ts-jest": "^24.1.0", "ts-loader": "^6.1.1", "ts-node": "^8.4.1", "tsconfig-paths": "^3.9.0", "tslint": "^5.20.0", "typescript": "^3.6.3", "webpack-node-externals": "^1.7.2" }, "jest": { "moduleFileExtensions": [ "js", "json", "ts" ], "rootDir": "src", "testRegex": ".spec.ts$", "transform": { "^.+\\.(t|j)s$": "ts-jest" }, "coverageDirectory": "./coverage", "testEnvironment": "node" } } ================================================ FILE: src/app.controller.spec.ts ================================================ import { Test, TestingModule } from '@nestjs/testing'; import { AppController } from './app.controller'; import { AppService } from './app.service'; describe('AppController', () => { let appController: AppController; beforeEach(async () => { const app: TestingModule = await Test.createTestingModule({ controllers: [AppController], providers: [AppService], }).compile(); appController = app.get(AppController); }); describe('root', () => { it('should return "Hello World!"', () => { expect(appController.getHello()).toBe('Hello World!'); }); }); }); ================================================ FILE: src/app.controller.ts ================================================ import { Controller, Get, Request, Post, UseGuards, Param, Res, UseInterceptors, UploadedFile, CacheInterceptor, Body, Query, Logger } from '@nestjs/common' import { AuthGuard } from '@nestjs/passport' import { ApiBearerAuth, ApiConsumes, ApiImplicitFile, ApiImplicitBody, ApiUseTags, ApiOperation, ApiResponse, ApiImplicitQuery } from '@nestjs/swagger' import { FileInterceptor } from '@nestjs/platform-express' import chalk from 'chalk' import { AppService } from './app.service' import { AuthService } from './auth/auth.service' import { LoginUserDto } from './modules/users/dto/login-user.dto' import { STATIC, SSL } from './environments' import { uploadFile } from './shared/upload' import { LoginResponseDto } from './modules/users/dto/login-response.dto' import { ErrorResponseDto } from './modules/users/dto/error-response.dto' import { UserEntity } from './modules/users/user.entity' import { UploadResponseDto } from './modules/users/dto/upload-response.dto' import { DealsService } from './modules/deals/deals.service' import { DealResponseDto } from './modules/deals/dto/deal-response.dto' import { DealType } from './modules/deals/enum/deal.enum' @ApiResponse({ status: 401, description: 'Unauthorized.', type: ErrorResponseDto }) @ApiResponse({ status: 403, description: 'Forbidden.', type: ErrorResponseDto }) @ApiUseTags('basic') @Controller() @UseInterceptors(CacheInterceptor) export class AppController { constructor( private readonly appService: AppService, private readonly authService: AuthService, private readonly dealService: DealsService ) {} @Get() getHello(): string { return this.appService.getHello() } @Post('/req') postHello(@Request() req) { Logger.log(`🤬 ${chalk.hex('#87e8de').bold(`${req.body}`)}`, 'Check') return req.body } @ApiResponse({ status: 200, description: 'The found record', type: LoginResponseDto }) @UseGuards(AuthGuard('local')) @ApiOperation({ title: 'Retrieve one Acess token 👻' }) @Post('login') @ApiImplicitBody({ name: 'input', type: LoginUserDto }) login(@Request() req): Promise { return this.authService.login(req.user) } @ApiResponse({ status: 200, description: 'The found profile', type: UserEntity }) @ApiBearerAuth() @UseGuards(AuthGuard('jwt')) @ApiOperation({ title: 'Retrieve one Profile 👻' }) @Get('profile') getProfile(@Request() req) { return req.user } @ApiResponse({ status: 200, description: 'The found records', type: [DealResponseDto] }) @ApiBearerAuth() @UseGuards(AuthGuard('jwt')) @ApiOperation({ title: 'Retrieve one My deal 👻' }) @Get('myDeal') @ApiImplicitQuery({ name: 'dealType', description: 'The dealType of the Deal', required: false, type: DealType, enum: ['Request', 'Offer'] }) getMyDeal(@Request() req, @Query() query) { const myDeal = this.dealService.findByUserId(req, query) return myDeal } @ApiResponse({ status: 201, description: 'The record has been successfully created.', type: UploadResponseDto }) @ApiBearerAuth() @UseGuards(AuthGuard('jwt')) @ApiOperation({ title: 'Create one File 👻' }) @Post('upload') @ApiConsumes('multipart/form-data') @ApiImplicitFile({ name: 'file', required: true }) @UseInterceptors(FileInterceptor('file')) async uploadFile(@UploadedFile() file): Promise { const url = await uploadFile(file) return { url } } @ApiBearerAuth() @UseGuards(AuthGuard('jwt')) @ApiOperation({ title: 'Retrieve many Files', deprecated: true }) @Post('uploads') @UseInterceptors(FileInterceptor('files')) @ApiConsumes('multipart/form-data') @ApiImplicitFile({ name: 'files', required: true, description: 'List of files.' }) uploadFiles(@UploadedFile() files: any) { // console.log(files); return ['path', 'path1'] } @ApiOperation({ title: 'Retrieve one File' // deprecated: true }) @Get(`${STATIC!}/:fileId`) getUpload(@Param('fileId') fileId: string, @Res() res): any { return res.sendFile(fileId, { root: `${STATIC!}` }) } @ApiBearerAuth() @UseGuards(AuthGuard('jwt')) @ApiOperation({ title: 'Verify one Ssl', deprecated: true }) @Get(`${SSL!}/:fileId`) getSSLKey(@Param('fileId') fileId: string, @Res() res): any { return res.sendFile(fileId, { root: `${SSL!}` }) } } ================================================ FILE: src/app.module.ts ================================================ import { Module, CacheModule, OnModuleInit } from '@nestjs/common' import { TypeOrmModule } from '@nestjs/typeorm' import { ServeStaticModule } from '@nestjs/serve-static' import { join } from 'path' import { TerminusModule } from '@nestjs/terminus' import { AppController } from './app.controller' import { AppService } from './app.service' import { CacheService, TypeormService } from './config' import { UsersModule } from './modules/users/users.module' import { AuthModule } from './auth/auth.module' import { TerminusOptionsService } from './terminus-options.service' import { EventsModule } from './modules/events/events.module' import { EventsGateway } from './modules/events/events.gateway' import { STATIC } from './environments' import { DealsModule } from './modules/deals/deals.module' import { BiddersModule } from './modules/bidders/bidders.module' import { AddressesModule } from './modules/addresses/addresses.module' import { ConnectionsModule } from './modules/connections/connections.module' import { BannersModule } from './modules/banners/banners.module' import { ClassesModule } from './modules/classes/classes.module' import { StudentsModule } from './modules/students/students.module' import { AttendanceModule } from './modules/attendance/attendance.module' import { ChatsModule } from './modules/chats/chats.module' import { ChatsGateway } from './modules/chats/chats.gateway' @Module({ imports: [ TypeOrmModule.forRootAsync({ useClass: TypeormService }), CacheModule.registerAsync({ useClass: CacheService }), ServeStaticModule.forRoot({ rootPath: join(__dirname, '..', STATIC) }), ServeStaticModule.forRoot({ rootPath: join(__dirname, '..', '.well-known/acme-challenge') }), TerminusModule.forRootAsync({ useClass: TerminusOptionsService }), AuthModule, UsersModule, AddressesModule, DealsModule, ConnectionsModule, EventsModule, BiddersModule, BannersModule, ClassesModule, StudentsModule, AttendanceModule, ChatsModule ], controllers: [AppController], providers: [AppService, EventsGateway, ChatsGateway] }) export class AppModule implements OnModuleInit { onModuleInit() { console.log('The module has been initialized.') } } ================================================ FILE: src/app.service.ts ================================================ import { Injectable } from '@nestjs/common' @Injectable() export class AppService { getHello() { return 'Hello World!' } } ================================================ FILE: src/assets/templates/udacity-index.html ================================================ Verify Your Email on {{ author }} {{ author }}_email
One more step to get started
{{ author }}
       

Hi {{ to }},

{{ text1 }}

{{ button }}

{{ text2 }}

Cheers,
{{ author }}'s Team
 
 
 
 
       
IOS Android
Twitter Facebook Google Linkedin

{{ number }} {{ street }} St.  •  {{ city }} City, {{ country }}

Be in demand
================================================ FILE: src/auth/auth.module.ts ================================================ import { Module } from '@nestjs/common' import { JwtModule } from '@nestjs/jwt' import { PassportModule } from '@nestjs/passport' import { AuthService } from './auth.service' // import { UsersModule } from '../modules/users/users.module' import { LocalStrategy } from './local.strategy' import { JwtStrategy } from './jwt.strategy' import { ACCESS_TOKEN_SECRET } from '../environments' @Module({ imports: [ // UsersModule, PassportModule.register({ defaultStrategy: 'jwt', session: true }), JwtModule.register({ secret: ACCESS_TOKEN_SECRET!, signOptions: { expiresIn: '30d' } }) ], providers: [AuthService, LocalStrategy, JwtStrategy], exports: [AuthService] }) export class AuthModule {} ================================================ FILE: src/auth/auth.service.ts ================================================ import { Injectable } from '@nestjs/common' import { JwtService } from '@nestjs/jwt' import { comparePassword } from '../utils' import { UserEntity } from '../modules/users/user.entity' import { LoginResponseDto } from 'modules/users/dto/login-response.dto' import { getMongoRepository } from 'typeorm' @Injectable() export class AuthService { constructor(private readonly jwtService: JwtService) {} async validateUser(email: string, password: string): Promise { const user = await getMongoRepository(UserEntity).findOne({ where: { email } }) if (user && (await comparePassword(password, user.password))) { // tslint:disable-next-line: no-shadowed-variable const { password, ...result } = user return result } return null } async login(user: UserEntity): Promise { const { _id } = user const payload = { sub: _id } const expiresIn = 60 * 60 * 24 * 30 return { accessToken: this.jwtService.sign(payload, { expiresIn }), user, expiresIn } } } ================================================ FILE: src/auth/facebook.strategy.ts ================================================ // import { Injectable } from "@nestjs/common"; // @Injectable() // export class FacebookStrategy { // constructor( // private readonly userService: UserService, // ) { // this.init(); // } // init() { // use( // new FacebookTokenStrategy( // { // clientID: , // clientSecret: , // fbGraphVersion: 'v3.0', // }, // async ( // accessToken: string, // refreshToken: string, // profile: any, // done: any, // ) => { // const user = await this.userService.findOrCreate( // profile, // ); // return done(null, user); // }, // ), // ); // } // } ================================================ FILE: src/auth/google.strategy.ts ================================================ // import { Injectable } from '@nestjs/common' // import { PassportStrategy } from '@nestjs/passport' // import { Strategy } from 'passport-google-oauth20' // import { AuthService, Provider } from './auth.service' // @Injectable() // export class GoogleStrategy extends PassportStrategy(Strategy, 'google') { // constructor(private readonly authService: AuthService) { // super({ // clientID: 'MY_CLIENT_ID', // Not my real client secret, see your own application credentials at Google! // clientSecret: 'MY_CLIENT_SECRET', // Not my real client secret, see your own application credentials at Google! // callbackURL: 'http://localhost:3000/auth/google/callback', // passReqToCallback: true, // scope: ['profile'] // }) // } // async validate( // request: any, // accessToken: string, // refreshToken: string, // profile, // done: any // ) { // try { // console.log(profile) // const jwt: string = await this.authService.validateOAuthLogin( // profile.id, // Provider.GOOGLE // ) // const user = { // jwt // } // done(null, user) // } catch (err) { // // console.log(err) // done(err, false) // } // } // } ================================================ FILE: src/auth/jwt.strategy.ts ================================================ import { ExtractJwt, Strategy } from 'passport-jwt' import { PassportStrategy } from '@nestjs/passport' import { Injectable, UnauthorizedException } from '@nestjs/common' import { ACCESS_TOKEN_SECRET } from '../environments' import { getMongoRepository } from 'typeorm' import { UserEntity } from '../modules/users/user.entity' @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { constructor() { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), ignoreExpiration: false, secretOrKey: ACCESS_TOKEN_SECRET! }) } async validate(payload: any) { try { const { sub } = payload const user = await getMongoRepository(UserEntity).findOne({ _id: sub }) const { password, ...result } = user return result } catch (err) { throw new UnauthorizedException('Email or password is incorrect.') } } } ================================================ FILE: src/auth/local.strategy.ts ================================================ import { Strategy } from 'passport-local' import { PassportStrategy } from '@nestjs/passport' import { Injectable, UnauthorizedException } from '@nestjs/common' import { AuthService } from './auth.service' @Injectable() export class LocalStrategy extends PassportStrategy(Strategy) { constructor(private readonly authService: AuthService) { super({ usernameField: 'email', passwordField: 'password' }) } async validate(username: string, password: string): Promise { try { const user = await this.authService.validateUser(username, password) if (!user) { throw new UnauthorizedException('Email or password is incorrect.') } return user } catch (err) { throw new UnauthorizedException('Email or password is incorrect.') } } } ================================================ FILE: src/common/filters/http-exception.filter.ts ================================================ import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from '@nestjs/common' @Catch(HttpException) export class HttpExceptionFilter implements ExceptionFilter { catch(exception: HttpException, host: ArgumentsHost) { const ctx = host.switchToHttp() const response = ctx.getResponse() const request = ctx.getRequest() const statusCode = exception.getStatus() response.status(statusCode).json({ statusCode, message: exception.message.message || exception.message.error, timestamp: new Date().toISOString(), path: request.url }) } } ================================================ FILE: src/common/index.ts ================================================ export * from './filters/http-exception.filter' export * from './interceptors/exception.interceptor' export * from './interceptors/http-cache.interceptor' export * from './interceptors/logging.interceptor' export * from './interceptors/timeout.interceptor' export * from './interceptors/transform.interceptor' export * from './middleware/logger.middleware' export * from './pipes/validation.pipe' // export * from './wiston' ================================================ FILE: src/common/interceptors/exception.interceptor.ts ================================================ import { CallHandler, ExecutionContext, HttpException, HttpStatus, Injectable, NestInterceptor, } from '@nestjs/common'; import { Observable, throwError } from 'rxjs'; import { catchError } from 'rxjs/operators'; @Injectable() export class ErrorsInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable { return next .handle() .pipe( catchError(err => throwError(new HttpException('New message', HttpStatus.BAD_GATEWAY)), ), ); } } ================================================ FILE: src/common/interceptors/http-cache.interceptor.ts ================================================ import { CacheInterceptor, ExecutionContext, Injectable } from '@nestjs/common' @Injectable() class HttpCacheInterceptor extends CacheInterceptor { trackBy(context: ExecutionContext): string | undefined { const request = context.switchToHttp().getRequest() const httpServer = request.applicationRef const isGetRequest = httpServer.getRequestMethod(request) === 'GET' const excludePaths = [] if ( !isGetRequest || (isGetRequest && excludePaths.includes(httpServer.getRequestUrl)) ) { return undefined } return httpServer.getRequestUrl(request) } } ================================================ FILE: src/common/interceptors/logging.interceptor.ts ================================================ import { CallHandler, ExecutionContext, Injectable, NestInterceptor, Logger, } from '@nestjs/common'; import { Observable } from 'rxjs'; import { tap } from 'rxjs/operators'; import chalk from 'chalk'; @Injectable() export class LoggingInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable { const parentType = chalk .hex('#87e8de') .bold(`${context.getArgs()[0].route.path}`); const fieldName = chalk .hex('#87e8de') .bold(`${context.getArgs()[0].route.stack[0].method}`); return next.handle().pipe( tap(() => { Logger.debug(`⛩ ${parentType} » ${fieldName}`, 'Restful'); // console.log(context.getArgs()[0]['route']); }), ); } } ================================================ FILE: src/common/interceptors/timeout.interceptor.ts ================================================ import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common' import { Observable } from 'rxjs' import { timeout } from 'rxjs/operators' @Injectable() export class TimeoutInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable { return next.handle().pipe(timeout(100000)) } } ================================================ FILE: src/common/interceptors/transform.interceptor.ts ================================================ import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common' import { Observable } from 'rxjs' import { map } from 'rxjs/operators' export interface Response { data: T } @Injectable() export class TransformInterceptor implements NestInterceptor> { intercept( context: ExecutionContext, next: CallHandler ): Observable> { return next.handle().pipe(map(data => ({ data }))) } } ================================================ FILE: src/common/middleware/logger.middleware.ts ================================================ // import chalk from 'chalk' // import { logger } from '../wiston' import { Logger } from '@nestjs/common'; export function LoggerMiddleware(req, res, next) { // logger.debug(`📢 ${req.headers['user-agent']}`) Logger.debug( `📢 ${req.headers['user-agent'].split(') ')[0]})`, 'Bootstrap', false, ); next(); } ================================================ FILE: src/common/pipes/validation.pipe.ts ================================================ import { Injectable, PipeTransform, ArgumentMetadata, BadRequestException, } from '@nestjs/common'; import { validate } from 'class-validator'; import { plainToClass } from 'class-transformer'; @Injectable() export class ValidationPipe implements PipeTransform { async transform(value: any, { metatype }: ArgumentMetadata) { // destructuring metadata if (!metatype || !this.toValidate(metatype)) { return value; } const object = plainToClass(metatype, value); const errors = await validate(object); if (errors.length > 0) { throw new BadRequestException( `Form Arguments invalid: ${this.formatErrors(errors)}`, ); } return value; } // tslint:disable-next-line:ban-types private toValidate(metatype: Function): boolean { // tslint:disable-next-line:ban-types const types: Function[] = [String, Boolean, Number, Array, Object]; return !types.includes(metatype); } private formatErrors(errors: any[]) { return errors .map(err => { // tslint:disable-next-line: forin for (const property in err.constraints) { return err.constraints[property]; } }) .join(', '); } } ================================================ FILE: src/common/wiston/index.ts ================================================ // import { addColors, createLogger, format, transports } from 'winston'; // const { label, json, timestamp, align, printf } = format; // const config = { // levels: { // error: 0, // debug: 1, // warn: 2, // data: 3, // info: 4, // verbose: 5, // silly: 6, // custom: 7, // }, // colors: { // error: 'red', // debug: 'blue', // warn: 'yellow', // data: 'grey', // info: 'green', // verbose: 'cyan', // silly: 'magenta', // custom: 'yellow', // }, // }; // // tslint:disable-next-line:no-shadowed-variable // const myFormat = printf(({ level, message, label, timestamp }) => { // // console.log(level) // return `{\n\tlabel: ${label},\n\ttimestamp: ${timestamp},\n\tlevel: ${level},\n\tmessage: ${message}\n},`; // }); // const logger = createLogger({ // level: 'error', // levels: config.levels, // format: format.combine( // label({ label: '👻 Chnirt!' }), // json(), // timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), // align(), // // prettyPrint(), // // colorize(), // myFormat, // ), // defaultMeta: { service: 'user-service' }, // transports: [ // // // // - Write to all logs with level `info` and below to `combined.log` // // - Write all logs error (and below) to `error.log`. // // // // new transports.Console(), // new transports.File({ // filename: 'logs/info.log', // level: 'info', // }), // new transports.File({ // filename: 'logs/error.log', // level: 'error', // }), // new transports.File({ // filename: 'logs/warn.log', // level: 'warn', // }), // new transports.File({ // filename: 'logs/debug.log', // level: 'debug', // }), // new transports.File({ // filename: 'logs/verbose.log', // level: 'verbose', // }), // // new transports.File({ filename: 'src/logs/combined.log' }) // ], // }); // addColors(config.colors); // export { logger }; ================================================ FILE: src/config/cache/index.ts ================================================ import { Injectable, CacheOptionsFactory, CacheModuleOptions, } from '@nestjs/common'; @Injectable() export class CacheService implements CacheOptionsFactory { createCacheOptions(): CacheModuleOptions { return { ttl: 5, // seconds max: 10, // maximum number of items in cache }; } } ================================================ FILE: src/config/index.ts ================================================ export * from './cache'; export * from './logger'; export * from './typeorm'; ================================================ FILE: src/config/logger/index.ts ================================================ import { LoggerService } from '@nestjs/common' export class MyLogger implements LoggerService { log(message: string) {} error(message: string, trace: string) {} warn(message: string) {} debug(message: string) {} verbose(message: string) {} } ================================================ FILE: src/config/typeorm/index.ts ================================================ import { Injectable, Logger } from '@nestjs/common'; import { TypeOrmOptionsFactory, TypeOrmModuleOptions } from '@nestjs/typeorm'; import { getMetadataArgsStorage, createConnection } from 'typeorm'; import config from '../../config.orm'; // import { logger } from '../../common' @Injectable() export class TypeormService implements TypeOrmOptionsFactory { async createTypeOrmOptions(): Promise { const options = { ...config, type: 'mongodb', entities: getMetadataArgsStorage().tables.map(tbl => tbl.target), // migrations: ['src/modules/**/migration/*.ts'], // subscribers: ['src/modules/**/subscriber/*.ts'], // cli: { // entitiesDir: 'src/modules/**/entity', // migrationsDir: 'src/modules/**/migration', // subscribersDir: 'src/modules/**/subscriber' // }, synchronize: true, useNewUrlParser: true, useUnifiedTopology: true, keepConnectionAlive: true, logging: true, }; createConnection(options) .then(data => { // logger.info(data) Logger.log(`☁️ Database connected`, 'TypeORM', false); }) .catch(err => { // logger.error(err) Logger.error(`❌ Database connect error`, '', 'TypeORM', false); }); return options; } } ================================================ FILE: src/config.orm.ts ================================================ import { NODE_ENV, MONGO_URL, MONGO_PORT, MONGO_DB } from './environments'; const orm = { development: { url: MONGO_URL!, }, testing: { url: MONGO_URL!, }, staging: { host: 'localhost', port: MONGO_PORT!, username: '', password: '', database: MONGO_DB!, }, production: { url: MONGO_URL!, }, }; export default orm[NODE_ENV!]; ================================================ FILE: src/environments/index.ts ================================================ import * as dotenv from 'dotenv' dotenv.config() // environment const NODE_ENV: string = process.env.NODE_ENV || 'development' // author const AUTHOR: string = process.env.AUTHOR || 'Chnirt' // application const DOMAIN: string = process.env.DOMAIN || 'localhost' const PORT: number = +process.env.PORT || 14047 const END_POINT: string = process.env.END_POINT || 'graphql' const VOYAGER: string = process.env.VOYAGER || 'voyager' const FE_URL: string = process.env.FE_URL || 'xxx' const RATE_LIMIT_MAX: number = +process.env.RATE_LIMIT_MAX || 10000 const GRAPHQL_DEPTH_LIMIT: number = +process.env.GRAPHQL_DEPTH_LIMIT || 3 // static const STATIC: string = process.env.STATIC || 'static' // ssl const SSL: string = process.env.SSL || '.well-known/acme-challenge' // mlab const MLAB_USER = process.env.MLAB_USER || 'admin' const MLAB_PASS = process.env.MLAB_PASS || 'chnirt1803' const MLAB_HOST = process.env.MLAB_HOST || 'ds243055.mlab.com' const MLAB_PORT = +process.env.MLAB_PORT || 43055 const MLAB_DATABASE = process.env.MLAB_DATABASE || 'nestjs-restful-best-practice' const MLAB_URL = process.env.MLAB_URL || `mongodb://${MLAB_USER}:${MLAB_PASS}@${MLAB_HOST}:${MLAB_PORT}/${MLAB_DATABASE}` // mongodb const MONGO_URL: string = process.env.MONGO_PORT ? `mongodb://localhost:${process.env.MONGO_PORT}` : MLAB_URL const MONGO_PORT: number = +process.env.MONGO_PORT || 11049 const MONGO_DB: string = process.env.MONGO_PORT ? 'chnirt-nest' : MLAB_DATABASE // jsonwebtoken const ISSUER: string = process.env.ISSUER || 'http://chnirt.github.io' const ACCESS_TOKEN: string = process.env.ACCESS_TOKEN || 'access-token' const ACCESS_TOKEN_SECRET: string = process.env.ACCESS_TOKEN_SECRET || 'basic' const REFRESH_TOKEN: string = process.env.REFRESH_TOKEN || 'refresh-token' const REFRESH_TOKEN_SECRET: string = process.env.REFRESH_TOKEN_SECRET || 'refresh-token-key' const EMAIL_TOKEN: string = process.env.EMAIL_TOKEN || 'email-token' const EMAIL_TOKEN_SECRET: string = process.env.EMAIL_TOKEN_SECRET || 'email-token-key' const RESETPASS_TOKEN: string = process.env.RESETPASS_TOKEN || 'resetpass-token' const RESETPASS_TOKEN_SECRET: string = process.env.RESETPASS_TOKEN_SECRET || 'resetpass-token-key' // bcrypt const SALT: number = +process.env.SALT || 10 // nodemailer const MAIL_USER: string = process.env.MAIL_USER || 'xxx' const MAIL_PASS: string = process.env.MAIL_PASS || 'xxx' // cloudinary const CLOUD_NAME: string = process.env.CLOUD_NAME || 'xxx' const API_KEY: string = process.env.API_KEY || 'xxx' const API_SECRET: string = process.env.API_SECRET || 'xxx' // speakeasy const SPEAKEASY_SECRET = process.env.SPEAKEASY_SECRET || 'speakeasy-secret' const SPEAKEASY_DIGITS = +process.env.SPEAKEASY_DIGITS || 6 const SPEAKEASY_STEP = +process.env.SPEAKEASY_STEP || 60 // pubsub const NOTIFICATION_SUBSCRIPTION: string = 'newNotification' const USER_SUBSCRIPTION: string = 'newUser' const MESSAGES_SUBSCRIPTION: string = 'newMessages' // passport const GOOGLE_CLIENT_ID: string = process.env.GOOGLE_CLIENT_ID || 'xxx' const GOOGLE_CLIENT_SECRET: string = process.env.GOOGLE_CLIENT_SECRET || 'xxx' const GOOGLE_CALLBACK_URL: string = process.env.GOOGLE_CALLBACK_URL || 'auth/google/callback' const FACEBOOK_APP_ID: string = process.env.FACEBOOK_APP_ID || 'xxx' const FACEBOOK_APP_SECRET: string = process.env.FACEBOOK_APP_SECRET || 'xxx' const FACEBOOK_CALLBACK_URL: string = process.env.FACEBOOK_CALLBACK_URL || 'auth/facebook/callback' // google cloud const GOOGLE_APPLICATION_CREDENTIALS: string = process.env.GOOGLE_APPLICATION_CREDENTIALS || 'xxx' // stripe const STRIPE_PUBLIC_KEY: string = process.env.STRIPE_PUBLIC_KEY || 'xxx' const STRIPE_SECRET_KEY: string = process.env.STRIPE_SECRET_KEY || 'xxx' const STRIPE_PLAN: string = process.env.STRIPE_PLAN || 'xxx' // twilio const TWILIO_ACCOUNT_SID: string = process.env.TWILIO_ACCOUNT_SID || 'xxx' const TWILIO_AUTH_TOKEN: string = process.env.TWILIO_AUTH_TOKEN || 'xxx' export { NODE_ENV, AUTHOR, DOMAIN, PORT, END_POINT, VOYAGER, FE_URL, RATE_LIMIT_MAX, GRAPHQL_DEPTH_LIMIT, STATIC, SSL, MLAB_USER, MLAB_PASS, MLAB_HOST, MLAB_PORT, MLAB_DATABASE, MLAB_URL, MONGO_URL, MONGO_PORT, MONGO_DB, ISSUER, ACCESS_TOKEN, ACCESS_TOKEN_SECRET, REFRESH_TOKEN, REFRESH_TOKEN_SECRET, RESETPASS_TOKEN, RESETPASS_TOKEN_SECRET, EMAIL_TOKEN, EMAIL_TOKEN_SECRET, SALT, MAIL_USER, MAIL_PASS, CLOUD_NAME, API_KEY, API_SECRET, SPEAKEASY_SECRET, SPEAKEASY_DIGITS, SPEAKEASY_STEP, USER_SUBSCRIPTION, NOTIFICATION_SUBSCRIPTION, MESSAGES_SUBSCRIPTION, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_CALLBACK_URL, FACEBOOK_APP_ID, FACEBOOK_APP_SECRET, FACEBOOK_CALLBACK_URL, GOOGLE_APPLICATION_CREDENTIALS, STRIPE_PUBLIC_KEY, STRIPE_SECRET_KEY, STRIPE_PLAN, TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN } ================================================ FILE: src/main.ts ================================================ import { NestFactory } from '@nestjs/core' import { Logger, InternalServerErrorException } from '@nestjs/common' import chalk from 'chalk' import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger' import * as bodyParser from 'body-parser' import * as helmet from 'helmet' import * as rateLimit from 'express-rate-limit' import * as fs from 'fs' import { AppModule } from './app.module' import { ValidationPipe, LoggerMiddleware, TimeoutInterceptor, LoggingInterceptor, HttpExceptionFilter } from './common' import { MyLogger } from './config' import { NODE_ENV, DOMAIN, PORT } from './environments' declare const module: any async function bootstrap() { try { // const httpsOptions = { // key: fs.readFileSync('ssl/private.key'), // cert: fs.readFileSync('ssl/certificate.crt'), // ca: fs.readFileSync('ssl/ca_bundle.crt'), // } const app = await NestFactory.create(AppModule, { // httpsOptions, logger: new MyLogger(), cors: true }) app.use(helmet()) // body parser app.use(bodyParser.json()) app.use(bodyParser.urlencoded()) // app.use(bodyParser.json({ limit: '2mb' })) // app.use( // bodyParser.urlencoded({ // limit: '2mb', // extended: true, // parameterLimit: 2000 // }) // ) app.use( rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 10000 // limit each IP to 10000 requests per windowMs }) ) // // adapter for e2e testing const httpAdapter = app.getHttpAdapter() // loggerMiddleware // tslint:disable-next-line:no-unused-expression NODE_ENV !== 'testing' && app.use(LoggerMiddleware) // interceptors // app.useGlobalInterceptors(new LoggingInterceptor()) // app.useGlobalInterceptors(new TimeoutInterceptor()) // app.useGlobalFilters(new HttpExceptionFilter()) // global nest setup app.useGlobalPipes(new ValidationPipe()) // Starts listening to shutdown hooks app.enableShutdownHooks() const options = new DocumentBuilder() .setTitle('Nestjs Restful Best Practice') .setVersion('3.0.0') // .setHost('nestjs-restful-best-practice.herokuapp.com') .setBasePath('/v1') .setDescription('built NestJS, TypeORM, MongoDB') .setTermsOfService( 'https://github.com/chnirt/nestjs-restful-best-practice/blob/master/LICENSE' ) .setContactEmail('trinhchinchin@gmail.com') .setLicense( 'MIT License', 'https://github.com/chnirt/nestjs-restful-best-practice/blob/master/LICENSE' ) .setExternalDoc('For more information', 'http://swagger.io') .setSchemes(NODE_ENV !== 'production' ? 'http' : 'https') .addBearerAuth('Authorization', 'header') .addTag('chnirt', 'developer') .build() const document = SwaggerModule.createDocument(app, options) SwaggerModule.setup('api', app, document) app.setGlobalPrefix('/v1') const server = await app.listen(PORT!) // hot module replacement if (module.hot) { module.hot.accept() module.hot.dispose(() => app.close()) } NODE_ENV !== 'production' ? Logger.log( `🚀 Server ready at https://${DOMAIN!}:${chalk .hex('#87e8de') .bold(`${PORT!}`)}`, 'Bootstrap' ) : Logger.log( `🚀 Server is listening on port ${chalk .hex('#87e8de') .bold(`${PORT!}`)}`, 'Bootstrap' ) } catch (error) { // logger.error(error) Logger.error(`❌ Error starting server, ${error}`, '', 'Bootstrap', false) process.exit() throw new InternalServerErrorException(error) } } bootstrap().catch(e => { throw e }) ================================================ FILE: src/modules/addresses/address.entity.ts ================================================ import { Entity, ObjectIdColumn, Column } from 'typeorm' import { uuidv4 } from '../../utils' // import { Exclude, plainToClass } from 'class-transformer' import { ApiModelProperty } from '@nestjs/swagger' import { Position } from '../deals/entity/position.entity' import { AddressType } from './enum/address.enum' @Entity({ name: 'addresses', orderBy: { createdAt: 'ASC' } }) export class AddressEntity { @ApiModelProperty({ description: 'The _id of the Address' }) @ObjectIdColumn() _id: string @ApiModelProperty({ description: 'The name of the Address' }) @Column() name: string @ApiModelProperty({ description: 'The addressType of the Address' }) @Column() addressType: AddressType @ApiModelProperty({ description: 'The location of the Address' }) @Column() location: Position @ApiModelProperty({ description: 'The unitNumber of the Address' }) @Column() unitNumber: number @ApiModelProperty({ description: 'The remarks of the Address' }) @Column() remarks: string @ApiModelProperty({ description: 'The createdBy of the Address' }) @Column() createdBy: string @ApiModelProperty({ description: 'The createdAt of the Address' }) @Column() createdAt: number @ApiModelProperty({ description: 'The updatedAt of the Address' }) @Column() updatedAt: number constructor(partial: Partial) { if (partial) { Object.assign(this, partial) this._id = this._id || uuidv4() this.createdAt = this.createdAt || +new Date() this.updatedAt = +new Date() } } } ================================================ FILE: src/modules/addresses/addresses.controller.ts ================================================ import { Controller, Post, Body, Request, UseGuards, Get, Query } from '@nestjs/common' import { ApiOperation, ApiBearerAuth, ApiImplicitQuery, ApiUseTags, ApiResponse } from '@nestjs/swagger' import { CreateAddressDto } from './dto/create-address.dto' import { AddressesService, Address } from './addresses.service' import { AuthGuard } from '@nestjs/passport' import { ErrorResponseDto } from '../users/dto/error-response.dto' import { AddressEntity } from './address.entity' @ApiBearerAuth() @UseGuards(AuthGuard('jwt')) @ApiResponse({ status: 401, description: 'Unauthorized.', type: ErrorResponseDto }) @ApiResponse({ status: 403, description: 'Forbidden.', type: ErrorResponseDto }) @ApiUseTags('addresses') @Controller('addresses') export class AddressesController { constructor(private readonly addressesService: AddressesService) {} @ApiResponse({ status: 200, description: 'The found records', type: [AddressEntity] }) @ApiOperation({ title: 'Retrieve many Addresses 👻' // description: 'Aaa', // operationId: 'aaaa' }) @Get() @ApiImplicitQuery({ name: 'limit', description: 'The maximum number of transactions to return', required: false, type: Number }) @ApiImplicitQuery({ name: 'offset', description: 'The maximum number of transactions to skip', required: false, type: Number }) findAll(@Query() query, @Request() req): Promise { return this.addressesService.findAll(query, req) } @ApiResponse({ status: 201, description: 'The record has been successfully created', type: Boolean }) @ApiOperation({ title: 'Create one Address 👻' }) @Post() insert( @Body() createAddressDto: CreateAddressDto, @Request() req ): Promise { return this.addressesService.insert(createAddressDto, req) } } ================================================ FILE: src/modules/addresses/addresses.module.ts ================================================ import { Module } from '@nestjs/common'; import { AddressesController } from './addresses.controller'; import { AddressesService } from './addresses.service'; @Module({ controllers: [AddressesController], providers: [AddressesService] }) export class AddressesModule {} ================================================ FILE: src/modules/addresses/addresses.service.ts ================================================ import { Injectable, ForbiddenException } from '@nestjs/common' import { CreateAddressDto } from './dto/create-address.dto' import { AddressEntity } from './address.entity' import { getMongoRepository } from 'typeorm' export type Address = any @Injectable() export class AddressesService { async findAll(query: any, req: any): Promise { const { offset, limit } = query const { user } = req const { _id } = user const pipelineArray = [] if (offset) { if (offset < 1) { throw new ForbiddenException('The offset must be greater than 0') } else { pipelineArray.push({ $skip: +offset }) } } if (limit) { if (limit < 1) { throw new ForbiddenException('The limit must be greater than 0') } else { pipelineArray.push({ $limit: +limit }) } } const match = [ { $match: { createdBy: _id } }, { $project: { createdBy: 0, createdAt: 0, updatedAt: 0 } } ] pipelineArray.push(...match) return await getMongoRepository(AddressEntity) .aggregate(pipelineArray) .toArray() } async insert(createAddressDto: CreateAddressDto, req: any): Promise { const { user } = req const { _id } = user const { addressType } = createAddressDto if (addressType !== 'Others') { const foundAddress = await getMongoRepository(AddressEntity).findOne({ addressType, createdBy: _id }) if (foundAddress) { throw new ForbiddenException( `Address at ${addressType} already existed` ) } } const newAddress = await getMongoRepository(AddressEntity).save( new AddressEntity({ ...createAddressDto, createdBy: _id }) ) return newAddress && true } } ================================================ FILE: src/modules/addresses/dto/create-address.dto.ts ================================================ import { ApiModelProperty } from '@nestjs/swagger' import { IsNotEmpty, IsEnum, IsOptional } from 'class-validator' import { AddressType } from '../enum/address.enum' import { Position } from '../../deals/entity/position.entity' export class CreateAddressDto { @ApiModelProperty({ enum: ['Home', 'Workplace', 'Others'], example: 'Home', description: 'The addressType of the Address' }) @IsEnum(AddressType) @IsNotEmpty() readonly addressType: AddressType @ApiModelProperty({ default: 'Viva coffee', example: 'Viva coffee', description: 'The name of the Address' }) @IsOptional() readonly name: string @ApiModelProperty({ default: { latitude: 10.780230999999999, longitude: 106.6645121 }, example: { latitude: 10.780230999999999, longitude: 106.6645121 }, description: 'The location of the Address' }) @IsNotEmpty() readonly location: Position @ApiModelProperty({ default: 69, example: 69, description: 'The unitNumber of the Address' }) @IsNotEmpty() // 30m 1h 1h30 2h readonly unitNumber: number @ApiModelProperty({ default: 'Đối diện BigC', example: 'Đối diện BigC', description: 'The remarks of the Address' }) @IsOptional() readonly remarks: string } ================================================ FILE: src/modules/addresses/dto/query-address.dto.ts ================================================ import { ApiModelProperty } from '@nestjs/swagger'; class QueryFilterDto { @ApiModelProperty({ type: String, example: 'age', }) readonly field: string; @ApiModelProperty({ type: String, example: 'gt', }) readonly comparator: string; @ApiModelProperty({ type: Object, example: 25, }) readonly value: any; } ================================================ FILE: src/modules/addresses/enum/address.enum.ts ================================================ export enum AddressType { Home = 'Home', Workplace = 'Workplace', Others = 'Others' } ================================================ FILE: src/modules/attendance/attendance.controller.ts ================================================ import { Controller, Get, Post, Body, Param, Put, Delete } from '@nestjs/common' import { AttendanceService } from './attendance.service' import { ApiResponse, ApiOperation, ApiUseTags } from '@nestjs/swagger' import { AttendanceEntity } from './entity/attendance.entity' import { ReplaceAttendanceDto } from './dto/replace-attendance.dto' import { CreateAttendanceDto } from './dto/create-attendance.dto' @ApiUseTags('attendance') @Controller('attendance') export class AttendanceController { constructor(private readonly attendanceService: AttendanceService) {} @ApiResponse({ status: 200, description: 'The found records', type: [AttendanceEntity] }) @ApiOperation({ title: 'Retrieve many attendance 👾' }) @Get() findAll() { return this.attendanceService.findAll() } @ApiResponse({ status: 200, description: 'The found record', type: AttendanceEntity }) @ApiOperation({ title: 'Create one Attendance 👾' }) @Post() async insert(@Body() createAttendanceDto: CreateAttendanceDto) { const newAttendance = await this.attendanceService.insert( createAttendanceDto ) return newAttendance } @ApiResponse({ status: 200, description: 'The found record', type: AttendanceEntity }) @ApiOperation({ title: 'Retrieve one Attendance 👾' }) @Get(':id') findOne(@Param('id') id: string) { return this.attendanceService.findOne(id) } @ApiOperation({ title: 'Replace one Attendance 👾' }) @Put(':id') replace( @Param('id') id: string, @Body() replaceAttendanceDto: ReplaceAttendanceDto ) { return this.attendanceService.findOneAndReplace(id, replaceAttendanceDto) } @ApiResponse({ status: 200, description: 'The found record is executed 👾', type: Boolean }) @ApiOperation({ title: 'Delete one Attendance 👾' }) @Delete(':id') remove(@Param('id') id: string) { return this.attendanceService.deleteOne(id) } } ================================================ FILE: src/modules/attendance/attendance.module.ts ================================================ import { Module } from '@nestjs/common'; import { AttendanceController } from './attendance.controller'; import { AttendanceService } from './attendance.service'; @Module({ controllers: [AttendanceController], providers: [AttendanceService] }) export class AttendanceModule {} ================================================ FILE: src/modules/attendance/attendance.service.ts ================================================ import { Injectable, ForbiddenException, NotFoundException } from '@nestjs/common' import { AttendanceEntity } from './entity/attendance.entity' import { CreateAttendanceDto } from './dto/create-attendance.dto' import { getMongoRepository } from 'typeorm' import { ReplaceAttendanceDto } from './dto/replace-attendance.dto' import { StudentEntity } from '../../modules/students/entity/student.entity' // let date = new Date() // console.log('startOfDay', date.setHours(0, 0, 0, 0)) // console.log('endOfDay', date.setHours(23, 59, 59, 999)) export type Attendance = any @Injectable() export class AttendanceService { async findAll(): Promise { return getMongoRepository(AttendanceEntity).find() } async insert( createAttendanceDto: CreateAttendanceDto ): Promise { const { studentId } = createAttendanceDto const foundStudent = await getMongoRepository(StudentEntity).findOne({ _id: studentId }) if (!foundStudent) { throw new NotFoundException('Student not found.') } const date = new Date() const existedAttendance = await getMongoRepository( AttendanceEntity ).findOne({ where: { studentId, createdAt: { $gt: date.setHours(0, 0, 0, 0), $lt: date.setHours(23, 59, 59, 999) } } }) if (existedAttendance) { throw new ForbiddenException('Attendance already existed.') } const newAttendance = await getMongoRepository(AttendanceEntity).save( new AttendanceEntity({ ...createAttendanceDto }) ) return newAttendance } async findOne(_id: string): Promise { const foundAttendance = await getMongoRepository(AttendanceEntity).findOne({ _id }) if (!foundAttendance) { throw new NotFoundException('Attendance not found.') } return foundAttendance } async findOneAndReplace( _id: string, replaceAttendanceDto: ReplaceAttendanceDto ): Promise { const foundAttendance = await getMongoRepository(AttendanceEntity).findOne({ _id }) if (!foundAttendance) { throw new NotFoundException('Attendance not found.') } const updateAttendance = await getMongoRepository(AttendanceEntity).save( new AttendanceEntity({ ...foundAttendance, ...replaceAttendanceDto }) ) return updateAttendance } async deleteOne(_id: string): Promise { const foundAttendance = await getMongoRepository(AttendanceEntity).findOne({ _id }) if (!foundAttendance) { throw new NotFoundException('Attendance not found.') } return ( (await getMongoRepository(AttendanceEntity).delete(foundAttendance)) && true ) } } ================================================ FILE: src/modules/attendance/dto/create-attendance.dto.ts ================================================ import { ApiModelProperty } from '@nestjs/swagger' import { IsNotEmpty } from 'class-validator' export class CreateAttendanceDto { @ApiModelProperty({ default: 'xxxx-xxxx-xxxx-xxxx', example: 'xxxx-xxxx-xxxx-xxxx', description: 'The studentId of the Student' }) @IsNotEmpty() readonly studentId: string @ApiModelProperty({ default: true, example: true, description: 'The present of the Student' }) @IsNotEmpty() readonly present: boolean } ================================================ FILE: src/modules/attendance/dto/replace-attendance.dto.ts ================================================ import { ApiModelProperty } from '@nestjs/swagger' import { IsNotEmpty } from 'class-validator' export class ReplaceAttendanceDto { @ApiModelProperty({ default: true, example: true, description: 'The present of the Student' }) @IsNotEmpty() readonly present: boolean } ================================================ FILE: src/modules/attendance/entity/attendance.entity.ts ================================================ import { Entity, Column, ObjectIdColumn } from 'typeorm' import { ApiModelProperty } from '@nestjs/swagger' import { uuidv4 } from '../../../utils' @Entity({ name: 'attendance', orderBy: { createdAt: 'ASC' } }) export class AttendanceEntity { @ApiModelProperty({ description: 'The _id of the Attendance' }) @ObjectIdColumn() _id: string @ApiModelProperty({ description: 'The studentId of the Attendance' }) @Column() studentId: string @ApiModelProperty({ description: 'The present of the Attendance' }) @Column() present: boolean @ApiModelProperty({ description: 'The createdAt of the Attendance' }) @Column() createdAt: number @ApiModelProperty({ description: 'The updatedAt of the Attendance' }) @Column() updatedAt: number constructor(partial: Partial) { if (partial) { Object.assign(this, partial) this._id = this._id || uuidv4() this.createdAt = this.createdAt || +new Date() this.updatedAt = +new Date() } } } ================================================ FILE: src/modules/banners/banner.entity.ts ================================================ import { Entity, ObjectIdColumn, Column } from 'typeorm' import { uuidv4 } from '../../utils' import { ApiModelProperty } from '@nestjs/swagger' @Entity({ name: 'banners', orderBy: { createdAt: 'ASC' } }) export class BannerEntity { @ApiModelProperty({ description: 'The _id of the Banner' }) @ObjectIdColumn() _id: string @ApiModelProperty({ description: 'The title of the Banner' }) @Column() title: string @ApiModelProperty({ description: 'The imageUrl of the Banner' }) @Column() imageUrl: string @ApiModelProperty({ description: 'The position of the Banner' }) @Column() position: number @ApiModelProperty({ description: 'The detail of the Banner' }) @Column() detail: string @ApiModelProperty({ description: 'The published of the Banner' }) @Column() published: boolean @ApiModelProperty({ description: 'The createdAt of the Banner' }) @Column() createdAt: number @ApiModelProperty({ description: 'The updatedAt of the Banner' }) @Column() updatedAt: number constructor(partial: Partial) { if (partial) { Object.assign(this, partial) this._id = this._id || uuidv4() this.createdAt = this.createdAt || +new Date() this.updatedAt = +new Date() } } } ================================================ FILE: src/modules/banners/banners.controller.ts ================================================ import { Controller, Get, Query, Post, Body, Request, UseGuards, Put, Param } from '@nestjs/common'; import { BannersService, Banner } from './banners.service'; import { ApiOperation, ApiImplicitQuery, ApiUseTags, ApiBearerAuth, ApiResponse } from '@nestjs/swagger'; import { CreateBannerDto } from './dto/create-banner.dto'; import { AuthGuard } from '@nestjs/passport'; import { BannerEntity } from './banner.entity'; import { ReplaceBannerDto } from './dto/replace-banner.dto'; import { ErrorResponseDto } from '../users/dto/error-response.dto'; @ApiResponse({ status: 401, description: 'Unauthorized.', type: ErrorResponseDto }) @ApiResponse({ status: 403, description: 'Forbidden.', type: ErrorResponseDto }) @ApiUseTags('banners') @Controller('banners') export class BannersController { constructor(private readonly bannersService: BannersService) { } @ApiResponse({ status: 200, description: 'The found records', type: [BannerEntity] }) @ApiOperation({ title: 'Retrieve many Banners 👻' // description: 'Aaa', // operationId: 'aaaa' }) @Get() @ApiImplicitQuery({ name: 'limit', description: 'The maximum number of transactions to return', required: false, type: Number }) @ApiImplicitQuery({ name: 'offset', description: 'The maximum number of transactions to skip', required: false, type: Number }) findAll(@Query() query): Promise { return this.bannersService.findAll(query) } @ApiResponse({ status: 201, description: 'The record has been successfully created.', type: BannerEntity }) @ApiBearerAuth() @UseGuards(AuthGuard('jwt')) @ApiOperation({ title: 'Create one Banner 👻' }) @Post() insert(@Body() createBannerDto: CreateBannerDto): Promise { return this.bannersService.insert(createBannerDto) } @ApiResponse({ status: 200, description: 'The found record is executed', type: Boolean }) @ApiBearerAuth() @UseGuards(AuthGuard('jwt')) @ApiOperation({ title: 'Replace one Banner 👻' }) @Put(':id') replace(@Param('id') id: string, @Body() replaceBannerDto: ReplaceBannerDto): Promise { return this.bannersService.findOneAndReplace(id, replaceBannerDto) } } ================================================ FILE: src/modules/banners/banners.module.ts ================================================ import { Module } from '@nestjs/common'; import { BannersController } from './banners.controller'; import { BannersService } from './banners.service'; @Module({ controllers: [BannersController], providers: [BannersService] }) export class BannersModule {} ================================================ FILE: src/modules/banners/banners.service.ts ================================================ import { Injectable, ForbiddenException, NotFoundException } from '@nestjs/common' import { getMongoRepository } from 'typeorm' import { BannerEntity } from './banner.entity' import { CreateBannerDto } from './dto/create-banner.dto' import { ReplaceBannerDto } from './dto/replace-banner.dto' export type Banner = any @Injectable() export class BannersService { async findAll(query): Promise { const { offset, limit } = query if (offset < 1) { throw new ForbiddenException('The offset must be greater than 0') } if (limit < 1) { throw new ForbiddenException('The offset must be greater than 0') } return getMongoRepository(BannerEntity).find({ where: { published: true }, skip: +offset | 0, take: +limit | 100 }) } async insert(createBannerDto: CreateBannerDto): Promise { const { position } = createBannerDto const foundBanner = await getMongoRepository(BannerEntity).findOne({ position, published: true }) if (foundBanner) { throw new ForbiddenException('Banner already published') } const newAddress = await getMongoRepository(BannerEntity).save( new BannerEntity({ ...createBannerDto }) ) return newAddress && true } async findOneAndReplace( _id: string, replaceBannerDto: ReplaceBannerDto ): Promise { const { position } = replaceBannerDto let foundBanner = await getMongoRepository(BannerEntity).findOne({ position, published: true }) if (foundBanner) { throw new ForbiddenException('Banner already published') } foundBanner = await getMongoRepository(BannerEntity).findOne({ _id }) if (!foundBanner) { throw new NotFoundException('Banner not found') } const updateBanner = await getMongoRepository(BannerEntity).save( new BannerEntity({ ...foundBanner, ...replaceBannerDto }) ) return updateBanner && true } } ================================================ FILE: src/modules/banners/dto/create-banner.dto.ts ================================================ import { ApiModelProperty } from '@nestjs/swagger' import { IsNotEmpty, IsOptional, Min, Max, IsNumber, IsBoolean } from 'class-validator' export class CreateBannerDto { @ApiModelProperty({ default: 'Enjoy Chewapp for Free', example: 'Enjoy Chewapp for Free', description: 'The title of the Banner' }) @IsNotEmpty() readonly title: string @ApiModelProperty({ default: 'https://xxxxxxxxx', example: 'https://xxxxxxxxx', description: 'The imageUrl of the Banner' }) @IsNotEmpty() readonly imageUrl: string @ApiModelProperty({ default: 1, example: 1, description: 'The position of the Banner' }) @Max(20) @Min(1) @IsNumber() @IsNotEmpty() readonly position: number @ApiModelProperty({ default: '

Hello

', example: '

Hello

', description: 'The detail of the Banner' }) @IsOptional() readonly detail: string @ApiModelProperty({ default: false, example: false, description: 'The published of the Banner' }) @IsBoolean() @IsNotEmpty() readonly published: boolean } ================================================ FILE: src/modules/banners/dto/replace-banner.dto.ts ================================================ import { ApiModelProperty } from '@nestjs/swagger' import { IsNotEmpty, IsOptional, Min, Max, IsNumber, IsBoolean } from 'class-validator' export class ReplaceBannerDto { @ApiModelProperty({ default: 'Enjoy Chewapp for Free', example: 'Enjoy Chewapp for Free', description: 'The title of the Banner' }) @IsNotEmpty() readonly title: string @ApiModelProperty({ default: 'https://xxxxxxxxx', example: 'https://xxxxxxxxx', description: 'The imageUrl of the Banner' }) @IsNotEmpty() readonly imageUrl: string @ApiModelProperty({ default: 1, example: 1, description: 'The position of the Banner' }) @Max(20) @Min(1) @IsNumber() @IsNotEmpty() readonly position: number @ApiModelProperty({ default: '

Hello

', example: '

Hello

', description: 'The detail of the Banner' }) @IsOptional() readonly detail: string @ApiModelProperty({ default: false, example: false, description: 'The published of the Banner' }) @IsBoolean() @IsNotEmpty() readonly published: boolean } ================================================ FILE: src/modules/bidders/bidders.controller.ts ================================================ import { Controller } from '@nestjs/common'; @Controller('bidders') export class BiddersController {} ================================================ FILE: src/modules/bidders/bidders.module.ts ================================================ import { Module } from '@nestjs/common'; import { BiddersService } from './bidders.service'; import { BiddersController } from './bidders.controller'; @Module({ providers: [BiddersService], controllers: [BiddersController] }) export class BiddersModule {} ================================================ FILE: src/modules/bidders/bidders.service.ts ================================================ import { Injectable } from '@nestjs/common'; @Injectable() export class BiddersService {} ================================================ FILE: src/modules/chats/chats.gateway.ts ================================================ import { SubscribeMessage, WebSocketGateway, OnGatewayInit, WebSocketServer, OnGatewayConnection, OnGatewayDisconnect } from '@nestjs/websockets' import { Logger } from '@nestjs/common' import { Socket, Server } from 'socket.io' @WebSocketGateway() export class ChatsGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect { @WebSocketServer() server: Server private logger: Logger = new Logger('AppGateway') @SubscribeMessage('msgToServer') handleMessage(client: Socket, payload: string): void { this.server.emit('msgToClient', payload) } afterInit(server: Server) { this.logger.log('Init') } handleDisconnect(client: Socket) { this.logger.log(`Client disconnected: ${client.id}`) } handleConnection(client: Socket, ...args: any[]) { this.logger.log(`Client connected: ${client.id}`) } } ================================================ FILE: src/modules/chats/chats.module.ts ================================================ import { Module } from '@nestjs/common' @Module({}) export class ChatsModule {} ================================================ FILE: src/modules/classes/classes.controller.ts ================================================ import { Controller, Get, Post, Patch, Delete, Param, UseGuards, Put, Body } from '@nestjs/common' import { ApiUseTags, ApiResponse, ApiBearerAuth, ApiOperation } from '@nestjs/swagger' import { ClassEntity } from './entity/class.entity' import { ClassesService } from './classes.service' import { AuthGuard } from '@nestjs/passport' import { CreateClassDto } from './dto/create-class.dto' import { ReplaceClassDto } from './dto/replace-class.dto' // @ApiBearerAuth() // @UseGuards(AuthGuard('jwt')) @ApiUseTags('classes') @Controller('classes') export class ClassesController { constructor(private readonly classesService: ClassesService) {} @ApiResponse({ status: 200, description: 'The found records', type: [ClassEntity] }) @ApiOperation({ title: 'Retrieve many Classs 👾' }) @Get() findAll() { return this.classesService.findAll() } @ApiResponse({ status: 200, description: 'The found record', type: ClassEntity }) @ApiOperation({ title: 'Create one Class 👾' }) @Post() async insert(@Body() createClassDto: CreateClassDto) { const newClass = await this.classesService.insert(createClassDto) return newClass } @ApiResponse({ status: 200, description: 'The found record', type: ClassEntity }) @ApiOperation({ title: 'Retrieve one Class 👾' }) @Get(':id') findOne(@Param('id') id: string) { return this.classesService.findOne(id) } @ApiOperation({ title: 'Replace one Class 👾' }) @Put(':id') replace(@Param('id') id: string, @Body() replaceClassDto: ReplaceClassDto) { return this.classesService.findOneAndReplace(id, replaceClassDto) } @ApiResponse({ status: 200, description: 'The found record is executed 👾', type: Boolean }) @ApiOperation({ title: 'Delete one Class 👾' }) @Delete(':id') remove(@Param('id') id: string) { return this.classesService.deleteOne(id) } } ================================================ FILE: src/modules/classes/classes.module.ts ================================================ import { Module } from '@nestjs/common'; import { ClassesController } from './classes.controller'; import { ClassesService } from './classes.service'; @Module({ controllers: [ClassesController], providers: [ClassesService] }) export class ClassesModule {} ================================================ FILE: src/modules/classes/classes.service.ts ================================================ import { Injectable, NotFoundException, ForbiddenException } from '@nestjs/common' import { CreateClassDto } from './dto/create-class.dto' import { ClassEntity } from './entity/class.entity' import { getMongoRepository } from 'typeorm' import { ReplaceClassDto } from './dto/replace-class.dto' export type Class = any @Injectable() export class ClassesService { async findAll(): Promise { return getMongoRepository(ClassEntity).find() } async insert(createClassDto: CreateClassDto): Promise { const { name } = createClassDto const existedClass = await getMongoRepository(ClassEntity).findOne({ name }) if (existedClass) { throw new ForbiddenException('Name already existed.') } const newClass = await getMongoRepository(ClassEntity).save( new ClassEntity({ ...createClassDto }) ) return newClass } async findOne(_id: string): Promise { const foundClass = await getMongoRepository(ClassEntity).findOne({ _id }) if (!foundClass) { throw new NotFoundException('Class not found.') } return foundClass } async findOneAndReplace( _id: string, replaceClassDto: ReplaceClassDto ): Promise { const foundClass = await getMongoRepository(ClassEntity).findOne({ _id }) if (!foundClass) { throw new NotFoundException('Class not found.') } const updateClass = await getMongoRepository(ClassEntity).save( new ClassEntity({ ...foundClass, ...replaceClassDto }) ) return updateClass } async deleteOne(_id: string): Promise { const foundClass = await getMongoRepository(ClassEntity).findOne({ _id }) if (!foundClass) { throw new NotFoundException('Class not found.') } return (await getMongoRepository(ClassEntity).delete(foundClass)) && true } } ================================================ FILE: src/modules/classes/dto/create-class.dto.ts ================================================ import { ApiModelProperty } from '@nestjs/swagger' import { IsNotEmpty } from 'class-validator' export class CreateClassDto { @ApiModelProperty({ default: '11A1', example: '11A1', description: 'The name of the Class' }) @IsNotEmpty() readonly name: string @ApiModelProperty({ default: 'TVK', example: 'TVK', description: 'The school of the Class' }) @IsNotEmpty() readonly school: string } ================================================ FILE: src/modules/classes/dto/replace-class.dto.ts ================================================ import { ApiModelProperty } from '@nestjs/swagger' import { IsNotEmpty } from 'class-validator' export class ReplaceClassDto { @ApiModelProperty({ default: '11A1', example: '11A1', description: 'The name of the Class' }) @IsNotEmpty() readonly name: string @ApiModelProperty({ default: 'TVK', example: 'TVK', description: 'The school of the Class' }) @IsNotEmpty() readonly school: string } ================================================ FILE: src/modules/classes/entity/class.entity.ts ================================================ import { Entity, Column, ObjectIdColumn } from 'typeorm' import { ApiModelProperty } from '@nestjs/swagger' import { uuidv4 } from '../../../utils' @Entity({ name: 'classes', orderBy: { createdAt: 'ASC' } }) export class ClassEntity { @ApiModelProperty({ description: 'The _id of the Class' }) @ObjectIdColumn() _id: string @ApiModelProperty({ description: 'The name of the Class' }) @Column() name: string @ApiModelProperty({ description: 'The school of the Class' }) @Column() school: string @ApiModelProperty({ description: 'The createdAt of the Class' }) @Column() createdAt: number @ApiModelProperty({ description: 'The updatedAt of the Class' }) @Column() updatedAt: number constructor(partial: Partial) { if (partial) { Object.assign(this, partial) this._id = this._id || uuidv4() this.createdAt = this.createdAt || +new Date() this.updatedAt = +new Date() } } } ================================================ FILE: src/modules/connections/connection.entity.ts ================================================ import { Entity, ObjectIdColumn, Column } from 'typeorm' import { uuidv4 } from '../../utils' // import { Exclude, plainToClass } from 'class-transformer' import { ApiModelProperty } from '@nestjs/swagger' import { ConnectionType } from './enum/connection.enum' import { CreatedByDto } from '../../modules/users/dto/created-by.dto' @Entity({ name: 'connections', orderBy: { createdAt: 'ASC' } }) export class ConnectionEntity { @ApiModelProperty({ description: 'The _id of the Connection' }) @ObjectIdColumn() _id: string @ApiModelProperty({ description: 'The deal of the Connection' }) @Column() deal: string @ApiModelProperty({ description: 'The amount of the Connection' }) @Column() amount: number @ApiModelProperty({ description: 'The connectionType of the Connection' }) @Column() connectionType: ConnectionType @ApiModelProperty({ description: 'The createdBy of the Connection' }) @Column() connectedBy: CreatedByDto @ApiModelProperty({ description: 'The createdAt of the Connection' }) @Column() createdAt: number @ApiModelProperty({ description: 'The updatedAt of the Connection' }) @Column() updatedAt: number constructor(partial: Partial) { if (partial) { Object.assign(this, partial) this._id = this._id || uuidv4() this.connectionType = ConnectionType.Connected this.createdAt = this.createdAt || +new Date() this.updatedAt = +new Date() } } } ================================================ FILE: src/modules/connections/connections.controller.ts ================================================ import { Controller, Get, Query, Request, UseGuards, Post, Body, Param } from '@nestjs/common' import { ConnectionsService, Connection } from './connections.service' import { ConnectionEntity } from './connection.entity' import { ApiResponse, ApiOperation, ApiImplicitQuery, ApiBearerAuth, ApiUseTags } from '@nestjs/swagger' import { AuthGuard } from '@nestjs/passport' import { ErrorResponseDto } from '../../modules/users/dto/error-response.dto' @ApiBearerAuth() @UseGuards(AuthGuard('jwt')) @ApiResponse({ status: 401, description: 'Unauthorized.', type: ErrorResponseDto }) @ApiResponse({ status: 403, description: 'Forbidden.', type: ErrorResponseDto }) @ApiUseTags('connections') @Controller('connections') export class ConnectionsController { constructor(private readonly connectionsService: ConnectionsService) { } @ApiResponse({ status: 200, description: 'The found records', type: [ConnectionEntity] }) @ApiOperation({ title: 'Retrieve many Connections 👻' // description: 'Aaa', // operationId: 'aaaa' }) @Get() @ApiImplicitQuery({ name: 'limit', description: 'The maximum number of transactions to return', required: false, type: Number }) @ApiImplicitQuery({ name: 'offset', description: 'The maximum number of transactions to skip', required: false, type: Number }) findAll(@Query() query, @Request() req): Promise { return this.connectionsService.findAll(query, req) } @ApiResponse({ status: 201, description: 'The record has been successfully created', type: ConnectionEntity }) @ApiOperation({ title: 'Create one Connection 👻' }) @Post('/:dealId') insert(@Param('dealId') dealId: string, @Request() req): Promise { return this.connectionsService.insert(dealId, req) } } ================================================ FILE: src/modules/connections/connections.module.ts ================================================ import { Module } from '@nestjs/common'; import { ConnectionsController } from './connections.controller'; import { ConnectionsService } from './connections.service'; @Module({ controllers: [ConnectionsController], providers: [ConnectionsService] }) export class ConnectionsModule {} ================================================ FILE: src/modules/connections/connections.service.ts ================================================ import { Injectable, ForbiddenException } from '@nestjs/common' import { getMongoRepository } from 'typeorm' import { ConnectionEntity } from './connection.entity' import { UserEntity } from '../../modules/users/user.entity' import { DealEntity } from '../../modules/deals/deal.entity' export type Connection = any @Injectable() export class ConnectionsService { async findAll(query: any, req: any): Promise { const { offset, limit } = query const { user } = req const { _id } = user const pipelineArray = [] if (offset) { if (offset < 1) { throw new ForbiddenException('The offset must be greater than 0') } else { pipelineArray.push({ $skip: +offset }) } } if (limit) { if (limit < 1) { throw new ForbiddenException('The limit must be greater than 0') } else { pipelineArray.push({ $limit: +limit }) } } const deal = [ { $lookup: { from: 'deals', localField: 'deal', foreignField: '_id', as: 'deal' } }, { $project: { 'deal.serviceType': 0, 'deal.itemType': 0, 'deal.description': 0, 'deal.shopName': 0, 'deal.location': 0, 'deal.destination': 0, 'deal.payment': 0, 'deal.expiredAt': 0, 'deal.createdAt': 0, 'deal.updatedAt': 0 } }, { $unwind: { path: '$deal', preserveNullAndEmptyArrays: true } } ] const match = [ { $match: { $or: [{ connectedBy: _id }, { 'deal.createdBy': _id }] } } ] const dealCreatedBy = [ { $lookup: { from: 'users', localField: 'deal.createdBy', foreignField: '_id', as: 'deal.createdBy' } }, { $project: { 'deal.createdBy.email': 0, 'deal.createdBy.password': 0, 'deal.createdBy.referralCode': 0, 'deal.createdBy.verified': 0, 'deal.createdBy.createdAt': 0, 'deal.createdBy.updatedAt': 0, 'deal.createdBy.phone': 0 } }, { $unwind: { path: '$deal.createdBy', preserveNullAndEmptyArrays: true } } ] const createdBy = [ { $lookup: { from: 'users', localField: 'connectedBy', foreignField: '_id', as: 'connectedBy' } }, { $project: { 'connectedBy.email': 0, 'connectedBy.password': 0, 'connectedBy.referralCode': 0, 'connectedBy.verified': 0, 'connectedBy.createdAt': 0, 'connectedBy.updatedAt': 0, 'connectedBy.phone': 0 } }, { $unwind: { path: '$connectedBy', preserveNullAndEmptyArrays: true } } ] pipelineArray.push(...deal, ...match, ...dealCreatedBy, ...createdBy) return await getMongoRepository(ConnectionEntity) .aggregate(pipelineArray) .toArray() } async insert(deal: string, req: any): Promise { const { user } = req const { _id } = user const foundDeal = await getMongoRepository(DealEntity).findOne({ _id: deal }) if (!foundDeal) { throw new ForbiddenException('Deal not found') } const foundAddress = await getMongoRepository(ConnectionEntity).findOne({ deal, connectedBy: _id }) if (foundAddress) { throw new ForbiddenException('Connection already existed') } const newConnection = await getMongoRepository(ConnectionEntity).save( new ConnectionEntity({ deal, connectedBy: _id }) ) const connectedBy = await getMongoRepository(UserEntity).findOne({ where: { _id: newConnection.connectedBy }, select: ['_id', 'name', 'avatar'] }) newConnection.connectedBy = connectedBy return newConnection } } ================================================ FILE: src/modules/connections/dto/create-connection.dto.ts ================================================ import { ApiModelProperty } from '@nestjs/swagger' import { IsNotEmpty, IsEnum, IsOptional } from 'class-validator' import { ConnectionType } from '../enum/connection.enum' import { Position } from '../../deals/entity/position.entity' export class CreateConnectionDto { @ApiModelProperty({ default: 'xxxx-xxxx-xxxx-xxxx', example: 'xxxx-xxxx-xxxx-xxxx', description: 'The deal of the Connection' }) @IsOptional() readonly deal: string @ApiModelProperty({ default: { latitude: 10.780230999999999, longitude: 106.6645121 }, example: { latitude: 10.780230999999999, longitude: 106.6645121 }, description: 'The location of the Connection' }) @IsNotEmpty() readonly location: Position @ApiModelProperty({ default: 69, example: 69, description: 'The unitNumber of the Connection' }) @IsNotEmpty() // 30m 1h 1h30 2h readonly unitNumber: number @ApiModelProperty({ default: 'Đối diện BigC', example: 'Đối diện BigC', description: 'The remarks of the Connection' }) @IsOptional() readonly remarks: string } ================================================ FILE: src/modules/connections/enum/connection.enum.ts ================================================ export enum ConnectionType { Connected = 'Connected', Accepted = 'Accepted', Declined = 'Declined', Completed = 'Completed' } ================================================ FILE: src/modules/deals/deal.entity.ts ================================================ import { Entity, ObjectIdColumn, Column } from 'typeorm' import { uuidv4 } from '../../utils' // import { Exclude, plainToClass } from 'class-transformer' import { ApiModelProperty } from '@nestjs/swagger' import { ItemType, ServiceType, PaymentType, DealType } from './enum/deal.enum' import { Position } from './entity/position.entity' import { UserEntity } from '../users/user.entity' import { CreatedByDto } from '../users/dto/created-by.dto' @Entity({ name: 'deals', orderBy: { createdAt: 'ASC' } }) export class DealEntity { @ApiModelProperty({ description: 'The _id of the Deal' }) @ObjectIdColumn() _id: string @ApiModelProperty({ description: 'The dealType of the Deal' }) @Column() dealType: DealType @ApiModelProperty({ description: 'The serviceType of the Deal' }) @Column() serviceType: ServiceType @ApiModelProperty({ description: 'The itemType of the Deal' }) @Column() itemType: ItemType @ApiModelProperty({ description: 'The items of the Deal' }) @Column() items: string @ApiModelProperty({ description: 'The description of the Deal' }) @Column() description: string @ApiModelProperty({ description: 'The shopName of the Deal' }) @Column() shopName: string @ApiModelProperty({ description: 'The thumbnail of the Deal' }) @Column() thumbnail: string @ApiModelProperty({ description: 'The location of the Deal' }) @Column() location: Position @ApiModelProperty({ description: 'The destination of the Deal' }) @Column() // I have no preference auto get location device destination: Position @ApiModelProperty({ description: 'The expiredAt of the Deal' }) @Column() // 30m 1h 1h30 2h expiredAt: number @ApiModelProperty({ description: 'The payment of the Deal' }) @Column() payment: PaymentType @ApiModelProperty({ description: 'The createdBy of the Deal' }) @Column() createdBy: CreatedByDto @ApiModelProperty({ description: 'The createdAt of the Deal' }) @Column() createdAt: number @ApiModelProperty({ description: 'The updatedAt of the Deal' }) @Column() updatedAt: number constructor(partial: Partial) { if (partial) { Object.assign(this, partial) this._id = this._id || uuidv4() this.createdAt = this.createdAt || +new Date() this.updatedAt = +new Date() } } } ================================================ FILE: src/modules/deals/deals.controller.ts ================================================ import { Controller, Get, Post, Body, Request, UseInterceptors, UploadedFile, UseGuards, Param, Query } from '@nestjs/common' import { DealsService } from './deals.service' import { ApiOperation, ApiResponse, ApiUseTags, ApiImplicitQuery, ApiConsumes, ApiImplicitFile, ApiBearerAuth } from '@nestjs/swagger' import { CreateDealDto } from './dto/create-deal.dto' import { FileInterceptor } from '@nestjs/platform-express' import { AuthGuard } from '@nestjs/passport' import { ItemType, ServiceType, DealType } from './enum/deal.enum' import { ErrorResponseDto } from '../../modules/users/dto/error-response.dto' import { DealEntity } from './deal.entity' import { DealResponseDto } from './dto/deal-response.dto' import { Position } from './entity/position.entity' @ApiResponse({ status: 401, description: 'Unauthorized.', type: ErrorResponseDto }) @ApiResponse({ status: 403, description: 'Forbidden.', type: ErrorResponseDto }) @ApiUseTags('deals') @Controller('deals') export class DealsController { constructor(private readonly dealsService: DealsService) {} @ApiResponse({ status: 200, description: 'The found records', type: [DealResponseDto] }) @ApiOperation({ title: 'Retrieve many Deals 👻' // description: 'Aaa', // operationId: 'aaaa' }) // @Get(':searchIn') @Get() @ApiImplicitQuery({ name: 'limit', description: 'The maximum number of transactions to return', required: false, type: Number }) @ApiImplicitQuery({ name: 'offset', description: 'The maximum number of transactions to skip', required: false, type: Number }) @ApiImplicitQuery({ name: 'itemType', description: 'The itemType of the Deal', required: false, type: ItemType, enum: ['None', 'Meal', 'Drinks', 'Desserts', 'Snacks'] }) @ApiImplicitQuery({ name: 'serviceType', description: 'The serviceType of the Deal', required: false, type: ServiceType, enum: [ 'FoodDelivery', 'Pickup', 'PharmacyPurchase', 'Queue', 'OverseasPurchase', 'Others' ] }) @ApiImplicitQuery({ name: 'dealType', description: 'The dealType of the Deal', required: false, type: DealType, enum: ['Request', 'Offer'] }) findAll( // @Param('searchIn') searchIn: string, @Query() query ) { // console.log('sdasd', searchIn) return this.dealsService.findAll(query) } @ApiResponse({ status: 200, description: 'The found record', type: DealResponseDto }) @ApiOperation({ title: 'Retrieve one Deal 👻' }) @Get(':id') findOne(@Param('id') id: string) { return this.dealsService.findOne(id) } @ApiBearerAuth() @UseGuards(AuthGuard('jwt')) @ApiResponse({ status: 201, description: 'The record has been successfully created.', type: DealResponseDto }) @ApiOperation({ title: 'Create one Deal 👻' }) @Post() insert(@Body() createDealDto: CreateDealDto, @Request() req) { return this.dealsService.insert(createDealDto, req) } } ================================================ FILE: src/modules/deals/deals.module.ts ================================================ import { Module } from '@nestjs/common' import { DealsController } from './deals.controller' import { DealsService } from './deals.service' @Module({ controllers: [DealsController], providers: [DealsService], exports: [DealsService] }) export class DealsModule {} ================================================ FILE: src/modules/deals/deals.service.ts ================================================ import { Injectable, NotFoundException, ForbiddenException } from '@nestjs/common' import { getMongoRepository } from 'typeorm' import * as geolib from 'geolib' import { CreateDealDto } from './dto/create-deal.dto' import { DealEntity } from './deal.entity' import { ServiceType, ItemType } from './enum/deal.enum' import { UserEntity } from '../../modules/users/user.entity' const from = { latitude: 10.783, longitude: 106.692 } const to = { latitude: 10.807, longitude: 106.709 } console.log( 'You are ', geolib.getDistance(from, to), `meters away from ${from}` ) export type Deal = any @Injectable() export class DealsService { // TODO: async findAll(query): Promise { // console.log(query) const { dealType, serviceType, itemType, offset, limit } = query const pipelineArray = [] if (offset) { if (offset < 1) { throw new ForbiddenException('The offset must be greater than 0') } else { pipelineArray.push({ $skip: +offset }) } } if (limit) { if (limit < 1) { throw new ForbiddenException('The limit must be greater than 0') } else { pipelineArray.push({ $limit: +limit }) } } const connections = [ { $lookup: { from: 'connections', localField: '_id', foreignField: 'dealId', as: 'connections' } }, { $addFields: { connections: { $size: '$connections' } } } ] const createdBy = [ { $lookup: { from: 'users', localField: 'createdBy', foreignField: '_id', as: 'createdBy' } }, { $project: { 'createdBy.email': 0, 'createdBy.password': 0, 'createdBy.referralCode': 0, 'createdBy.verified': 0, 'createdBy.createdAt': 0, 'createdBy.updatedAt': 0, 'createdBy.phone': 0 } }, { $unwind: { path: '$createdBy', preserveNullAndEmptyArrays: true } } ] pipelineArray.push(...connections, ...createdBy) // console.log(dealType) if (dealType) { pipelineArray.push({ $match: { dealType } }) } if (serviceType) { pipelineArray.push({ $match: { serviceType } }) } if (itemType) { pipelineArray.push({ $match: { itemType } }) } return await getMongoRepository(DealEntity) .aggregate(pipelineArray) .toArray() } async findOne(_id: string): Promise { const foundDeal = await getMongoRepository(DealEntity).findOne({ _id }) if (!foundDeal) { throw new NotFoundException('Deal not found') } return foundDeal } // TODO: async insert(createDealDto: CreateDealDto, req: any) { // console.log(createDealDto, file, req.user._id) try { const { user } = req const { _id } = user const { dealType, serviceType, itemType, location, destination, duration, payment } = createDealDto let convertCreateDealDto let newDeal if ( (createDealDto.serviceType === ServiceType.FoodDelivery && createDealDto.itemType === ItemType.None) || (createDealDto.serviceType !== ServiceType.FoodDelivery && createDealDto.itemType !== ItemType.None) ) { throw new ForbiddenException('Service type and Item type is incorrect.') } // if (file && file.size > 1024 * 1024 * 2) { // throw new ForbiddenException('The thumbnail is too large to upload') // } // console.log(createDealDto) if (createDealDto.items === 'Anything') { convertCreateDealDto = { dealType, serviceType, itemType, location: JSON.parse(location.toString()), destination: JSON.parse(destination.toString()), duration, payment } newDeal = await getMongoRepository(DealEntity).save( new DealEntity(convertCreateDealDto) ) } else { // console.log(file) // if (!file) { // throw new ForbiddenException('Thumbnail not found.') // } // const thumbnail = await uploadFile(file) convertCreateDealDto = { ...createDealDto, // thumbnail, expiredAt: +new Date() + 1000 * createDealDto.duration, createdBy: _id } delete convertCreateDealDto.duration if (createDealDto.serviceType !== ServiceType.FoodDelivery) { delete convertCreateDealDto.itemType } newDeal = await getMongoRepository(DealEntity).save( new DealEntity(convertCreateDealDto) ) } const createdBy = await getMongoRepository(UserEntity).findOne({ where: { _id: newDeal.createdBy }, select: ['_id', 'name', 'avatar'] }) newDeal.createdBy = createdBy return newDeal } catch (error) { throw new Error(error) } } async findByUserId(req: any, query: any): Promise { const { user } = req const { _id } = user const { dealType } = query console.log(_id) const pipelineArray = [] if (_id) { pipelineArray.push({ $match: { createdBy: _id } }) } if (dealType) { pipelineArray.push({ $match: { dealType } }) } const connections = [ { $lookup: { from: 'connections', localField: '_id', foreignField: 'dealId', as: 'connections' } }, { $addFields: { connections: { $size: '$connections' } } } ] const createdBy = [ { $lookup: { from: 'users', localField: 'createdBy', foreignField: '_id', as: 'createdBy' } }, { $project: { 'createdBy.email': 0, 'createdBy.password': 0, 'createdBy.referralCode': 0, 'createdBy.verified': 0, 'createdBy.createdAt': 0, 'createdBy.updatedAt': 0, 'createdBy.phone': 0 } }, { $unwind: { path: '$createdBy', preserveNullAndEmptyArrays: true } } ] pipelineArray.push(...connections, ...createdBy) return await getMongoRepository(DealEntity) .aggregate(pipelineArray) .toArray() } } ================================================ FILE: src/modules/deals/dto/create-deal.dto.ts ================================================ import { ApiModelProperty, ApiModelPropertyOptional } from '@nestjs/swagger' import { IsNotEmpty, IsEnum, IsOptional } from 'class-validator' import { ItemType, ServiceType, PaymentType, DealType } from '../enum/deal.enum' import { Position } from '../entity/position.entity' export class CreateDealDto { @ApiModelProperty({ enum: ['Request', 'Offer'], example: 'Request', description: 'The deal type of the Deal' }) @IsEnum(DealType) @IsNotEmpty() readonly dealType: DealType @ApiModelProperty({ enum: [ 'FoodDelivery', 'Pickup', 'PharmacyPurchase', 'Queue', 'OverseasPurchase', 'Others' ], example: 'FoodDelivery', description: 'The service type of the Deal' }) @IsEnum(ServiceType) @IsNotEmpty() readonly serviceType: ServiceType @ApiModelProperty({ enum: ['None', 'Meal', 'Drinks', 'Desserts', 'Snacks'], example: 'Meal', description: 'The item type of the Deal' }) @IsEnum(ItemType) @IsNotEmpty() readonly itemType: ItemType @ApiModelProperty({ default: 'Nui xào bò lúc lắc', example: 'Nui xào bò lúc lắc', required: false, description: 'The items of the Deal' }) @IsOptional() readonly items: string @ApiModelProperty({ default: 'Noodle beef cube', example: 'Noodle beef cube', required: false, description: 'The description of the Deal' }) @IsOptional() readonly description: string @ApiModelProperty({ default: 'Tâm Ký', example: 'Tâm Ký', required: false, description: 'The shopName of the Deal' }) @IsOptional() readonly shopName: string @ApiModelProperty({ default: 'https://xxx.xxx', example: 'https://xxx.xxx', required: false, description: 'The thumbnail of the Deal' }) @IsOptional() thumbnail: string @ApiModelPropertyOptional({ default: { latitude: 10.780230999999999, longitude: 106.6645121 }, example: { latitude: 10.780230999999999, longitude: 106.6645121 }, description: 'The location of the Deal' }) @IsNotEmpty() readonly location: Position @ApiModelPropertyOptional({ default: { latitude: 10.780230999999999, longitude: 106.6645121 }, example: { latitude: 10.780230999999999, longitude: 106.6645121 }, description: 'The destination of the Deal' }) @IsNotEmpty() // I have no preference auto get location device readonly destination: Position @ApiModelProperty({ default: 60 * 60 * 30, example: 60 * 60 * 30, description: 'The duration for the expiredAt of the Deal' }) @IsNotEmpty() // 30m 1h 1h30 2h readonly duration: number @ApiModelProperty({ enum: ['Chewpay', 'Cod'], description: 'The payment of the Deal' }) @IsEnum(PaymentType) @IsNotEmpty() readonly payment: PaymentType } ================================================ FILE: src/modules/deals/dto/deal-response.dto.ts ================================================ import { ApiModelProperty, ApiModelPropertyOptional } from '@nestjs/swagger' import { IsNotEmpty, IsEnum, IsOptional } from 'class-validator' import { ItemType, ServiceType, PaymentType, DealType } from '../enum/deal.enum' import { Position } from '../entity/position.entity' export class DealResponseDto { @ApiModelProperty({ enum: ['Request', 'Offer'], example: 'Request', description: 'The deal type of the Deal' }) @IsEnum(DealType) @IsNotEmpty() readonly dealType: DealType @ApiModelProperty({ enum: [ 'FoodDelivery', 'Pickup', 'PharmacyPurchase', 'Queue', 'OverseasPurchase', 'Others' ], example: 'FoodDelivery', description: 'The service type of the Deal' }) @IsEnum(ServiceType) @IsNotEmpty() readonly serviceType: ServiceType @ApiModelProperty({ enum: ['None', 'Meal', 'Drinks', 'Desserts', 'Snacks'], example: 'Meal', description: 'The item type of the Deal' }) @IsEnum(ItemType) @IsNotEmpty() readonly itemType: ItemType @ApiModelProperty({ example: 'Nui xào bò lúc lắc', required: false, description: 'The items of the Deal' }) @IsOptional() readonly items: string @ApiModelProperty({ example: 'Noodle beef cube', required: false, description: 'The description of the Deal' }) @IsOptional() readonly description: string @ApiModelProperty({ example: 'Tâm Ký', required: false, description: 'The shopName of the Deal' }) @IsOptional() readonly shopName: string @ApiModelProperty({ example: 'https://xxx.xxx', required: false, description: 'The thumbnail of the Deal' }) @IsOptional() thumbnail: string @ApiModelPropertyOptional({ example: { latitude: 10.780230999999999, longitude: 106.6645121 }, description: 'The location of the Deal' }) @IsNotEmpty() readonly location: Position @ApiModelPropertyOptional({ example: { latitude: 10.780230999999999, longitude: 106.6645121 }, description: 'The destination of the Deal' }) @IsNotEmpty() // I have no preference auto get location device readonly destination: Position @ApiModelProperty({ example: +new Date() + 60 * 60 * 30, description: 'The expiredAtt of the Deal' }) @IsNotEmpty() // 30m 1h 1h30 2h readonly expiredAt: number @ApiModelProperty({ enum: ['Chewpay', 'Cod'], description: 'The payment of the Deal' }) @IsEnum(PaymentType) @IsNotEmpty() readonly payment: PaymentType @ApiModelProperty({ example: 10, description: 'The connections of the Deal' }) @IsNotEmpty() readonly connections: number @ApiModelProperty({ example: +new Date(), description: 'The createdAt of the Deal' }) @IsNotEmpty() readonly createdAt: number @ApiModelProperty({ example: +new Date(), description: 'The updatedAt of the Deal' }) @IsNotEmpty() readonly updatedAt: number } ================================================ FILE: src/modules/deals/entity/position.entity.ts ================================================ import { ApiModelProperty } from '@nestjs/swagger' import { IsNotEmpty } from 'class-validator' import { Column } from 'typeorm' export class Position { @ApiModelProperty({ default: 10.780230999999999, example: 10.780230999999999, description: 'The latitude of the Position', type: Number }) @Column() @IsNotEmpty() latitude: number @ApiModelProperty({ default: 106.6645121, example: 106.6645121, description: 'The longitude of the Position', type: Number }) @Column() @IsNotEmpty() longitude: number } ================================================ FILE: src/modules/deals/enum/deal.enum.ts ================================================ export enum DealType { Request = 'Request', Offer = 'Offer' } export enum ServiceType { FoodDelivery = 'FoodDelivery', Pickup = 'Pickup', PharmacyPurchase = 'PharmacyPurchase', Queue = 'Queue', OverseasPurchase = 'OverseasPurchase', Others = 'Others' } export enum ItemType { None = 'None', Meal = 'Meal', Drinks = 'Drinks', Desserts = 'Desserts', Snacks = 'Snacks' } export enum PaymentType { Chewpay = 'Chewpay', Cod = 'Cod' } ================================================ FILE: src/modules/events/events.gateway.ts ================================================ import { SubscribeMessage, WebSocketGateway, WebSocketServer, MessageBody, WsResponse, } from '@nestjs/websockets'; import { Server } from 'socket.io'; import { Observable, from } from 'rxjs'; import { map } from 'rxjs/operators'; @WebSocketGateway() export class EventsGateway { @WebSocketServer() server: Server; @SubscribeMessage('message') handleMessage(client: any, payload: any): string { return 'Hello world!'; } @SubscribeMessage('events') findAll(@MessageBody() data: any): Observable> { return from([1, 2, 3]).pipe(map(item => ({ event: 'events', data: item }))); } @SubscribeMessage('identity') async identity(@MessageBody() data: number): Promise { return data; } } ================================================ FILE: src/modules/events/events.module.ts ================================================ import { Module } from '@nestjs/common'; @Module({}) export class EventsModule {} ================================================ FILE: src/modules/students/dto/create-student.dto.ts ================================================ import { ApiModelProperty } from '@nestjs/swagger' import { IsNotEmpty } from 'class-validator' export class CreateStudentDto { @ApiModelProperty({ default: 'xxxx-xxxx-xxxx-xxxx', example: 'xxxx-xxxx-xxxx-xxxx', description: 'The classId of the Student' }) @IsNotEmpty() readonly classId: string @ApiModelProperty({ default: 1, example: 1, description: 'The stt of the Student' }) @IsNotEmpty() readonly stt: number @ApiModelProperty({ default: 'Trần Thế Anh', example: 'Trần Thế Anh', description: 'The fullName of the Student' }) @IsNotEmpty() readonly fullName: string } ================================================ FILE: src/modules/students/dto/replace-student.dto.ts ================================================ import { ApiModelProperty } from '@nestjs/swagger' import { IsNotEmpty } from 'class-validator' export class ReplaceStudentDto { @ApiModelProperty({ default: 'xxxx-xxxx-xxxx-xxxx', example: 'xxxx-xxxx-xxxx-xxxx', description: 'The classId of the Student' }) @IsNotEmpty() readonly classId: string @ApiModelProperty({ default: 1, example: 1, description: 'The stt of the Student' }) @IsNotEmpty() readonly stt: number @ApiModelProperty({ default: 'Trần Thế Anh', example: 'Trần Thế Anh', description: 'The fullName of the Student' }) @IsNotEmpty() readonly fullName: string } ================================================ FILE: src/modules/students/entity/student.entity.ts ================================================ import { Entity, Column, ObjectIdColumn } from 'typeorm' import { ApiModelProperty } from '@nestjs/swagger' import { uuidv4 } from '../../../utils' @Entity({ name: 'students', orderBy: { createdAt: 'ASC' } }) export class StudentEntity { @ApiModelProperty({ description: 'The _id of the Student' }) @ObjectIdColumn() _id: string @ApiModelProperty({ description: 'The classId of the Student' }) @Column() classId: string @ApiModelProperty({ description: 'The stt of the Student' }) @Column() stt: number @ApiModelProperty({ description: 'The fullname of the Student' }) @Column() fullName: string @ApiModelProperty({ description: 'The createdAt of the Student' }) @Column() createdAt: number @ApiModelProperty({ description: 'The updatedAt of the Student' }) @Column() updatedAt: number constructor(partial: Partial) { if (partial) { Object.assign(this, partial) this._id = this._id || uuidv4() this.createdAt = this.createdAt || +new Date() this.updatedAt = +new Date() } } } ================================================ FILE: src/modules/students/students.controller.ts ================================================ import { Controller, Get, Body, Param, Put, Delete, Post } from '@nestjs/common' import { StudentsService } from './students.service' import { ApiResponse, ApiOperation, ApiUseTags } from '@nestjs/swagger' import { StudentEntity } from './entity/student.entity' import { CreateStudentDto } from './dto/create-student.dto' import { ReplaceStudentDto } from './dto/replace-student.dto' // @ApiBearerAuth() // @UseGuards(AuthGuard('jwt')) @ApiUseTags('students') @Controller('students') export class StudentsController { constructor(private readonly studentsService: StudentsService) {} @ApiResponse({ status: 200, description: 'The found records', type: [StudentEntity] }) @ApiOperation({ title: 'Retrieve many Students 👾' }) @Get() findAll() { return this.studentsService.findAll() } @ApiResponse({ status: 200, description: 'The found record', type: StudentEntity }) @ApiOperation({ title: 'Create one Student 👾' }) @Post() async insert(@Body() createStudentDto: CreateStudentDto) { const newStudent = await this.studentsService.insert(createStudentDto) return newStudent } @ApiResponse({ status: 200, description: 'The found record', type: StudentEntity }) @ApiOperation({ title: 'Retrieve one Student 👾' }) @Get(':id') findOne(@Param('id') id: string) { return this.studentsService.findOne(id) } @ApiOperation({ title: 'Replace one Student 👾' }) @Put(':id') replace( @Param('id') id: string, @Body() replaceStudentDto: ReplaceStudentDto ) { return this.studentsService.findOneAndReplace(id, replaceStudentDto) } @ApiResponse({ status: 200, description: 'The found record is executed 👾', type: Boolean }) @ApiOperation({ title: 'Delete one Student 👾' }) @Delete(':id') remove(@Param('id') id: string) { return this.studentsService.deleteOne(id) } } ================================================ FILE: src/modules/students/students.module.ts ================================================ import { Module } from '@nestjs/common'; import { StudentsController } from './students.controller'; import { StudentsService } from './students.service'; @Module({ controllers: [StudentsController], providers: [StudentsService] }) export class StudentsModule {} ================================================ FILE: src/modules/students/students.service.ts ================================================ import { Injectable, NotFoundException, ForbiddenException } from '@nestjs/common' import { getMongoRepository } from 'typeorm' import { StudentEntity } from './entity/student.entity' import { ReplaceStudentDto } from './dto/replace-student.dto' import { CreateStudentDto } from './dto/create-student.dto' import { ClassEntity } from '../../modules/classes/entity/class.entity' export type Student = any @Injectable() export class StudentsService { async findAll(): Promise { return getMongoRepository(StudentEntity).find() } async insert( createStudentDto: CreateStudentDto ): Promise { const { classId, stt, fullName } = createStudentDto const foundClass = await getMongoRepository(ClassEntity).findOne({ _id: classId }) if (!foundClass) { throw new NotFoundException('Class not found.') } const existedStudent = await getMongoRepository(StudentEntity).findOne({ where: { $or: [ { classId, stt }, { classId, fullName } ] } }) if (existedStudent) { throw new ForbiddenException('Stt or fullName already existed.') } const newStudent = await getMongoRepository(StudentEntity).save( new StudentEntity({ ...createStudentDto }) ) return newStudent } async findOne(_id: string): Promise { const foundStudent = await getMongoRepository(StudentEntity).findOne({ _id }) if (!foundStudent) { throw new NotFoundException('Student not found.') } return foundStudent } async findOneAndReplace( _id: string, replaceStudentDto: ReplaceStudentDto ): Promise { const { classId, stt, fullName } = replaceStudentDto const foundStudent = await getMongoRepository(StudentEntity).findOne({ _id }) if (!foundStudent) { throw new NotFoundException('Student not found.') } const foundClass = await getMongoRepository(ClassEntity).findOne({ _id: classId }) if (!foundClass) { throw new NotFoundException('Class not found.') } const existedStudent = await getMongoRepository(StudentEntity).findOne({ where: { $or: [{ stt }, { fullName }] } }) if (existedStudent) { throw new ForbiddenException('Stt or fullName already existed.') } const updateStudent = await getMongoRepository(StudentEntity).save( new StudentEntity({ ...foundStudent, ...replaceStudentDto }) ) return updateStudent } async deleteOne(_id: string): Promise { const foundStudent = await getMongoRepository(StudentEntity).findOne({ _id }) if (!foundStudent) { throw new NotFoundException('Student not found.') } return ( (await getMongoRepository(StudentEntity).delete(foundStudent)) && true ) } } ================================================ FILE: src/modules/users/dto/create-user.dto.ts ================================================ import { ApiModelProperty } from '@nestjs/swagger' import { IsNotEmpty, IsEmail, Length } from 'class-validator' export class CreateUserDto { @ApiModelProperty({ example: 'trinhchin', description: 'The name of the User' }) @Length(5, 20) @IsNotEmpty() readonly name: string @ApiModelProperty({ example: 'trinhchin.innos@gmail.com', description: 'The email of the User' }) @IsEmail() @IsNotEmpty() readonly email: string @ApiModelProperty({ example: '0', description: 'The password of the User' }) @IsNotEmpty() readonly password: string @ApiModelProperty({ example: '12341234', description: 'The referralCode of the User' }) @Length(8, 8) @IsNotEmpty() readonly referralCode: string } ================================================ FILE: src/modules/users/dto/created-by.dto.ts ================================================ import { ApiModelProperty } from '@nestjs/swagger' import { IsNotEmpty } from 'class-validator' export class CreatedByDto { @ApiModelProperty({ example: 'xxxx-xxxx-xxxx-xxxx', description: 'The _id of the CreatedBy' }) @IsNotEmpty() readonly _id: string @ApiModelProperty({ example: 'xxxxxxxxxx', description: 'The name of the CreatedBy' }) @IsNotEmpty() readonly name: string @ApiModelProperty({ example: 'https://xxx.xxx', description: 'The avatar of the CreatedBy' }) @IsNotEmpty() readonly avatar: string } ================================================ FILE: src/modules/users/dto/error-response.dto.ts ================================================ import { ApiModelProperty } from '@nestjs/swagger' import { IsNotEmpty } from 'class-validator' export class ErrorResponseDto { @ApiModelProperty({ example: '40x', description: 'The statusCode of the ErrorResponse' }) @IsNotEmpty() readonly statusCode: string @ApiModelProperty({ example: 'xxxxxxxxxx', description: 'The message of the ErrorResponse' }) @IsNotEmpty() readonly message: string @ApiModelProperty({ example: '2019-11-28T03:08:10.980Z', description: 'The timestamp of the ErrorResponse' }) @IsNotEmpty() readonly timestamp: string @ApiModelProperty({ example: '/v1', description: 'The path of the ErrorResponse' }) @IsNotEmpty() readonly path: string } ================================================ FILE: src/modules/users/dto/login-response.dto.ts ================================================ import { ApiModelProperty } from '@nestjs/swagger' import { IsNotEmpty } from 'class-validator' import { UserEntity } from '../user.entity' export class LoginResponseDto { @ApiModelProperty({ example: UserEntity, description: 'The user of the LoginResponse' }) @IsNotEmpty() readonly user: UserEntity @ApiModelProperty({ example: 'xxxxxxxxxx', description: 'The accessToken of the LoginResponse' }) @IsNotEmpty() readonly accessToken: string @ApiModelProperty({ example: 60 * 60 * 24 * 30, description: 'The expiresIn of the LoginResponse' }) @IsNotEmpty() readonly expiresIn: number } ================================================ FILE: src/modules/users/dto/login-user.dto.ts ================================================ import { ApiModelProperty } from '@nestjs/swagger' import { IsNotEmpty, IsEmail } from 'class-validator' export class LoginUserDto { @ApiModelProperty({ default: 'trinhchin.innos@gmail.com', example: 'trinhchin.innos@gmail.com', description: 'The email of the User' }) @IsEmail() @IsNotEmpty() readonly email: string @ApiModelProperty({ default: '0', example: '0', description: 'The password of the User' }) @IsNotEmpty() readonly password: string } ================================================ FILE: src/modules/users/dto/otp-response.dto.ts ================================================ import { ApiModelProperty } from '@nestjs/swagger' import { IsNotEmpty } from 'class-validator' export class OtpResponseDto { @ApiModelProperty({ default: 678900, example: 678900, description: 'The otp of the OtpResponseDto' }) @IsNotEmpty() readonly otp: number @ApiModelProperty({ default: '60s', example: '60s', description: 'The remaining of the OtpResponseDto' }) @IsNotEmpty() readonly remaining: string } ================================================ FILE: src/modules/users/dto/replace-user.dto.ts ================================================ import { ApiModelProperty } from '@nestjs/swagger'; import { IsNotEmpty, Length } from 'class-validator'; export class ReplaceUserDto { @ApiModelProperty({ description: 'The name of the User' }) @Length(5, 20) @IsNotEmpty() readonly name: string; @ApiModelProperty({ description: 'The referralCode of the User' }) @Length(8, 8) @IsNotEmpty() readonly referralCode: string; } ================================================ FILE: src/modules/users/dto/update-user.dto.ts ================================================ import { ApiModelProperty } from '@nestjs/swagger'; import { Length, IsOptional } from 'class-validator'; export class UpdateUserDto { @ApiModelProperty({ required: false, description: 'The name of the User', }) @Length(5, 20) @IsOptional() readonly name: string; @ApiModelProperty({ required: false, description: 'The referralCode of the User', }) @Length(8, 8) @IsOptional() readonly referralCode: string; } ================================================ FILE: src/modules/users/dto/upload-response.dto.ts ================================================ import { ApiModelProperty } from '@nestjs/swagger' import { IsNotEmpty } from 'class-validator' export class UploadResponseDto { @ApiModelProperty({ example: 'https://xxx.xxx', description: 'The path of the UploadResponse' }) @IsNotEmpty() readonly url: string } ================================================ FILE: src/modules/users/index.ts ================================================ import * as speakeasy from 'speakeasy' const token = speakeasy.totp({ secret: '123456', encoding: 'base32' }) const tokenValidates = speakeasy.totp.verify({ secret: '123456', encoding: 'base32', token, window: 0 }) console.log(tokenValidates) ================================================ FILE: src/modules/users/user.entity.ts ================================================ import { Entity, ObjectIdColumn, Column } from 'typeorm' import { uuidv4 } from '../../utils' import { Exclude, plainToClass } from 'class-transformer' import { ApiModelProperty } from '@nestjs/swagger' import { Position } from '../../modules/deals/entity/position.entity' @Entity({ name: 'users', orderBy: { createdAt: 'ASC' } }) export class UserEntity { @ApiModelProperty({ description: 'The _id of the User' }) @ObjectIdColumn() _id: string // basic @ApiModelProperty({ description: 'The name of the User' }) @Column() name: string @ApiModelProperty({ description: 'The email of the User' }) @Column() email: string @ApiModelProperty({ description: 'The password of the User' }) @Exclude() @Column() password: string @ApiModelProperty({ description: 'The referralCode of the User' }) @Column() referralCode: string @ApiModelProperty({ description: 'The search location of the User' }) @Column() searchIn: Position // @Column() // countryCode: string; // Vietname +84 // @Column() // phone: string; // 0704498756 // @Column() // verified: boolean; // false // @Column() // authyId: string; // null @ApiModelProperty({ description: 'The avatar of the User' }) @Column() avatar: string @ApiModelProperty({ description: 'The phone of the User' }) @Column() phone: string @ApiModelProperty({ description: 'The verified of the User' }) @Column() verified: boolean @ApiModelProperty({ description: 'The createdAt of the User' }) @Column() createdAt: number @ApiModelProperty({ description: 'The updatedAt of the User' }) @Column() updatedAt: number constructor(partial: Partial) { if (partial) { Object.assign(this, partial) this._id = this._id || uuidv4() this.avatar = this.avatar || 'https://res.cloudinary.com/chnirt/image/upload/v1573662028/rest/2019-11-13T16:20:22.699Z.png' this.verified = this.verified || false this.createdAt = this.createdAt || +new Date() this.updatedAt = +new Date() } } } ================================================ FILE: src/modules/users/users.controller.ts ================================================ import { Controller, UseGuards, Post, Get, Param, ClassSerializerInterceptor, UseInterceptors, Put, Patch, Body, Delete, UploadedFile, Request } from '@nestjs/common' import { AuthGuard } from '@nestjs/passport' import { ApiBearerAuth, ApiUseTags, ApiOperation, ApiResponse, ApiConsumes, ApiImplicitFile } from '@nestjs/swagger' import { FileInterceptor } from '@nestjs/platform-express' import * as jwt from 'jsonwebtoken' import { UserEntity } from './user.entity' import { CreateUserDto } from './dto/create-user.dto' import { UsersService } from './users.service' import { UpdateUserDto } from './dto/update-user.dto' import { ReplaceUserDto } from './dto/replace-user.dto' import { ErrorResponseDto } from './dto/error-response.dto' import { LoginResponseDto } from './dto/login-response.dto' import { OtpResponseDto } from './dto/otp-response.dto' import { AuthService } from '../../auth/auth.service' @ApiResponse({ status: 401, description: 'Unauthorized.', type: ErrorResponseDto }) @ApiResponse({ status: 403, description: 'Forbidden.', type: ErrorResponseDto }) @UseInterceptors(ClassSerializerInterceptor) @ApiUseTags('users') @Controller('users') export class UsersController { constructor( private readonly authService: AuthService, private readonly userService: UsersService ) {} @ApiResponse({ status: 200, description: 'The found records', type: [UserEntity] }) @ApiBearerAuth() @UseGuards(AuthGuard('jwt')) @ApiOperation({ title: 'Retrieve many Users 👻' }) @Get() findAll() { return this.userService.findAll() } @ApiResponse({ status: 200, description: 'The found record', type: LoginResponseDto }) @ApiOperation({ title: 'Create one User 👻' }) @Post() async insert(@Body() createUserDto: CreateUserDto) { const newUser = await this.userService.insert(createUserDto) const loginResponseDto = await this.authService.login(newUser) return loginResponseDto } @ApiResponse({ status: 200, description: 'The found record', type: UserEntity }) @ApiBearerAuth() @UseGuards(AuthGuard('jwt')) @ApiOperation({ title: 'Retrieve one User 👻' }) @Get(':id') findOne(@Param('id') id: string) { return this.userService.findOne(id) } @ApiBearerAuth() @UseGuards(AuthGuard('jwt')) @ApiOperation({ title: 'Update one User 👻' }) @Patch(':id') update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) { return this.userService.findOneAndUpdate(id, updateUserDto) } @ApiBearerAuth() @UseGuards(AuthGuard('jwt')) @ApiOperation({ title: 'Replace one User 👻' }) @Put(':id') replace(@Param('id') id: string, @Body() replaceUserDto: ReplaceUserDto) { return this.userService.findOneAndReplace(id, replaceUserDto) } @ApiResponse({ status: 200, description: 'The found record is executed 👻', type: Boolean }) @ApiBearerAuth() @UseGuards(AuthGuard('jwt')) @ApiOperation({ title: 'Delete one User 👻' }) @Delete(':id') remove(@Param('id') id: string) { return this.userService.deleteOne(id) } @ApiResponse({ status: 200, description: 'The found record is executed', type: Boolean }) @ApiBearerAuth() @UseGuards(AuthGuard('jwt')) @ApiOperation({ title: 'Update one Avatar for current User 👻' }) @Post('avatar') @ApiConsumes('multipart/form-data') @ApiImplicitFile({ name: 'avatar', required: true, description: 'Send one file' }) @UseInterceptors(FileInterceptor('avatar')) updateAvatar(@Request() req, @UploadedFile() file) { const { user } = req const { _id } = user return this.userService.updateAvatar(_id, file) } @ApiBearerAuth() @UseGuards(AuthGuard('jwt')) @ApiResponse({ status: 201, description: 'The found record is executed', type: OtpResponseDto }) @ApiOperation({ title: 'Otp one User 👻' }) @Post('/otp/:phone') otp1(@Request() req, @Param('phone') phone: string) { const { user } = req const { _id } = user return this.userService.otp(_id, phone) } @ApiBearerAuth() @UseGuards(AuthGuard('jwt')) @ApiResponse({ status: 200, description: 'The found record is executed', type: LoginResponseDto }) @ApiOperation({ title: 'Verify one User 👻' }) @Post('/verify/:otp') async verify( @Request() req, @Param('otp') otp: string ): Promise { const { user } = req const { _id } = user const updateUser = await this.userService.verify(_id, otp) const loginResponseDto = await this.authService.login(updateUser) return loginResponseDto } } ================================================ FILE: src/modules/users/users.module.ts ================================================ import { Module } from '@nestjs/common' import { UsersService } from './users.service' import { UsersController } from './users.controller' import { AuthModule } from '../../auth/auth.module' @Module({ imports: [AuthModule], controllers: [UsersController], providers: [UsersService], exports: [UsersService] }) export class UsersModule {} ================================================ FILE: src/modules/users/users.service.ts ================================================ import { Injectable, ForbiddenException, NotFoundException } from '@nestjs/common' import { CreateUserDto } from './dto/create-user.dto' import { getMongoRepository } from 'typeorm' import * as speakeasy from 'speakeasy' import { Validator } from 'class-validator' import { UserEntity } from './user.entity' import { hashPassword } from '../../utils' import { UpdateUserDto } from './dto/update-user.dto' import { ReplaceUserDto } from './dto/replace-user.dto' import { uploadFile } from '../../shared' import { SPEAKEASY_SECRET, SPEAKEASY_STEP } from '../../environments' import { OtpResponseDto } from './dto/otp-response.dto' const validator = new Validator() export type User = any @Injectable() export class UsersService { async insert(createUserDto: CreateUserDto): Promise { const { email } = createUserDto const existedUser = await getMongoRepository(UserEntity).findOne({ email }) if (existedUser) { throw new ForbiddenException('Email already existed.') } const newUser = await getMongoRepository(UserEntity).save( new UserEntity({ ...createUserDto, password: await hashPassword(createUserDto.password) }) ) return newUser } async findAll(): Promise { return getMongoRepository(UserEntity).find() } async findOne(_id: string): Promise { const foundUser = await getMongoRepository(UserEntity).findOne({ _id }) if (!foundUser) { throw new NotFoundException('User not found.') } return foundUser } async findOneAndReplace( _id: string, replaceUserDto: ReplaceUserDto ): Promise { const foundUser = await getMongoRepository(UserEntity).findOne({ _id }) if (!foundUser) { throw new NotFoundException('User not found.') } const updateUser = await getMongoRepository(UserEntity).save( new UserEntity({ ...foundUser, ...replaceUserDto }) ) return updateUser } async findOneAndUpdate( _id: string, updateUserDto: UpdateUserDto ): Promise { const foundUser = await getMongoRepository(UserEntity).findOne({ _id }) if (!foundUser) { throw new NotFoundException('User not found.') } const updateUser = await getMongoRepository(UserEntity).save( new UserEntity({ ...foundUser, ...updateUserDto }) ) return updateUser } async deleteOne(_id: string): Promise { const foundUser = await getMongoRepository(UserEntity).findOne({ _id }) if (!foundUser) { throw new NotFoundException('User not found.') } return (await getMongoRepository(UserEntity).delete(foundUser)) ? true : false } async findOneWithEmail(email: string): Promise { const foundUser = await getMongoRepository(UserEntity).findOne({ email }) if (!foundUser) { throw new NotFoundException('User not found.') } return foundUser } async updateAvatar(_id: string, file: any): Promise { // console.log(_id, file) const foundUser = await getMongoRepository(UserEntity).findOne({ _id }) if (!foundUser) { throw new NotFoundException('User not found.') } foundUser.avatar = await uploadFile(file) const updateUser = await getMongoRepository(UserEntity).save(foundUser) return updateUser ? true : false } async otp(_id: string, phone: string): Promise { validator.isMobilePhone(phone, 'en-SG') const foundUser = await getMongoRepository(UserEntity).findOne({ where: { _id, verified: false } }) if (!foundUser) { throw new NotFoundException('User not found.') } const token = await speakeasy.totp({ secret: SPEAKEASY_SECRET!, encoding: 'base32', // digits: SPEAKEASY_DIGITS! step: SPEAKEASY_STEP! // 30s // window: 1 // pre 30s cur 30s nxt 30s }) const remaining = SPEAKEASY_STEP - Math.floor((+new Date() / 1000.0) % SPEAKEASY_STEP) + 's' foundUser.phone = phone await getMongoRepository(UserEntity).save(foundUser) return { otp: +token, remaining } } async verify(_id: string, otp: string) { const foundUser = await getMongoRepository(UserEntity).findOne({ where: { _id, verified: false } }) if (!foundUser) { throw new NotFoundException('User not found.') } // console.log(otp) const verified = await speakeasy.totp.verify({ secret: SPEAKEASY_SECRET!, encoding: 'base32', token: otp, step: SPEAKEASY_STEP!, // 30s window: 1 }) // console.log(verified) if (verified) { foundUser.verified = true return await getMongoRepository(UserEntity).save(foundUser) } throw new ForbiddenException('Otp is incorrect.') } } ================================================ FILE: src/shared/index.ts ================================================ export * from './upload' ================================================ FILE: src/shared/upload/index.ts ================================================ import * as cloudinary from 'cloudinary' import { CLOUD_NAME, API_KEY, API_SECRET } from '../../environments' /** * Returns image url by upload file. * * @remarks * This method is part of the {@link shared/upload}. * * @param createReadStream - 1st input number * @returns The string mean of `createReadStream` * * @beta */ export const uploadFile = async (file: any): Promise => { cloudinary.config({ cloud_name: CLOUD_NAME!, api_key: API_KEY!, api_secret: API_SECRET! }) const uniqueFilename = new Date().toISOString() const result = await new Promise(async (resolve, reject) => { cloudinary.v2.uploader .upload_stream( { folder: 'rest', public_id: uniqueFilename, tags: 'rest' }, // directory and tags are optional (err, image) => { if (err) { reject(err) } resolve(image) } ) .end(file.buffer) }) // tslint:disable-next-line:no-string-literal return result['secure_url'] } ================================================ FILE: src/terminus-options.service.ts ================================================ import { TerminusEndpoint, TerminusOptionsFactory, DNSHealthIndicator, TerminusModuleOptions, } from '@nestjs/terminus'; import { Injectable } from '@nestjs/common'; @Injectable() export class TerminusOptionsService implements TerminusOptionsFactory { constructor(private readonly dns: DNSHealthIndicator) {} createTerminusOptions(): TerminusModuleOptions { const healthEndpoint: TerminusEndpoint = { url: '/health', healthIndicators: [ async () => this.dns.pingCheck('google', 'https://google.com'), ], }; return { endpoints: [healthEndpoint], }; } } ================================================ FILE: src/utils/index.ts ================================================ export * from './password'; export * from './uuid'; ================================================ FILE: src/utils/password/index.ts ================================================ import { hash, compare } from 'bcrypt'; import { SALT } from '../../environments'; /** * Returns hashed password by hash password. * * @remarks * This method is part of the {@link utils/password}. * * @param password - 1st input number * @returns The hashed password mean of `password` * * @beta */ export const hashPassword = async (password: string): Promise => { return await hash(password, SALT); }; /** * Returns boolean by compare password. * * @remarks * This method is part of the {@link utils/password}. * * @param password - 1st input number * @param hash - The second input number * @returns The boolean mean of `password` and `hash` * * @beta */ export const comparePassword = async ( password: string, hash: string, ): Promise => { return await compare(password, hash); }; ================================================ FILE: src/utils/uuid/index.ts ================================================ /** * Returns string by uuidv4. * * @remarks * This method is part of the {@link utils/uuid}. * * @returns The string * * @beta */ export const uuidv4 = () => { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { const r = (Math.random() * 16) | 0; const v = c === 'x' ? r : (r & 0x3) | 0x8; return v.toString(16); }); }; /** * Returns string by generateUID. * * @remarks * This method is part of the {@link utils/uuid}. * * @returns The string * * @beta */ export const generateUID = () => { // I generate the UID from two parts here // to ensure the random number provide enough bits. const firstPart = (Math.random() * 46656) | 0; const secondPart = (Math.random() * 46656) | 0; const newFirstPart = ('000' + firstPart.toString(36)).slice(-3); const newSecondPart = ('000' + secondPart.toString(36)).slice(-3); return newFirstPart + newSecondPart; }; // console.log(uuidv4()) // console.log(generateUID()) ================================================ FILE: ssl/ca_bundle.crt ================================================ -----BEGIN CERTIFICATE----- MIIEkjCCA3qgAwIBAgIQCgFBQgAAAVOFc2oLheynCDANBgkqhkiG9w0BAQsFADA/ MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT DkRTVCBSb290IENBIFgzMB4XDTE2MDMxNzE2NDA0NloXDTIxMDMxNzE2NDA0Nlow SjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxldCdzIEVuY3J5cHQxIzAhBgNVBAMT GkxldCdzIEVuY3J5cHQgQXV0aG9yaXR5IFgzMIIBIjANBgkqhkiG9w0BAQEFAAOC AQ8AMIIBCgKCAQEAnNMM8FrlLke3cl03g7NoYzDq1zUmGSXhvb418XCSL7e4S0EF q6meNQhY7LEqxGiHC6PjdeTm86dicbp5gWAf15Gan/PQeGdxyGkOlZHP/uaZ6WA8 SMx+yk13EiSdRxta67nsHjcAHJyse6cF6s5K671B5TaYucv9bTyWaN8jKkKQDIZ0 Z8h/pZq4UmEUEz9l6YKHy9v6Dlb2honzhT+Xhq+w3Brvaw2VFn3EK6BlspkENnWA a6xK8xuQSXgvopZPKiAlKQTGdMDQMc2PMTiVFrqoM7hD8bEfwzB/onkxEz0tNvjj /PIzark5McWvxI0NHWQWM6r6hCm21AvA2H3DkwIDAQABo4IBfTCCAXkwEgYDVR0T AQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwfwYIKwYBBQUHAQEEczBxMDIG CCsGAQUFBzABhiZodHRwOi8vaXNyZy50cnVzdGlkLm9jc3AuaWRlbnRydXN0LmNv bTA7BggrBgEFBQcwAoYvaHR0cDovL2FwcHMuaWRlbnRydXN0LmNvbS9yb290cy9k c3Ryb290Y2F4My5wN2MwHwYDVR0jBBgwFoAUxKexpHsscfrb4UuQdf/EFWCFiRAw VAYDVR0gBE0wSzAIBgZngQwBAgEwPwYLKwYBBAGC3xMBAQEwMDAuBggrBgEFBQcC ARYiaHR0cDovL2Nwcy5yb290LXgxLmxldHNlbmNyeXB0Lm9yZzA8BgNVHR8ENTAz MDGgL6AthitodHRwOi8vY3JsLmlkZW50cnVzdC5jb20vRFNUUk9PVENBWDNDUkwu Y3JsMB0GA1UdDgQWBBSoSmpjBH3duubRObemRWXv86jsoTANBgkqhkiG9w0BAQsF AAOCAQEA3TPXEfNjWDjdGBX7CVW+dla5cEilaUcne8IkCJLxWh9KEik3JHRRHGJo uM2VcGfl96S8TihRzZvoroed6ti6WqEBmtzw3Wodatg+VyOeph4EYpr/1wXKtx8/ wApIvJSwtmVi4MFU5aMqrSDE6ea73Mj2tcMyo5jMd6jmeWUHK8so/joWUoHOUgwu X4Po1QYz+3dszkDqMp4fklxBwXRsW10KXzPMTZ+sOPAveyxindmjkW8lGy+QsRlG PfZ+G6Z6h7mjem0Y+iWlkYcV4PIWL1iwBi8saCbGS5jN2p8M+X+Q7UNKEkROb3N6 KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg== -----END CERTIFICATE----- ================================================ FILE: ssl/certificate.crt ================================================ -----BEGIN CERTIFICATE----- MIIFjDCCBHSgAwIBAgISAxzigJJ7e4L0h878QlVRGOH6MA0GCSqGSIb3DQEBCwUA MEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQD ExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMzAeFw0xOTExMTAwMzM2MDFaFw0y MDAyMDgwMzM2MDFaMDUxMzAxBgNVBAMTKm5lc3Rqcy1yZXN0ZnVsLWJlc3QtcHJh Y3RpY2UuaGVyb2t1YXBwLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC ggEBAKOMUnWgi/uPB3Kr8Ay51rzVES/Tr8EaebjRKSSOGw8h4ZNaPAeDg7ZH1PvV m5JexIJ4EAfd2jSV8GZZl7xBmG1GWgyjnWnQTwJ643F2EK2UNC5KLaCvMyeDkNTl JW2Ur/0nCR8oUEBAx9QsbIJ0HhWofDRTOttOcrfsjnqtnWJl20PNeDS7NGjewUhe +Yk1Z705mGZezxeU3dNmQnpGEFxo6p5Y/M8maD04LxZUUKxReeLPDHGYuNkY+klq Y8FReaXJTTFH92XIOS4ECwvLk6XgCwiX+xCH3gerdBIiuP6LTA2RNf0lhs24duhI eGHwJeYSvmVfUXS3V11OyDcv7qUCAwEAAaOCAn8wggJ7MA4GA1UdDwEB/wQEAwIF oDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAd BgNVHQ4EFgQUSk/oIsx9g6CBliPWqcsK52nF6CUwHwYDVR0jBBgwFoAUqEpqYwR9 3brm0Tm3pkVl7/Oo7KEwbwYIKwYBBQUHAQEEYzBhMC4GCCsGAQUFBzABhiJodHRw Oi8vb2NzcC5pbnQteDMubGV0c2VuY3J5cHQub3JnMC8GCCsGAQUFBzAChiNodHRw Oi8vY2VydC5pbnQteDMubGV0c2VuY3J5cHQub3JnLzA1BgNVHREELjAsgipuZXN0 anMtcmVzdGZ1bC1iZXN0LXByYWN0aWNlLmhlcm9rdWFwcC5jb20wTAYDVR0gBEUw QzAIBgZngQwBAgEwNwYLKwYBBAGC3xMBAQEwKDAmBggrBgEFBQcCARYaaHR0cDov L2Nwcy5sZXRzZW5jcnlwdC5vcmcwggEEBgorBgEEAdZ5AgQCBIH1BIHyAPAAdgCy HgXMi6LNiiBOh2b5K7mKJSBna9r6cOeySVMt74uQXgAAAW5TmOwuAAAEAwBHMEUC ICFD2zRfc3lLDyFv8ziOkIhGAqm3m0OfsLUfEfBsOFD3AiEA1RW78rTM9GSCvcfz ng7JtgocQGoCwRtHUOwueNxRlGUAdgBvU3asMfAxGdiZAKRRFf93FRwR2QLBACkG jbIImjfZEwAAAW5TmOxgAAAEAwBHMEUCIQDG6XXZbQvg+OKdAJ6F5bEtRvdH2ZOO MFrErGSfbA5QdgIgFzhbbaFKGwk4G+kO2+mJYo7clgLR+WIuW/C8fQSIOI8wDQYJ KoZIhvcNAQELBQADggEBACksWIQEdpa9cuDVw/gsOqnbNirDZCJI3JqM58zWRxSl kOlBmsmQRzHeveR6CSu/GOiR4HeFF+To0zjBX8U14ufwNOj5eS0jVkwEgN5weWbh inbWc1PsFuwzISYrG0ssDXP2ty+8Dpg3WjfZg9RE36PsQuH3rQl1A6i0uz9vVASu bccgjJWVvsJF72DPNzOAD9T8wdcXvKrVb/GU19D1s5NHDUcogBqRSPrjUuLiW8BN p6F8UMV1iXbPDSVdI71sDhpaBt7EP6jwBgfDnGuNLrUDBg7WsmtQrBdbg5oRhTw0 5FiaP0roSCHV9gpH3R+VgLmLbRa8o6ssWYlS8vEoA7A= -----END CERTIFICATE----- ================================================ FILE: ssl/private.key ================================================ -----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCjjFJ1oIv7jwdy q/AMuda81REv06/BGnm40SkkjhsPIeGTWjwHg4O2R9T71ZuSXsSCeBAH3do0lfBm WZe8QZhtRloMo51p0E8CeuNxdhCtlDQuSi2grzMng5DU5SVtlK/9JwkfKFBAQMfU LGyCdB4VqHw0UzrbTnK37I56rZ1iZdtDzXg0uzRo3sFIXvmJNWe9OZhmXs8XlN3T ZkJ6RhBcaOqeWPzPJmg9OC8WVFCsUXnizwxxmLjZGPpJamPBUXmlyU0xR/dlyDku BAsLy5Ol4AsIl/sQh94Hq3QSIrj+i0wNkTX9JYbNuHboSHhh8CXmEr5lX1F0t1dd Tsg3L+6lAgMBAAECggEAB+R4Vj+D1y/1JrjawhfLH8cTJ+u0n5es0sPFPdkIapfd UERv1oJSPjRZODimuU7kIiezyQK7yliHXBPBbs77kXUZA2Mh8D6ikXa8uHeqIQug jltQuGBmvObB0S79uhFi13n6xrYl/p53BkDekQJphpdgPGYygO2C5mf+uDeIpibl u4TwRZmELPjY7ZPJlq0Sv+G93FDbF5yLa4/sQwrjfPFBq0XDEmN4r2kcCGZwpq0L BIw4Jl+XnNU/HHPvBShe5Nx+URgQuGkWCHmpChYRAfXjLZk65ZomcfFVgTgxg786 gmZZdpHr129F9BAwDcp5QMfl1zs4g4qlN8c9ttxgQQKBgQDcBhGdvt4aNNFbxVWw pr7hwmryYYzR64Wnp3IjaIMpCabOzsIVoS/8kq60l7c9aag5KgJoqLNZfUeue38H oDXnAkflXveI41VbpwleAAb3+R1d6jlIJjJaJtAX1byivLkqXl4qjoau+040myKj 7POhUgNjcGEtyClvNgkAY4nfPwKBgQC+SkC6F9LQSeDinhFWty9kjaeL15hnjJMR QJ7fzPkziFZLlEa9ggv5CBJD+k/uXguLRxt6IaHCE4uwQPALGL0iMC6xaAfCOLJz ojOJxlYOuLjDSPeylAQ+QjlM62zF23JvQlc1/QZBsgBFfCNWkDHpHhyiQi6Cv7mp yr42fxTdGwKBgCi+VvTHK4nezgYYfM3BkwdrYTKRLeqRmqZ5M4GrEN7AksspLnei 6afz4bY/ggc1UZmEVf3bf5rKwENnSxa2bETi/z1SYLRQpLXcMLffeWriDrYdcY4S xLA9D7vaMJxSJlfaMcXfrsEoeEr1j2ybrGHrNgVsAhLgRgv6DaCszhMxAoGBALQW Q7GacEntUSZHH/OoQ/Lu2LzQ2gxNjrWKKZF2Q/WQNtMqTdR1qe0RxW+OCm11lYlH T2rDP3oT02SH4GUwEXa0kMwWvxkBXWlv/USLbtBZ44n1mW3pBScCt4XjXDrYFzHS YATZJD2yPu2DsVHv/zw24jRxW+Ejn4tgM6oRlOY3AoGAFKbGbnHHP39shBPWKi5A yQwy1pmgrShiptzHCghrRyT0zuXsOI/570DsdG8bbo2QolF5vtGVjJO1x8inDjGH hJMk0CS3IZYLctM1/H1gZMT50sAyyTEsTxcHmEXROfV0X9cLzmGxC592nCr6NRwz U0RWwhjG3bKfzRWTy2trtQs= -----END PRIVATE KEY----- ================================================ FILE: static/index.html ================================================ Nestjs SocketIO

{{ title }}



  • {{ message.name }}: {{ message.text }}


================================================ FILE: static/main.js ================================================ const app = new Vue({ el: '#app', data: { title: 'Nestjs Websockets Chat', name: '', text: '', messages: [], socket: null }, methods: { sendMessage() { if (this.validateInput()) { const message = { name: this.name, text: this.text } this.socket.emit('msgToServer', message) this.text = '' } }, receivedMessage(message) { this.messages.push(message) }, validateInput() { return this.name.length > 0 && this.text.length > 0 } }, created() { this.socket = io('https://nestjs-restful-best-practice.herokuapp.com') this.socket.on('msgToClient', message => { this.receivedMessage(message) }) } }) ================================================ FILE: static/style.css ================================================ #messages { height: 300px; overflow-y: scroll; } #app { margin-top: 2rem; } ================================================ FILE: test/app.e2e-spec.ts ================================================ import { Test, TestingModule } from '@nestjs/testing'; import * as request from 'supertest'; import { AppModule } from './../src/app.module'; describe('AppController (e2e)', () => { let app; beforeEach(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); app = moduleFixture.createNestApplication(); await app.init(); }); it('/ (GET)', () => { return request(app.getHttpServer()) .get('/') .expect(200) .expect('Hello World!'); }); }); ================================================ FILE: test/jest-e2e.json ================================================ { "moduleFileExtensions": ["js", "json", "ts"], "rootDir": ".", "testEnvironment": "node", "testRegex": ".e2e-spec.ts$", "transform": { "^.+\\.(t|j)s$": "ts-jest" } } ================================================ FILE: tsconfig.build.json ================================================ { "extends": "./tsconfig.json", "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] } ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "module": "commonjs", "declaration": true, "removeComments": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, "target": "es2017", "sourceMap": true, "outDir": "./dist", "baseUrl": "./src", "incremental": true, "paths": { "@auth": ["auth"], "@common": ["common"], "@config": ["config"], "@environments": ["environments"], "@models": ["models"], "@shared": ["shared"], "@utils": ["utils"], "@validations": ["validations"] } }, "exclude": ["node_modules", "dist"] } ================================================ FILE: tslint.json ================================================ { "defaultSeverity": "error", "extends": ["tslint:recommended"], "jsRules": { "no-unused-expression": true }, "rules": { "quotemark": [true, "single"], "member-access": [false], "ordered-imports": [false], "max-line-length": [true, 150], "member-ordering": [false], "interface-name": [false], "arrow-parens": false, "object-literal-sort-keys": false, "variable-name": [true, "allow-leading-underscore"], "indent": false, "trailing-comma": false, "semicolon": false, "no-bitwise": false }, "rulesDirectory": [] } ================================================ FILE: webpack.config.js ================================================ const webpack = require('webpack') const path = require('path') const nodeExternals = require('webpack-node-externals') const chalk = require('chalk') const ProgressBarPlugin = require('progress-bar-webpack-plugin') const BundleAnalyzerPlugin = require('webpack-bundle-analyzer') .BundleAnalyzerPlugin module.exports = { entry: ['webpack/hot/poll?100', './src/main.ts'], watch: true, target: 'node', externals: [ nodeExternals({ whitelist: ['webpack/hot/poll?100'] }) ], module: { rules: [ { test: /.tsx?$/, use: 'ts-loader', exclude: /node_modules/ } ] }, mode: 'development', resolve: { extensions: ['.tsx', '.ts', '.js'] }, plugins: [ new webpack.HotModuleReplacementPlugin(), new webpack.WatchIgnorePlugin([/\.js$/, /\.d\.ts$/]), new ProgressBarPlugin({ format: chalk.hex('#6c5ce7')('build ') + chalk.hex('#0984e3')('▯:bar▯ ') + // chalk.red('▯ :bar ▯ ') + chalk.hex('#00b894')('(:percent) ') + // chalk.green(':percent ') + chalk.hex('#ffeaa7')(':msg'), // chalk.blue('( :elapsed s )') complete: '▰', incomplete: '▱', clear: false }), new BundleAnalyzerPlugin({ analyzerMode: 'static', analyzerHost: '127.0.0.1', analyzerPort: '8888', reportFilename: process.env.NODE_ENV === 'development' && 'report.html', openAnalyzer: false, generateStatsFile: false, statsFilename: 'stats.json' }), new webpack.BannerPlugin({ banner: 'require("source-map-support").install();', raw: true, entryOnly: false }) ], optimization: { removeAvailableModules: false, removeEmptyChunks: false, splitChunks: false }, output: { pathinfo: false // path: path.join(__dirname, 'dist'), // filename: 'server.js' } }