├── .eslintrc.js
├── .gitignore
├── .prettierrc
├── README.md
├── config
└── default.json
├── nest-cli.json
├── package-lock.json
├── package.json
├── src
├── app.controller.spec.ts
├── app.controller.ts
├── app.module.ts
├── app.service.ts
├── httpExceptionFilter.ts
├── main.ts
├── orders
│ ├── dto
│ │ ├── checkout.dto.ts
│ │ ├── create-order.dto.ts
│ │ └── update-order.dto.ts
│ ├── orders.controller.spec.ts
│ ├── orders.controller.ts
│ ├── orders.module.ts
│ ├── orders.service.spec.ts
│ └── orders.service.ts
├── products
│ ├── dto
│ │ ├── create-product.dto.ts
│ │ ├── get-product-query-dto.ts
│ │ ├── product-sku.dto.ts
│ │ └── update-product.dto.ts
│ ├── products.controller.spec.ts
│ ├── products.controller.ts
│ ├── products.module.ts
│ ├── products.service.spec.ts
│ └── products.service.ts
├── responseInterceptor.ts
├── shared
│ ├── middleware
│ │ ├── auth.ts
│ │ ├── role.decorators.ts
│ │ └── roles.guard.ts
│ ├── repositories
│ │ ├── order.repository.ts
│ │ ├── product.repository.ts
│ │ └── user.repository.ts
│ ├── schema
│ │ ├── license.ts
│ │ ├── orders.ts
│ │ ├── products.ts
│ │ └── users.ts
│ └── utility
│ │ ├── mail-handler.ts
│ │ ├── password-manager.ts
│ │ └── token-generator.ts
└── users
│ ├── dto
│ ├── create-user.dto.ts
│ └── update-user.dto.ts
│ ├── users.controller.spec.ts
│ ├── users.controller.ts
│ ├── users.module.ts
│ ├── users.service.spec.ts
│ └── users.service.ts
├── test
├── app.e2e-spec.ts
└── jest-e2e.json
├── tsconfig.build.json
└── tsconfig.json
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: '@typescript-eslint/parser',
3 | parserOptions: {
4 | project: 'tsconfig.json',
5 | tsconfigRootDir : __dirname,
6 | sourceType: 'module',
7 | },
8 | plugins: ['@typescript-eslint/eslint-plugin'],
9 | extends: [
10 | 'plugin:@typescript-eslint/recommended',
11 | 'plugin:prettier/recommended',
12 | ],
13 | root: true,
14 | env: {
15 | node: true,
16 | jest: true,
17 | },
18 | ignorePatterns: ['.eslintrc.js'],
19 | rules: {
20 | '@typescript-eslint/interface-name-prefix': 'off',
21 | '@typescript-eslint/explicit-function-return-type': 'off',
22 | '@typescript-eslint/explicit-module-boundary-types': 'off',
23 | '@typescript-eslint/no-explicit-any': 'off',
24 | },
25 | };
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # compiled output
2 | /dist
3 | /node_modules
4 |
5 | # Logs
6 | logs
7 | *.log
8 | npm-debug.log*
9 | pnpm-debug.log*
10 | yarn-debug.log*
11 | yarn-error.log*
12 | lerna-debug.log*
13 |
14 | # OS
15 | .DS_Store
16 |
17 | # Tests
18 | /coverage
19 | /.nyc_output
20 |
21 | # IDEs and editors
22 | /.idea
23 | .project
24 | .classpath
25 | .c9/
26 | *.launch
27 | .settings/
28 | *.sublime-workspace
29 |
30 | # IDE - VSCode
31 | .vscode/*
32 | !.vscode/settings.json
33 | !.vscode/tasks.json
34 | !.vscode/launch.json
35 | !.vscode/extensions.json
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all"
4 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
6 | [circleci-url]: https://circleci.com/gh/nestjs/nest
7 |
8 | A progressive Node.js framework for building efficient and scalable server-side applications.
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
24 |
25 | ## Description
26 |
27 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
28 |
29 | ## Installation
30 |
31 | ```bash
32 | $ npm install
33 | ```
34 |
35 | ## Running the app
36 |
37 | ```bash
38 | # development
39 | $ npm run start
40 |
41 | # watch mode
42 | $ npm run start:dev
43 |
44 | # production mode
45 | $ npm run start:prod
46 | ```
47 |
48 | ## Test
49 |
50 | ```bash
51 | # unit tests
52 | $ npm run test
53 |
54 | # e2e tests
55 | $ npm run test:e2e
56 |
57 | # test coverage
58 | $ npm run test:cov
59 | ```
60 |
61 | ## Support
62 |
63 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
64 |
65 | ## Stay in touch
66 |
67 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
68 | - Website - [https://nestjs.com](https://nestjs.com/)
69 | - Twitter - [@nestframework](https://twitter.com/nestframework)
70 |
71 | ## License
72 |
73 | Nest is [MIT licensed](LICENSE).
74 |
--------------------------------------------------------------------------------
/config/default.json:
--------------------------------------------------------------------------------
1 | {
2 | "port": "3100",
3 | "mongoDbUrl": "mongodb+srv://arindam:arindam@test.g7xwc.mongodb.net/digizone_yt?retryWrites=true&w=majority",
4 | "adminSecretToken": "admin@123",
5 | "jwtSecret": "secretTest@123456",
6 | "loginLink": "http://localhost:3000/auth",
7 | "appPrefix": "/api/v1",
8 | "fileStoragePath": "../uploads/",
9 | "emailService": {
10 | "testDomain": "sandboxe4de527199c94ccfb6d6c08a8081a74c.mailgun.org",
11 | "privateApiKey": "7d1e83a6248b73895f2e0262ad1c3a60-835621cf-e8afd81f",
12 | "publicApiKey": "pubkey-88ba02ebc871125a5438ad1865535538",
13 | "emailTemplates": {
14 | "forgotPassword": "forgot-password-template",
15 | "verifyEmail": "verify-email-template",
16 | "orderSuccess": "order-success"
17 | }
18 | },
19 | "cloudinary": {
20 | "cloud_name": "df2uufrnc",
21 | "api_key": "315812872785858",
22 | "api_secret": "5QEv4lqlmwFKvjVoGCfQzWCnbKg",
23 | "folderPath": "digizone/products/",
24 | "publicId_prefix": "digi_prods_",
25 | "bigSize": "400X400"
26 | },
27 | "stripe": {
28 | "publishable_key": "pk_test_51GzkT8C75ex8AkqxBjjmzLHcc1aBuKKkdxk0IQDhXfx58yaCtcOHm1rRm1zZbfYtOvesyS459XnBwcir8o6hVeQt00s2xbwlpk",
29 | "secret_key": "sk_test_51GzkT8C75ex8AkqxNlnmuLprbZGDFTvbL2jGMVkyH70ZHXWRXfTeGCPeRlZjhhrABUm8s1GAGT43T5skBUv4hqBv00GrCiFOpg",
30 | "successUrl": "http://localhost:3000/order-success",
31 | "cancelUrl": "http://localhost:3000/order-cancel",
32 | "webhookSecret": "whsec_65091664c52548dae5e241f06b7b885826abc096430fc3fcfea6fe567623d859"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/nest-cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/nest-cli",
3 | "collection": "@nestjs/schematics",
4 | "sourceRoot": "src"
5 | }
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "digizone",
3 | "version": "0.0.1",
4 | "description": "",
5 | "author": "",
6 | "private": true,
7 | "license": "UNLICENSED",
8 | "scripts": {
9 | "prebuild": "rimraf dist",
10 | "build": "nest build",
11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
12 | "start": "nest start",
13 | "start:dev": "nest start --watch",
14 | "start:debug": "nest start --debug --watch",
15 | "start:prod": "node dist/main",
16 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
17 | "test": "jest",
18 | "test:watch": "jest --watch",
19 | "test:cov": "jest --coverage",
20 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
21 | "test:e2e": "jest --config ./test/jest-e2e.json"
22 | },
23 | "dependencies": {
24 | "@nestjs/common": "^9.0.0",
25 | "@nestjs/core": "^9.0.0",
26 | "@nestjs/mapped-types": "*",
27 | "@nestjs/mongoose": "^9.2.0",
28 | "@nestjs/platform-express": "^9.0.0",
29 | "@types/bcrypt": "^5.0.0",
30 | "@types/csurf": "^1.11.2",
31 | "@types/jsonwebtoken": "^8.5.9",
32 | "@types/mongoose": "^5.11.97",
33 | "axios": "^1.1.3",
34 | "bcrypt": "^5.1.0",
35 | "class-validator": "^0.13.2",
36 | "cloudinary": "^1.32.0",
37 | "config": "^3.3.8",
38 | "cookie-parser": "^1.4.6",
39 | "csurf": "^1.11.0",
40 | "form-data": "^4.0.0",
41 | "jsonwebtoken": "^8.5.1",
42 | "nestjs-stripe": "^1.0.0",
43 | "qs-to-mongo": "^3.0.0",
44 | "reflect-metadata": "^0.1.13",
45 | "rimraf": "^3.0.2",
46 | "rxjs": "^7.2.0",
47 | "stripe": "^10.14.0"
48 | },
49 | "devDependencies": {
50 | "@nestjs/cli": "^9.0.0",
51 | "@nestjs/schematics": "^9.0.0",
52 | "@nestjs/testing": "^9.0.0",
53 | "@types/config": "^3.3.0",
54 | "@types/cookie-parser": "^1.4.3",
55 | "@types/express": "^4.17.13",
56 | "@types/jest": "28.1.8",
57 | "@types/node": "^16.0.0",
58 | "@types/supertest": "^2.0.11",
59 | "@typescript-eslint/eslint-plugin": "^5.0.0",
60 | "@typescript-eslint/parser": "^5.0.0",
61 | "eslint": "^8.0.1",
62 | "eslint-config-prettier": "^8.3.0",
63 | "eslint-plugin-prettier": "^4.0.0",
64 | "jest": "28.1.3",
65 | "prettier": "^2.3.2",
66 | "source-map-support": "^0.5.20",
67 | "supertest": "^6.1.3",
68 | "ts-jest": "28.0.8",
69 | "ts-loader": "^9.2.3",
70 | "ts-node": "^10.0.0",
71 | "tsconfig-paths": "4.1.0",
72 | "typescript": "^4.7.4"
73 | },
74 | "jest": {
75 | "moduleFileExtensions": [
76 | "js",
77 | "json",
78 | "ts"
79 | ],
80 | "rootDir": "src",
81 | "testRegex": ".*\\.spec\\.ts$",
82 | "transform": {
83 | "^.+\\.(t|j)s$": "ts-jest"
84 | },
85 | "collectCoverageFrom": [
86 | "**/*.(t|j)s"
87 | ],
88 | "coverageDirectory": "../coverage",
89 | "testEnvironment": "node"
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/app.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { AppController } from './app.controller';
3 | import { AppService } from './app.service';
4 |
5 | describe('AppController', () => {
6 | let appController: AppController;
7 |
8 | beforeEach(async () => {
9 | const app: TestingModule = await Test.createTestingModule({
10 | controllers: [AppController],
11 | providers: [AppService],
12 | }).compile();
13 |
14 | appController = app.get(AppController);
15 | });
16 |
17 | describe('root', () => {
18 | it('should return "Hello World!"', () => {
19 | expect(appController.getHello()).toBe('Hello World!');
20 | });
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/src/app.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Get, Req } from '@nestjs/common';
2 | import { Request } from 'express';
3 | import { AppService } from './app.service';
4 |
5 | @Controller()
6 | export class AppController {
7 | constructor(private readonly appService: AppService) {}
8 |
9 | @Get()
10 | getHello(): string {
11 | return this.appService.getHello();
12 | }
13 |
14 | @Get('/test')
15 | getTest(): string {
16 | return this.appService.getTest();
17 | }
18 |
19 | @Get('/csrf-token')
20 | getCsrfToken(@Req() req: Request): any {
21 | return {
22 | result: req.csrfToken(),
23 | };
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/app.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { MongooseModule } from '@nestjs/mongoose';
3 | import { AppController } from './app.controller';
4 | import { AppService } from './app.service';
5 | import config from 'config';
6 | import { AllExceptionFilter } from './httpExceptionFilter';
7 | import { UsersModule } from './users/users.module';
8 | import { ProductsModule } from './products/products.module';
9 | import { OrdersModule } from './orders/orders.module';
10 |
11 | @Module({
12 | imports: [
13 | MongooseModule.forRoot(config.get('mongoDbUrl'), {
14 | useNewUrlParser: true,
15 | keepAlive: true,
16 | useUnifiedTopology: true,
17 | w: 1,
18 | }),
19 | UsersModule,
20 | ProductsModule,
21 | OrdersModule,
22 | ],
23 | controllers: [AppController],
24 | providers: [
25 | AppService,
26 | {
27 | provide: 'APP_FILTER',
28 | useClass: AllExceptionFilter,
29 | },
30 | ],
31 | })
32 | export class AppModule {}
33 |
--------------------------------------------------------------------------------
/src/app.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 |
3 | @Injectable()
4 | export class AppService {
5 | getHello(): string {
6 | return 'Hello World!';
7 | }
8 |
9 | getTest(): string {
10 | return 'Test :: The Heart Coder (DigiZone)';
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/httpExceptionFilter.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ArgumentsHost,
3 | Catch,
4 | ExceptionFilter,
5 | HttpException,
6 | HttpStatus,
7 | } from '@nestjs/common';
8 | import { HttpAdapterHost } from '@nestjs/core';
9 |
10 | export interface HttpExceptionResponse {
11 | statusCode: number;
12 | message: string;
13 | error: string;
14 | }
15 |
16 | @Catch()
17 | export class AllExceptionFilter implements ExceptionFilter {
18 | constructor(private readonly httpAdapterHost: HttpAdapterHost) {}
19 | // For error catch response
20 | catch(exception: unknown, host: ArgumentsHost): void {
21 | const { httpAdapter } = this.httpAdapterHost;
22 |
23 | const ctx = host.switchToHttp();
24 |
25 | const httpStatus =
26 | exception instanceof HttpException
27 | ? exception.getStatus()
28 | : HttpStatus.INTERNAL_SERVER_ERROR;
29 | console.log('exception ==> ', exception);
30 |
31 | const exceptionResponse =
32 | exception instanceof HttpException
33 | ? exception.getResponse()
34 | : String(exception);
35 |
36 | const responseBody = {
37 | success: false,
38 | statusCode: httpStatus,
39 | timestamp: new Date().toISOString(),
40 | path: httpAdapter.getRequestUrl(ctx.getRequest()),
41 | message:
42 | (exceptionResponse as HttpExceptionResponse).error ||
43 | (exceptionResponse as HttpExceptionResponse).message ||
44 | exceptionResponse ||
45 | 'Something went wrong',
46 | errorResponse: exceptionResponse as HttpExceptionResponse,
47 | };
48 |
49 | httpAdapter.reply(ctx.getResponse(), responseBody, httpStatus);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { NestFactory } from '@nestjs/core';
2 | import { AppModule } from './app.module';
3 | import config from 'config';
4 | import { TransformationInterceptor } from './responseInterceptor';
5 | import cookieParser from 'cookie-parser';
6 | import { NextFunction, raw, Request, Response } from 'express';
7 | import csurf from 'csurf';
8 | const ROOT_IGNORED_PATHS = ['/api/v1/orders/webhook'];
9 |
10 | async function bootstrap() {
11 | const app = await NestFactory.create(AppModule, { rawBody: true });
12 |
13 | app.use(cookieParser());
14 |
15 | app.use('/api/v1/orders/webhook', raw({ type: '*/*' }));
16 |
17 | const csrfMiddleware = csurf({
18 | cookie: true,
19 | });
20 |
21 | app.use((req: Request, res: Response, next: NextFunction) => {
22 | if (ROOT_IGNORED_PATHS.includes(req.path)) {
23 | return next();
24 | }
25 | return csrfMiddleware(req, res, next);
26 | });
27 |
28 | app.setGlobalPrefix(config.get('appPrefix'));
29 | app.useGlobalInterceptors(new TransformationInterceptor());
30 | await app.listen(config.get('port'), () => {
31 | return console.log(`Server is running on port ${config.get('port')}`);
32 | });
33 | }
34 | bootstrap();
35 |
--------------------------------------------------------------------------------
/src/orders/dto/checkout.dto.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ArrayMinSize,
3 | IsArray,
4 | IsNotEmpty,
5 | IsNumber,
6 | IsString,
7 | ValidateNested,
8 | } from 'class-validator';
9 | export class checkoutDto {
10 | @IsString()
11 | @IsNotEmpty()
12 | skuPriceId: string;
13 |
14 | @IsNumber()
15 | @IsNotEmpty()
16 | quantity: number;
17 |
18 | @IsString()
19 | @IsNotEmpty()
20 | skuId: string;
21 | }
22 |
23 | export class checkoutDtoArr {
24 | @IsArray()
25 | @IsNotEmpty()
26 | @ValidateNested({ each: true })
27 | @ArrayMinSize(1)
28 | checkoutDetails: checkoutDto[];
29 | }
30 |
--------------------------------------------------------------------------------
/src/orders/dto/create-order.dto.ts:
--------------------------------------------------------------------------------
1 | export class CreateOrderDto {}
2 |
--------------------------------------------------------------------------------
/src/orders/dto/update-order.dto.ts:
--------------------------------------------------------------------------------
1 | import { PartialType } from '@nestjs/mapped-types';
2 | import { CreateOrderDto } from './create-order.dto';
3 |
4 | export class UpdateOrderDto extends PartialType(CreateOrderDto) {}
5 |
--------------------------------------------------------------------------------
/src/orders/orders.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { OrdersController } from './orders.controller';
3 | import { OrdersService } from './orders.service';
4 |
5 | describe('OrdersController', () => {
6 | let controller: OrdersController;
7 |
8 | beforeEach(async () => {
9 | const module: TestingModule = await Test.createTestingModule({
10 | controllers: [OrdersController],
11 | providers: [OrdersService],
12 | }).compile();
13 |
14 | controller = module.get(OrdersController);
15 | });
16 |
17 | it('should be defined', () => {
18 | expect(controller).toBeDefined();
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/src/orders/orders.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Controller,
3 | Get,
4 | Post,
5 | Body,
6 | Patch,
7 | Param,
8 | Delete,
9 | Req,
10 | Query,
11 | Headers,
12 | } from '@nestjs/common';
13 | import { checkoutDtoArr } from './dto/checkout.dto';
14 | import { OrdersService } from './orders.service';
15 |
16 | @Controller('orders')
17 | export class OrdersController {
18 | constructor(private readonly ordersService: OrdersService) {}
19 |
20 | @Get()
21 | async findAll(@Query('status') status: string, @Req() req: any) {
22 | return await this.ordersService.findAll(status, req.user);
23 | }
24 |
25 | @Get(':id')
26 | async findOne(@Param('id') id: string) {
27 | return await this.ordersService.findOne(id);
28 | }
29 |
30 | @Post('/checkout')
31 | async checkout(@Body() body: checkoutDtoArr, @Req() req: any) {
32 | return await this.ordersService.checkout(body, req.user);
33 | }
34 |
35 | @Post('/webhook')
36 | async webhook(
37 | @Body() rawBody: Buffer,
38 | @Headers('stripe-signature') sig: string,
39 | ) {
40 | return await this.ordersService.webhook(rawBody, sig);
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/orders/orders.module.ts:
--------------------------------------------------------------------------------
1 | import {
2 | MiddlewareConsumer,
3 | Module,
4 | NestModule,
5 | RequestMethod,
6 | } from '@nestjs/common';
7 | import { OrdersService } from './orders.service';
8 | import { OrdersController } from './orders.controller';
9 | import { UserRepository } from 'src/shared/repositories/user.repository';
10 | import { ProductRepository } from 'src/shared/repositories/product.repository';
11 | import { OrdersRepository } from 'src/shared/repositories/order.repository';
12 | import { APP_GUARD } from '@nestjs/core';
13 | import { RolesGuard } from 'src/shared/middleware/roles.guard';
14 | import { StripeModule } from 'nestjs-stripe';
15 | import config from 'config';
16 | import { Products, ProductSchema } from 'src/shared/schema/products';
17 | import { License, LicenseSchema } from 'src/shared/schema/license';
18 | import { Orders, OrderSchema } from 'src/shared/schema/orders';
19 | import { Users, UserSchema } from 'src/shared/schema/users';
20 | import { MongooseModule } from '@nestjs/mongoose';
21 | import { AuthMiddleware } from 'src/shared/middleware/auth';
22 |
23 | @Module({
24 | controllers: [OrdersController],
25 | providers: [
26 | OrdersService,
27 | UserRepository,
28 | ProductRepository,
29 | OrdersRepository,
30 | {
31 | provide: APP_GUARD,
32 | useClass: RolesGuard,
33 | },
34 | ],
35 | imports: [
36 | StripeModule.forRoot({
37 | apiKey: config.get('stripe.secret_key'),
38 | apiVersion: '2022-08-01',
39 | }),
40 | MongooseModule.forFeature([{ name: Products.name, schema: ProductSchema }]),
41 | MongooseModule.forFeature([{ name: License.name, schema: LicenseSchema }]),
42 | MongooseModule.forFeature([{ name: Users.name, schema: UserSchema }]),
43 | MongooseModule.forFeature([{ name: Orders.name, schema: OrderSchema }]),
44 | ],
45 | })
46 | export class OrdersModule implements NestModule {
47 | configure(consumer: MiddlewareConsumer) {
48 | consumer
49 | .apply(AuthMiddleware)
50 | .exclude({
51 | path: `${config.get('appPrefix')}/orders/webhook`,
52 | method: RequestMethod.POST,
53 | })
54 | .forRoutes(OrdersController);
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/orders/orders.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { OrdersService } from './orders.service';
3 |
4 | describe('OrdersService', () => {
5 | let service: OrdersService;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [OrdersService],
10 | }).compile();
11 |
12 | service = module.get(OrdersService);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(service).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/orders/orders.service.ts:
--------------------------------------------------------------------------------
1 | import { BadRequestException, Inject, Injectable } from '@nestjs/common';
2 | import { InjectStripe } from 'nestjs-stripe';
3 | import { OrdersRepository } from 'src/shared/repositories/order.repository';
4 | import { ProductRepository } from 'src/shared/repositories/product.repository';
5 | import { UserRepository } from 'src/shared/repositories/user.repository';
6 | import Stripe from 'stripe';
7 | import { checkoutDtoArr } from './dto/checkout.dto';
8 | import config from 'config';
9 | import { userTypes } from 'src/shared/schema/users';
10 | import { orderStatus, paymentStatus } from 'src/shared/schema/orders';
11 | import { sendEmail } from 'src/shared/utility/mail-handler';
12 |
13 | @Injectable()
14 | export class OrdersService {
15 | constructor(
16 | @InjectStripe() private readonly stripeClient: Stripe,
17 | @Inject(OrdersRepository) private readonly orderDB: OrdersRepository,
18 | @Inject(ProductRepository) private readonly productDB: ProductRepository,
19 | @Inject(UserRepository) private readonly userDB: UserRepository,
20 | ) {}
21 |
22 | async create(createOrderDto: Record) {
23 | try {
24 | const orderExists = await this.orderDB.findOne({
25 | checkoutSessionId: createOrderDto.checkoutSessionId,
26 | });
27 | if (orderExists) return orderExists;
28 | const result = await this.orderDB.create(createOrderDto);
29 | return result;
30 | } catch (error) {
31 | throw error;
32 | }
33 | }
34 |
35 | async findAll(status: string, user: Record) {
36 | try {
37 | const userDetails = await this.userDB.findOne({
38 | _id: user._id.toString(),
39 | });
40 | const query = {} as Record;
41 | if (userDetails.type === userTypes.CUSTOMER) {
42 | query.userId = user._id.toString();
43 | }
44 | if (status) {
45 | query.status = status;
46 | }
47 | const orders = await this.orderDB.find(query);
48 | return {
49 | success: true,
50 | result: orders,
51 | message: 'Orders fetched successfully',
52 | };
53 | } catch (error) {
54 | throw error;
55 | }
56 | }
57 |
58 | async findOne(id: string) {
59 | try {
60 | const result = await this.orderDB.findOne({ _id: id });
61 | return {
62 | success: true,
63 | result,
64 | message: 'Order fetched successfully',
65 | };
66 | } catch (error) {
67 | throw error;
68 | }
69 | }
70 |
71 | async checkout(body: checkoutDtoArr, user: Record) {
72 | try {
73 | const lineItems = [];
74 | const cartItems = body.checkoutDetails;
75 | for (const item of cartItems) {
76 | const itemsAreInStock = await this.productDB.findLicense({
77 | productSku: item.skuId,
78 | isSold: false,
79 | });
80 | if (itemsAreInStock.length <= item.quantity) {
81 | lineItems.push({
82 | price: item.skuPriceId,
83 | quantity: item.quantity,
84 | adjustable_quantity: {
85 | enabled: true,
86 | maximum: 5,
87 | minimum: 1,
88 | },
89 | });
90 | }
91 | }
92 |
93 | if (lineItems.length === 0) {
94 | throw new BadRequestException(
95 | 'These products are not available right now',
96 | );
97 | }
98 |
99 | const session = await this.stripeClient.checkout.sessions.create({
100 | line_items: lineItems,
101 | metadata: {
102 | userId: user._id.toString(),
103 | },
104 | mode: 'payment',
105 | billing_address_collection: 'required',
106 | phone_number_collection: {
107 | enabled: true,
108 | },
109 | customer_email: user.email,
110 | success_url: config.get('stripe.successUrl'),
111 | cancel_url: config.get('stripe.cancelUrl'),
112 | });
113 |
114 | return {
115 | message: 'Payment checkout session successfully created',
116 | success: true,
117 | result: session.url,
118 | };
119 | } catch (error) {
120 | throw error;
121 | }
122 | }
123 |
124 | async webhook(rawBody: Buffer, sig: string) {
125 | try {
126 | let event;
127 | try {
128 | event = this.stripeClient.webhooks.constructEvent(
129 | rawBody,
130 | sig,
131 | config.get('stripe.webhookSecret'),
132 | );
133 | } catch (err) {
134 | throw new BadRequestException('Webhook Error:', err.message);
135 | }
136 |
137 | if (event.type === 'checkout.session.completed') {
138 | const session = event.data.object as Stripe.Checkout.Session;
139 | const orderData = await this.createOrderObject(session);
140 | const order = await this.create(orderData);
141 | if (session.payment_status === paymentStatus.paid) {
142 | if (order.orderStatus !== orderStatus.completed) {
143 | for (const item of order.orderedItems) {
144 | const licenses = await this.getLicense(orderData.orderId, item);
145 | item.licenses = licenses;
146 | }
147 | }
148 | await this.fullfillOrder(session.id, {
149 | orderStatus: orderStatus.completed,
150 | isOrderDelivered: true,
151 | ...orderData,
152 | });
153 | this.sendOrderEmail(
154 | orderData.customerEmail,
155 | orderData.orderId,
156 | `${config.get('emailService.emailTemplates.orderSuccess')}${
157 | order._id
158 | }`,
159 | );
160 | }
161 | } else {
162 | console.log('Unhandled event type', event.type);
163 | }
164 | } catch (error) {
165 | throw error;
166 | }
167 | }
168 |
169 | async fullfillOrder(
170 | checkoutSessionId: string,
171 | updateOrderDto: Record,
172 | ) {
173 | try {
174 | return await this.orderDB.findOneAndUpdate(
175 | { checkoutSessionId },
176 | updateOrderDto,
177 | { new: true },
178 | );
179 | } catch (error) {
180 | throw error;
181 | }
182 | }
183 |
184 | async sendOrderEmail(email: string, orderId: string, orderLink: string) {
185 | await sendEmail(
186 | email,
187 | config.get('emailService.emailTemplates.orderSuccess'),
188 | 'Order Success - Digizone',
189 | {
190 | orderId,
191 | orderLink,
192 | },
193 | );
194 | }
195 | async getLicense(orderId: string, item: Record) {
196 | try {
197 | const product = await this.productDB.findOne({
198 | _id: item.productId,
199 | });
200 |
201 | const skuDetails = product.skuDetails.find(
202 | (sku) => sku.skuCode === item.skuCode,
203 | );
204 |
205 | const licenses = await this.productDB.findLicense(
206 | {
207 | productSku: skuDetails._id,
208 | isSold: false,
209 | },
210 | item.quantity,
211 | );
212 |
213 | const licenseIds = licenses.map((license) => license._id);
214 |
215 | await this.productDB.updateLicenseMany(
216 | {
217 | _id: {
218 | $in: licenseIds,
219 | },
220 | },
221 | {
222 | isSold: true,
223 | orderId,
224 | },
225 | );
226 |
227 | return licenses.map((license) => license.licenseKey);
228 | } catch (error) {
229 | throw error;
230 | }
231 | }
232 |
233 | async createOrderObject(session: Stripe.Checkout.Session) {
234 | try {
235 | const lineItems = await this.stripeClient.checkout.sessions.listLineItems(
236 | session.id,
237 | );
238 | const orderData = {
239 | orderId: Math.floor(new Date().valueOf() * Math.random()) + '',
240 | userId: session.metadata?.userId?.toString(),
241 | customerAddress: session.customer_details?.address,
242 | customerEmail: session.customer_email,
243 | customerPhoneNumber: session.customer_details?.phone,
244 | paymentInfo: {
245 | paymentMethod: session.payment_method_types[0],
246 | paymentIntentId: session.payment_intent,
247 | paymentDate: new Date(),
248 | paymentAmount: session.amount_total / 100,
249 | paymentStatus: session.payment_status,
250 | },
251 | orderDate: new Date(),
252 | checkoutSessionId: session.id,
253 | orderedItems: lineItems.data.map((item) => {
254 | item.price.metadata.quantity = item.quantity + '';
255 | return item.price.metadata;
256 | }),
257 | };
258 | return orderData;
259 | } catch (error) {
260 | throw error;
261 | }
262 | }
263 | }
264 |
--------------------------------------------------------------------------------
/src/products/dto/create-product.dto.ts:
--------------------------------------------------------------------------------
1 | import {
2 | IsArray,
3 | IsEnum,
4 | IsNotEmpty,
5 | IsOptional,
6 | IsString,
7 | } from 'class-validator';
8 | import {
9 | baseType,
10 | categoryType,
11 | platformType,
12 | SkuDetails,
13 | } from 'src/shared/schema/products';
14 |
15 | export class CreateProductDto {
16 | @IsString()
17 | @IsNotEmpty()
18 | productName: string;
19 |
20 | @IsString()
21 | @IsNotEmpty()
22 | description: string;
23 |
24 | @IsOptional()
25 | image?: string;
26 |
27 | @IsOptional()
28 | imageDetails?: Record;
29 |
30 | @IsString()
31 | @IsNotEmpty()
32 | @IsEnum(categoryType)
33 | category: string;
34 |
35 | @IsString()
36 | @IsNotEmpty()
37 | @IsEnum(platformType)
38 | platformType: string;
39 |
40 | @IsString()
41 | @IsNotEmpty()
42 | @IsEnum(baseType)
43 | baseType: string;
44 |
45 | @IsString()
46 | @IsNotEmpty()
47 | productUrl: string;
48 |
49 | @IsString()
50 | @IsNotEmpty()
51 | downloadUrl: string;
52 |
53 | @IsArray()
54 | @IsNotEmpty()
55 | requirementSpecification: Record[];
56 |
57 | @IsArray()
58 | @IsNotEmpty()
59 | highlights: string[];
60 |
61 | @IsOptional()
62 | @IsArray()
63 | skuDetails: SkuDetails[];
64 |
65 | @IsOptional()
66 | stripeProductId?: string;
67 | }
68 |
--------------------------------------------------------------------------------
/src/products/dto/get-product-query-dto.ts:
--------------------------------------------------------------------------------
1 | export class GetProductQueryDto {
2 | search?: string;
3 | category?: string;
4 | platformType?: string;
5 | baseType?: string;
6 | homepage?: string;
7 | }
8 |
--------------------------------------------------------------------------------
/src/products/dto/product-sku.dto.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ArrayMinSize,
3 | IsArray,
4 | IsBoolean,
5 | IsNotEmpty,
6 | IsNumber,
7 | IsOptional,
8 | IsString,
9 | ValidateNested,
10 | } from 'class-validator';
11 |
12 | export class ProductSkuDto {
13 | @IsString()
14 | @IsNotEmpty()
15 | skuName: string;
16 |
17 | @IsNotEmpty()
18 | @IsNumber()
19 | price: number;
20 |
21 | @IsNotEmpty()
22 | @IsNumber()
23 | validity: number;
24 |
25 | @IsNotEmpty()
26 | @IsBoolean()
27 | lifetime: boolean;
28 |
29 | @IsOptional()
30 | stripePriceId?: string;
31 |
32 | @IsOptional()
33 | skuCode?: string;
34 | }
35 |
36 | export class ProductSkuDtoArr {
37 | @IsArray()
38 | @IsNotEmpty()
39 | @ValidateNested({ each: true })
40 | @ArrayMinSize(1)
41 | skuDetails: ProductSkuDto[];
42 | }
43 |
--------------------------------------------------------------------------------
/src/products/dto/update-product.dto.ts:
--------------------------------------------------------------------------------
1 | import { PartialType } from '@nestjs/mapped-types';
2 | import { CreateProductDto } from './create-product.dto';
3 |
4 | export class UpdateProductDto extends PartialType(CreateProductDto) {}
5 |
--------------------------------------------------------------------------------
/src/products/products.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { ProductsController } from './products.controller';
3 | import { ProductsService } from './products.service';
4 |
5 | describe('ProductsController', () => {
6 | let controller: ProductsController;
7 |
8 | beforeEach(async () => {
9 | const module: TestingModule = await Test.createTestingModule({
10 | controllers: [ProductsController],
11 | providers: [ProductsService],
12 | }).compile();
13 |
14 | controller = module.get(ProductsController);
15 | });
16 |
17 | it('should be defined', () => {
18 | expect(controller).toBeDefined();
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/src/products/products.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Controller,
3 | Get,
4 | Post,
5 | Body,
6 | Patch,
7 | Param,
8 | Delete,
9 | HttpCode,
10 | Query,
11 | UseInterceptors,
12 | UploadedFile,
13 | Put,
14 | Req,
15 | } from '@nestjs/common';
16 | import { ProductsService } from './products.service';
17 | import { CreateProductDto } from './dto/create-product.dto';
18 | import { Roles } from 'src/shared/middleware/role.decorators';
19 | import { userTypes } from 'src/shared/schema/users';
20 | import { GetProductQueryDto } from './dto/get-product-query-dto';
21 | import { FileInterceptor } from '@nestjs/platform-express';
22 | import config from 'config';
23 | import { ProductSkuDto, ProductSkuDtoArr } from './dto/product-sku.dto';
24 |
25 | @Controller('products')
26 | export class ProductsController {
27 | constructor(private readonly productsService: ProductsService) {}
28 |
29 | @Post()
30 | @HttpCode(201)
31 | @Roles(userTypes.ADMIN)
32 | async create(@Body() createProductDto: CreateProductDto) {
33 | return await this.productsService.createProduct(createProductDto);
34 | }
35 |
36 | @Get()
37 | findAll(@Query() query: GetProductQueryDto) {
38 | return this.productsService.findAllProducts(query);
39 | }
40 |
41 | @Get(':id')
42 | async findOne(@Param('id') id: string) {
43 | return this.productsService.findOneProduct(id);
44 | }
45 |
46 | @Patch(':id')
47 | @Roles(userTypes.ADMIN)
48 | async update(
49 | @Param('id') id: string,
50 | @Body() updateProductDto: CreateProductDto,
51 | ) {
52 | return await this.productsService.updateProduct(id, updateProductDto);
53 | }
54 |
55 | @Delete(':id')
56 | async remove(@Param('id') id: string) {
57 | return await this.productsService.removeProduct(id);
58 | }
59 |
60 | @Post('/:id/image')
61 | @Roles(userTypes.ADMIN)
62 | @UseInterceptors(
63 | FileInterceptor('productImage', {
64 | dest: config.get('fileStoragePath'),
65 | limits: {
66 | fileSize: 3145728, // 3 MB
67 | },
68 | }),
69 | )
70 | async uploadProductImage(
71 | @Param('id') id: string,
72 | @UploadedFile() file: ParameterDecorator,
73 | ) {
74 | return await this.productsService.uploadProductImage(id, file);
75 | }
76 |
77 | @Post('/:productId/skus')
78 | @Roles(userTypes.ADMIN)
79 | async updateProductSku(
80 | @Param('productId') productId: string,
81 | @Body() updateProductSkuDto: ProductSkuDtoArr,
82 | ) {
83 | return await this.productsService.updateProductSku(
84 | productId,
85 | updateProductSkuDto,
86 | );
87 | }
88 |
89 | @Put('/:productId/skus/:skuId')
90 | @Roles(userTypes.ADMIN)
91 | async updateProductSkuById(
92 | @Param('productId') productId: string,
93 | @Param('skuId') skuId: string,
94 | @Body() updateProductSkuDto: ProductSkuDto,
95 | ) {
96 | return await this.productsService.updateProductSkuById(
97 | productId,
98 | skuId,
99 | updateProductSkuDto,
100 | );
101 | }
102 |
103 | @Delete('/:productId/skus/:skuId')
104 | @Roles(userTypes.ADMIN)
105 | async deleteSkuById(
106 | @Param('productId') productId: string,
107 | @Param('skuId') skuId: string,
108 | ) {
109 | return await this.productsService.deleteProductSkuById(productId, skuId);
110 | }
111 |
112 | @Post('/:productId/skus/:skuId/licenses')
113 | @Roles(userTypes.ADMIN)
114 | async addProductSkuLicense(
115 | @Param('productId') productId: string,
116 | @Param('skuId') skuId: string,
117 | @Body('licenseKey') licenseKey: string,
118 | ) {
119 | return await this.productsService.addProductSkuLicense(
120 | productId,
121 | skuId,
122 | licenseKey,
123 | );
124 | }
125 |
126 | @Delete('/licenses/:licenseKeyId')
127 | @Roles(userTypes.ADMIN)
128 | async removeProductSkuLicense(@Param('licenseKeyId') licenseId: string) {
129 | return await this.productsService.removeProductSkuLicense(licenseId);
130 | }
131 |
132 | @Get('/:productId/skus/:skuId/licenses')
133 | @Roles(userTypes.ADMIN)
134 | async getProductSkuLicenses(
135 | @Param('productId') productId: string,
136 | @Param('skuId') skuId: string,
137 | ) {
138 | return await this.productsService.getProductSkuLicenses(productId, skuId);
139 | }
140 |
141 | @Put('/:productId/skus/:skuId/licenses/:licenseKeyId')
142 | @Roles(userTypes.ADMIN)
143 | async updateProductSkuLicense(
144 | @Param('productId') productId: string,
145 | @Param('skuId') skuId: string,
146 | @Param('licenseKeyId') licenseKeyId: string,
147 | @Body('licenseKey') licenseKey: string,
148 | ) {
149 | return await this.productsService.updateProductSkuLicense(
150 | productId,
151 | skuId,
152 | licenseKeyId,
153 | licenseKey,
154 | );
155 | }
156 |
157 | @Post('/:productId/reviews')
158 | @Roles(userTypes.CUSTOMER)
159 | async addProductReview(
160 | @Param('productId') productId: string,
161 | @Body('rating') rating: number,
162 | @Body('review') review: string,
163 | @Req() req: any,
164 | ) {
165 | return await this.productsService.addProductReview(
166 | productId,
167 | rating,
168 | review,
169 | req.user,
170 | );
171 | }
172 |
173 | @Delete('/:productId/reviews/:reviewId')
174 | async removeProductReview(
175 | @Param('productId') productId: string,
176 | @Param('reviewId') reviewId: string,
177 | ) {
178 | return await this.productsService.removeProductReview(productId, reviewId);
179 | }
180 | }
181 |
--------------------------------------------------------------------------------
/src/products/products.module.ts:
--------------------------------------------------------------------------------
1 | import {
2 | MiddlewareConsumer,
3 | Module,
4 | NestModule,
5 | RequestMethod,
6 | } from '@nestjs/common';
7 | import { ProductsService } from './products.service';
8 | import { ProductsController } from './products.controller';
9 | import { MongooseModule } from '@nestjs/mongoose';
10 | import { Products, ProductSchema } from 'src/shared/schema/products';
11 | import { Users, UserSchema } from 'src/shared/schema/users';
12 | import { StripeModule } from 'nestjs-stripe';
13 | import config from 'config';
14 | import { AuthMiddleware } from 'src/shared/middleware/auth';
15 | import { ProductRepository } from 'src/shared/repositories/product.repository';
16 | import { UserRepository } from 'src/shared/repositories/user.repository';
17 | import { APP_GUARD } from '@nestjs/core';
18 | import { RolesGuard } from 'src/shared/middleware/roles.guard';
19 | import { License, LicenseSchema } from 'src/shared/schema/license';
20 | import { Orders, OrderSchema } from 'src/shared/schema/orders';
21 | import { OrdersRepository } from 'src/shared/repositories/order.repository';
22 |
23 | @Module({
24 | controllers: [ProductsController],
25 | providers: [
26 | ProductsService,
27 | ProductRepository,
28 | UserRepository,
29 | OrdersRepository,
30 | { provide: APP_GUARD, useClass: RolesGuard },
31 | ],
32 | imports: [
33 | MongooseModule.forFeature([{ name: Products.name, schema: ProductSchema }]),
34 | MongooseModule.forFeature([{ name: Users.name, schema: UserSchema }]),
35 | MongooseModule.forFeature([{ name: License.name, schema: LicenseSchema }]),
36 | MongooseModule.forFeature([{ name: Orders.name, schema: OrderSchema }]),
37 | StripeModule.forRoot({
38 | apiKey: config.get('stripe.secret_key'),
39 | apiVersion: '2022-08-01',
40 | }),
41 | ],
42 | })
43 | export class ProductsModule implements NestModule {
44 | configure(consumer: MiddlewareConsumer) {
45 | consumer
46 | .apply(AuthMiddleware)
47 | .exclude(
48 | {
49 | path: `${config.get('appPrefix')}/products`,
50 | method: RequestMethod.GET,
51 | },
52 | {
53 | path: `${config.get('appPrefix')}/products/:id`,
54 | method: RequestMethod.GET,
55 | },
56 | )
57 | .forRoutes(ProductsController);
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/products/products.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { ProductsService } from './products.service';
3 |
4 | describe('ProductsService', () => {
5 | let service: ProductsService;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [ProductsService],
10 | }).compile();
11 |
12 | service = module.get(ProductsService);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(service).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/products/products.service.ts:
--------------------------------------------------------------------------------
1 | import { BadRequestException, Inject, Injectable } from '@nestjs/common';
2 | import { InjectStripe } from 'nestjs-stripe';
3 | import { ProductRepository } from 'src/shared/repositories/product.repository';
4 | import { Products } from 'src/shared/schema/products';
5 | import Stripe from 'stripe';
6 | import { CreateProductDto } from './dto/create-product.dto';
7 | import { GetProductQueryDto } from './dto/get-product-query-dto';
8 | import qs2m from 'qs-to-mongo';
9 | import cloudinary from 'cloudinary';
10 | import config from 'config';
11 | import { unlinkSync } from 'fs';
12 | import { ProductSkuDto, ProductSkuDtoArr } from './dto/product-sku.dto';
13 | import { OrdersRepository } from 'src/shared/repositories/order.repository';
14 |
15 | @Injectable()
16 | export class ProductsService {
17 | constructor(
18 | @Inject(ProductRepository) private readonly productDB: ProductRepository,
19 | @Inject(OrdersRepository) private readonly orderDB: OrdersRepository,
20 | @InjectStripe() private readonly stripeClient: Stripe,
21 | ) {
22 | cloudinary.v2.config({
23 | cloud_name: config.get('cloudinary.cloud_name'),
24 | api_key: config.get('cloudinary.api_key'),
25 | api_secret: config.get('cloudinary.api_secret'),
26 | });
27 | }
28 | async createProduct(createProductDto: CreateProductDto): Promise<{
29 | message: string;
30 | result: Products;
31 | success: boolean;
32 | }> {
33 | try {
34 | // create a product in stripe
35 | if (!createProductDto.stripeProductId) {
36 | const createdProductInStripe = await this.stripeClient.products.create({
37 | name: createProductDto.productName,
38 | description: createProductDto.description,
39 | });
40 | createProductDto.stripeProductId = createdProductInStripe.id;
41 | }
42 |
43 | const createdProductInDB = await this.productDB.create(createProductDto);
44 | return {
45 | message: 'Product created successfully',
46 | result: createdProductInDB,
47 | success: true,
48 | };
49 | } catch (error) {
50 | throw error;
51 | }
52 | }
53 |
54 | async findAllProducts(query: GetProductQueryDto) {
55 | try {
56 | let callForHomePage = false;
57 | if (query.homepage) {
58 | callForHomePage = true;
59 | }
60 | delete query.homepage;
61 | const { criteria, options, links } = qs2m(query);
62 | if (callForHomePage) {
63 | const products = await this.productDB.findProductWithGroupBy();
64 | return {
65 | message:
66 | products.length > 0
67 | ? 'Products fetched successfully'
68 | : 'No products found',
69 | result: products,
70 | success: true,
71 | };
72 | }
73 | const { totalProductCount, products } = await this.productDB.find(
74 | criteria,
75 | options,
76 | );
77 | return {
78 | message:
79 | products.length > 0
80 | ? 'Products fetched successfully'
81 | : 'No products found',
82 | result: {
83 | metadata: {
84 | skip: options.skip || 0,
85 | limit: options.limit || 10,
86 | total: totalProductCount,
87 | pages: options.limit
88 | ? Math.ceil(totalProductCount / options.limit)
89 | : 1,
90 | links: links('/', totalProductCount),
91 | },
92 | products,
93 | },
94 | success: true,
95 | };
96 | } catch (error) {
97 | throw error;
98 | }
99 | }
100 |
101 | async findOneProduct(id: string): Promise<{
102 | message: string;
103 | result: { product: Products; relatedProducts: Products[] };
104 | success: boolean;
105 | }> {
106 | try {
107 | const product: Products = await this.productDB.findOne({ _id: id });
108 | if (!product) {
109 | throw new Error('Product does not exist');
110 | }
111 | const relatedProducts: Products[] =
112 | await this.productDB.findRelatedProducts({
113 | category: product.category,
114 | _id: { $ne: id },
115 | });
116 |
117 | return {
118 | message: 'Product fetched successfully',
119 | result: { product, relatedProducts },
120 | success: true,
121 | };
122 | } catch (error) {
123 | throw error;
124 | }
125 | }
126 |
127 | async updateProduct(
128 | id: string,
129 | updateProductDto: CreateProductDto,
130 | ): Promise<{
131 | message: string;
132 | result: Products;
133 | success: boolean;
134 | }> {
135 | try {
136 | const productExist = await this.productDB.findOne({ _id: id });
137 | if (!productExist) {
138 | throw new Error('Product does not exist');
139 | }
140 | const updatedProduct = await this.productDB.findOneAndUpdate(
141 | { _id: id },
142 | updateProductDto,
143 | );
144 | if (!updateProductDto.stripeProductId)
145 | await this.stripeClient.products.update(productExist.stripeProductId, {
146 | name: updateProductDto.productName,
147 | description: updateProductDto.description,
148 | });
149 | return {
150 | message: 'Product updated successfully',
151 | result: updatedProduct,
152 | success: true,
153 | };
154 | } catch (error) {
155 | throw error;
156 | }
157 | }
158 |
159 | async removeProduct(id: string): Promise<{
160 | message: string;
161 | success: boolean;
162 | result: null;
163 | }> {
164 | try {
165 | const productExist = await this.productDB.findOne({ _id: id });
166 | if (!productExist) {
167 | throw new Error('Product does not exist');
168 | }
169 | await this.productDB.findOneAndDelete({ _id: id });
170 | await this.stripeClient.products.del(productExist.stripeProductId);
171 | return {
172 | message: 'Product deleted successfully',
173 | success: true,
174 | result: null,
175 | };
176 | } catch (error) {
177 | throw error;
178 | }
179 | }
180 |
181 | async uploadProductImage(
182 | id: string,
183 | file: any,
184 | ): Promise<{
185 | message: string;
186 | success: boolean;
187 | result: string;
188 | }> {
189 | try {
190 | const product = await this.productDB.findOne({ _id: id });
191 | if (!product) {
192 | throw new Error('Product does not exist');
193 | }
194 | if (product.imageDetails?.public_id) {
195 | await cloudinary.v2.uploader.destroy(product.imageDetails.public_id, {
196 | invalidate: true,
197 | });
198 | }
199 |
200 | const resOfCloudinary = await cloudinary.v2.uploader.upload(file.path, {
201 | folder: config.get('cloudinary.folderPath'),
202 | public_id: `${config.get('cloudinary.publicId_prefix')}${Date.now()}`,
203 | transformation: [
204 | {
205 | width: config.get('cloudinary.bigSize').toString().split('X')[0],
206 | height: config.get('cloudinary.bigSize').toString().split('X')[1],
207 | crop: 'fill',
208 | },
209 | { quality: 'auto' },
210 | ],
211 | });
212 | unlinkSync(file.path);
213 | await this.productDB.findOneAndUpdate(
214 | { _id: id },
215 | {
216 | imageDetails: resOfCloudinary,
217 | image: resOfCloudinary.secure_url,
218 | },
219 | );
220 |
221 | await this.stripeClient.products.update(product.stripeProductId, {
222 | images: [resOfCloudinary.secure_url],
223 | });
224 |
225 | return {
226 | message: 'Image uploaded successfully',
227 | success: true,
228 | result: resOfCloudinary.secure_url,
229 | };
230 | } catch (error) {
231 | throw error;
232 | }
233 | }
234 |
235 | // this is for create one or multiple sku for an product
236 | async updateProductSku(productId: string, data: ProductSkuDtoArr) {
237 | try {
238 | const product = await this.productDB.findOne({ _id: productId });
239 | if (!product) {
240 | throw new Error('Product does not exist');
241 | }
242 |
243 | const skuCode = Math.random().toString(36).substring(2, 5) + Date.now();
244 | for (let i = 0; i < data.skuDetails.length; i++) {
245 | if (!data.skuDetails[i].stripePriceId) {
246 | const stripPriceDetails = await this.stripeClient.prices.create({
247 | unit_amount: data.skuDetails[i].price * 100,
248 | currency: 'inr',
249 | product: product.stripeProductId,
250 | metadata: {
251 | skuCode: skuCode,
252 | lifetime: data.skuDetails[i].lifetime + '',
253 | productId: productId,
254 | price: data.skuDetails[i].price,
255 | productName: product.productName,
256 | productImage: product.image,
257 | },
258 | });
259 | data.skuDetails[i].stripePriceId = stripPriceDetails.id;
260 | }
261 | data.skuDetails[i].skuCode = skuCode;
262 | }
263 |
264 | await this.productDB.findOneAndUpdate(
265 | { _id: productId },
266 | { $push: { skuDetails: data.skuDetails } },
267 | );
268 |
269 | return {
270 | message: 'Product sku updated successfully',
271 | success: true,
272 | result: null,
273 | };
274 | } catch (error) {
275 | throw error;
276 | }
277 | }
278 |
279 | async updateProductSkuById(
280 | productId: string,
281 | skuId: string,
282 | data: ProductSkuDto,
283 | ) {
284 | try {
285 | const product = await this.productDB.findOne({ _id: productId });
286 | if (!product) {
287 | throw new Error('Product does not exist');
288 | }
289 |
290 | const sku = product.skuDetails.find((sku) => sku._id == skuId);
291 | if (!sku) {
292 | throw new Error('Sku does not exist');
293 | }
294 |
295 | if (data.price !== sku.price) {
296 | const priceDetails = await this.stripeClient.prices.create({
297 | unit_amount: data.price * 100,
298 | currency: 'inr',
299 | product: product.stripeProductId,
300 | metadata: {
301 | skuCode: sku.skuCode,
302 | lifetime: data.lifetime + '',
303 | productId: productId,
304 | price: data.price,
305 | productName: product.productName,
306 | productImage: product.image,
307 | },
308 | });
309 |
310 | data.stripePriceId = priceDetails.id;
311 | }
312 |
313 | const dataForUpdate = {};
314 | for (const key in data) {
315 | if (data.hasOwnProperty(key)) {
316 | dataForUpdate[`skuDetails.$.${key}`] = data[key];
317 | }
318 | }
319 |
320 | const result = await this.productDB.findOneAndUpdate(
321 | { _id: productId, 'skuDetails._id': skuId },
322 | { $set: dataForUpdate },
323 | );
324 |
325 | return {
326 | message: 'Product sku updated successfully',
327 | success: true,
328 | result,
329 | };
330 | } catch (error) {
331 | throw error;
332 | }
333 | }
334 |
335 | async deleteProductSkuById(id: string, skuId: string) {
336 | try {
337 | const productDetails = await this.productDB.findOne({ _id: id });
338 | const skuDetails = productDetails.skuDetails.find(
339 | (sku) => sku._id.toString() === skuId,
340 | );
341 | await this.stripeClient.prices.update(skuDetails.stripePriceId, {
342 | active: false,
343 | });
344 |
345 | // delete the sku details from product
346 | await this.productDB.deleteSku(id, skuId);
347 | // delete all the licences from db for that sku
348 | await this.productDB.deleteAllLicences(undefined, skuId);
349 |
350 | return {
351 | message: 'Product sku details deleted successfully',
352 | success: true,
353 | result: {
354 | id,
355 | skuId,
356 | },
357 | };
358 | } catch (error) {
359 | throw error;
360 | }
361 | }
362 |
363 | async addProductSkuLicense(
364 | productId: string,
365 | skuId: string,
366 | licenseKey: string,
367 | ) {
368 | try {
369 | const product = await this.productDB.findOne({ _id: productId });
370 | if (!product) {
371 | throw new Error('Product does not exist');
372 | }
373 |
374 | const sku = product.skuDetails.find((sku) => sku._id == skuId);
375 | if (!sku) {
376 | throw new Error('Sku does not exist');
377 | }
378 |
379 | const result = await this.productDB.createLicense(
380 | productId,
381 | skuId,
382 | licenseKey,
383 | );
384 |
385 | return {
386 | message: 'License key added successfully',
387 | success: true,
388 | result: result,
389 | };
390 | } catch (error) {
391 | throw error;
392 | }
393 | }
394 |
395 | async removeProductSkuLicense(id: string) {
396 | try {
397 | const result = await this.productDB.removeLicense({ _id: id });
398 |
399 | return {
400 | message: 'License key removed successfully',
401 | success: true,
402 | result: result,
403 | };
404 | } catch (error) {
405 | throw error;
406 | }
407 | }
408 |
409 | async getProductSkuLicenses(productId: string, skuId: string) {
410 | try {
411 | const product = await this.productDB.findOne({ _id: productId });
412 | if (!product) {
413 | throw new Error('Product does not exist');
414 | }
415 |
416 | const sku = product.skuDetails.find((sku) => sku._id == skuId);
417 | if (!sku) {
418 | throw new Error('Sku does not exist');
419 | }
420 |
421 | const result = await this.productDB.findLicense({
422 | product: productId,
423 | productSku: skuId,
424 | });
425 |
426 | return {
427 | message: 'Licenses fetched successfully',
428 | success: true,
429 | result: result,
430 | };
431 | } catch (error) {
432 | throw error;
433 | }
434 | }
435 |
436 | async updateProductSkuLicense(
437 | productId: string,
438 | skuId: string,
439 | licenseKeyId: string,
440 | licenseKey: string,
441 | ) {
442 | try {
443 | const product = await this.productDB.findOne({ _id: productId });
444 | if (!product) {
445 | throw new Error('Product does not exist');
446 | }
447 |
448 | const sku = product.skuDetails.find((sku) => sku._id == skuId);
449 | if (!sku) {
450 | throw new Error('Sku does not exist');
451 | }
452 |
453 | const result = await this.productDB.updateLicense(
454 | { _id: licenseKeyId },
455 | { licenseKey: licenseKey },
456 | );
457 |
458 | return {
459 | message: 'License key updated successfully',
460 | success: true,
461 | result: result,
462 | };
463 | } catch (error) {
464 | throw error;
465 | }
466 | }
467 |
468 | async addProductReview(
469 | productId: string,
470 | rating: number,
471 | review: string,
472 | user: Record,
473 | ) {
474 | try {
475 | const product = await this.productDB.findOne({ _id: productId });
476 | if (!product) {
477 | throw new Error('Product does not exist');
478 | }
479 |
480 | if (
481 | product.feedbackDetails.find(
482 | (value: { customerId: string }) =>
483 | value.customerId === user._id.toString(),
484 | )
485 | ) {
486 | throw new BadRequestException(
487 | 'You have already gave the review for this product',
488 | );
489 | }
490 |
491 | const order = await this.orderDB.findOne({
492 | customerId: user._id,
493 | 'orderedItems.productId': productId,
494 | });
495 |
496 | if (!order) {
497 | throw new BadRequestException('You have not purchased this product');
498 | }
499 |
500 | const ratings: any[] = [];
501 | product.feedbackDetails.forEach((comment: { rating: any }) =>
502 | ratings.push(comment.rating),
503 | );
504 |
505 | let avgRating = String(rating);
506 | if (ratings.length > 0) {
507 | avgRating = (ratings.reduce((a, b) => a + b) / ratings.length).toFixed(
508 | 2,
509 | );
510 | }
511 |
512 | const reviewDetails = {
513 | rating: rating,
514 | feedbackMsg: review,
515 | customerId: user._id,
516 | customerName: user.name,
517 | };
518 |
519 | const result = await this.productDB.findOneAndUpdate(
520 | { _id: productId },
521 | { $set: { avgRating }, $push: { feedbackDetails: reviewDetails } },
522 | );
523 |
524 | return {
525 | message: 'Product review added successfully',
526 | success: true,
527 | result,
528 | };
529 | } catch (error) {
530 | throw error;
531 | }
532 | }
533 |
534 | async removeProductReview(productId: string, reviewId: string) {
535 | try {
536 | const product = await this.productDB.findOne({ _id: productId });
537 | if (!product) {
538 | throw new Error('Product does not exist');
539 | }
540 |
541 | const review = product.feedbackDetails.find(
542 | (review) => review._id == reviewId,
543 | );
544 | if (!review) {
545 | throw new Error('Review does not exist');
546 | }
547 |
548 | const ratings: any[] = [];
549 | product.feedbackDetails.forEach((comment) => {
550 | if (comment._id.toString() !== reviewId) {
551 | ratings.push(comment.rating);
552 | }
553 | });
554 |
555 | let avgRating = '0';
556 | if (ratings.length > 0) {
557 | avgRating = (ratings.reduce((a, b) => a + b) / ratings.length).toFixed(
558 | 2,
559 | );
560 | }
561 |
562 | const result = await this.productDB.findOneAndUpdate(
563 | { _id: productId },
564 | { $set: { avgRating }, $pull: { feedbackDetails: { _id: reviewId } } },
565 | );
566 |
567 | return {
568 | message: 'Product review removed successfully',
569 | success: true,
570 | result,
571 | };
572 | } catch (error) {
573 | throw error;
574 | }
575 | }
576 | }
577 |
--------------------------------------------------------------------------------
/src/responseInterceptor.ts:
--------------------------------------------------------------------------------
1 | import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common';
2 | import { map, Observable } from 'rxjs';
3 |
4 | export interface Response {
5 | message: string;
6 | success: boolean;
7 | result: any;
8 | error: null;
9 | timestamps: Date;
10 | statusCode: number;
11 | }
12 |
13 | export class TransformationInterceptor
14 | implements NestInterceptor>
15 | {
16 | intercept(
17 | context: ExecutionContext,
18 | next: CallHandler,
19 | ): Observable> {
20 | const statusCode = context.switchToHttp().getResponse().statusCode;
21 | const path = context.switchToHttp().getRequest().url;
22 | return next.handle().pipe(
23 | map((data) => ({
24 | message: data.message,
25 | success: data.success,
26 | result: data.result,
27 | timestamps: new Date(),
28 | statusCode,
29 | path,
30 | error: null,
31 | })),
32 | );
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/shared/middleware/auth.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Inject,
3 | Injectable,
4 | NestMiddleware,
5 | UnauthorizedException,
6 | } from '@nestjs/common';
7 | import { NextFunction, Request, Response } from 'express';
8 | import { UserRepository } from '../repositories/user.repository';
9 | import { decodeAuthToken } from '../utility/token-generator';
10 |
11 | @Injectable()
12 | export class AuthMiddleware implements NestMiddleware {
13 | constructor(
14 | @Inject(UserRepository) private readonly userDB: UserRepository,
15 | ) {}
16 |
17 | async use(req: Request | any, res: Response, next: NextFunction) {
18 | try {
19 | console.log('AuthMiddleware', req.headers);
20 | const token = req.cookies._digi_auth_token;
21 | if (!token) {
22 | throw new UnauthorizedException('Missing auth token');
23 | }
24 | const decodedData: any = decodeAuthToken(token);
25 | const user = await this.userDB.findById(decodedData.id);
26 | if (!user) {
27 | throw new UnauthorizedException('Unauthorized');
28 | }
29 | user.password = undefined;
30 | req.user = user;
31 | next();
32 | } catch (error) {
33 | throw new UnauthorizedException(error.message);
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/shared/middleware/role.decorators.ts:
--------------------------------------------------------------------------------
1 | import { SetMetadata } from '@nestjs/common';
2 | import { userTypes } from '../schema/users';
3 |
4 | export const ROLES_KEY = 'roles';
5 | export const Roles = (...roles: userTypes[]) => SetMetadata(ROLES_KEY, roles);
6 |
--------------------------------------------------------------------------------
/src/shared/middleware/roles.guard.ts:
--------------------------------------------------------------------------------
1 | import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
2 | import { Reflector } from '@nestjs/core';
3 | import { userTypes } from '../schema/users';
4 | import { ROLES_KEY } from './role.decorators';
5 |
6 | @Injectable()
7 | export class RolesGuard implements CanActivate {
8 | constructor(private reflector: Reflector) {}
9 |
10 | async canActivate(context: ExecutionContext): Promise {
11 | const requiredRoles = this.reflector.getAllAndOverride(
12 | ROLES_KEY,
13 | [context.getHandler(), context.getClass()],
14 | );
15 | if (!requiredRoles) {
16 | return true;
17 | }
18 | const { user } = await context.switchToHttp().getRequest();
19 | return requiredRoles.some((role) => user.type?.includes(role));
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/shared/repositories/order.repository.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { InjectModel } from '@nestjs/mongoose';
3 | import { Model } from 'mongoose';
4 | import { Orders } from '../schema/orders';
5 |
6 | @Injectable()
7 | export class OrdersRepository {
8 | constructor(
9 | @InjectModel(Orders.name) private readonly orderModel: Model,
10 | ) {}
11 |
12 | async find(query: any) {
13 | const order = await this.orderModel.find(query);
14 | return order;
15 | }
16 |
17 | async findOne(query: any) {
18 | const order = await this.orderModel.findOne(query);
19 | return order;
20 | }
21 |
22 | async create(order: any) {
23 | const createdOrder = await this.orderModel.create(order);
24 | return createdOrder;
25 | }
26 |
27 | async findOneAndUpdate(query: any, update: any, options: any) {
28 | const order = await this.orderModel.findOneAndUpdate(
29 | query,
30 | update,
31 | options,
32 | );
33 | return order;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/shared/repositories/product.repository.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { InjectModel } from '@nestjs/mongoose';
3 | import { Model } from 'mongoose';
4 | import { CreateProductDto } from 'src/products/dto/create-product.dto';
5 | import { Products } from '../schema/products';
6 | import { ParsedOptions } from 'qs-to-mongo/lib/query/options-to-mongo';
7 | import { License } from '../schema/license';
8 |
9 | @Injectable()
10 | export class ProductRepository {
11 | constructor(
12 | @InjectModel(Products.name) private readonly productModel: Model,
13 | @InjectModel(License.name) private readonly licenseModel: Model,
14 | ) {}
15 |
16 | async create(product: CreateProductDto) {
17 | const createdProduct = await this.productModel.create(product);
18 | return createdProduct;
19 | }
20 |
21 | async findOne(query: any) {
22 | const product = await this.productModel.findOne(query);
23 | return product;
24 | }
25 |
26 | async findOneAndUpdate(query: any, update: any) {
27 | const product = await this.productModel.findOneAndUpdate(query, update, {
28 | new: true,
29 | });
30 | return product;
31 | }
32 |
33 | async findOneAndDelete(query: any) {
34 | const product = await this.productModel.findOneAndDelete(query);
35 | return product;
36 | }
37 |
38 | async findProductWithGroupBy() {
39 | const products = await this.productModel.aggregate([
40 | {
41 | $facet: {
42 | latestProducts: [{ $sort: { createdAt: -1 } }, { $limit: 4 }],
43 | topRatedProducts: [{ $sort: { avgRating: -1 } }, { $limit: 8 }],
44 | },
45 | },
46 | ]);
47 | return products;
48 | }
49 |
50 | async find(query: Record, options: ParsedOptions) {
51 | options.sort = options.sort || { _id: 1 };
52 | options.limit = options.limit || 12;
53 | options.skip = options.skip || 0;
54 |
55 | if (query.search) {
56 | query.productName = new RegExp(query.search, 'i');
57 | delete query.search;
58 | }
59 |
60 | const products = await this.productModel.aggregate([
61 | {
62 | $match: query,
63 | },
64 | {
65 | $sort: options.sort,
66 | },
67 | { $skip: options.skip },
68 | { $limit: options.limit },
69 | ]);
70 |
71 | const totalProductCount = await this.productModel.countDocuments(query);
72 | return { totalProductCount, products };
73 | }
74 |
75 | async findRelatedProducts(query: Record) {
76 | const products = await this.productModel.aggregate([
77 | {
78 | $match: query,
79 | },
80 | {
81 | $sample: { size: 4 },
82 | },
83 | ]);
84 | return products;
85 | }
86 |
87 | async createLicense(product: string, productSku: string, licenseKey: string) {
88 | const license = await this.licenseModel.create({
89 | product,
90 | productSku,
91 | licenseKey,
92 | });
93 | return license;
94 | }
95 |
96 | async removeLicense(query: any) {
97 | const license = await this.licenseModel.findOneAndDelete(query);
98 | return license;
99 | }
100 |
101 | async findLicense(query: any, limit?: number) {
102 | if (limit && limit > 0) {
103 | const license = await this.licenseModel.find(query).limit(limit);
104 | return license;
105 | }
106 | const license = await this.licenseModel.find(query);
107 | return license;
108 | }
109 |
110 | async updateLicense(query: any, update: any) {
111 | const license = await this.licenseModel.findOneAndUpdate(query, update, {
112 | new: true,
113 | });
114 | return license;
115 | }
116 |
117 | async updateLicenseMany(query: any, data: any) {
118 | const license = await this.licenseModel.updateMany(query, data);
119 | return license;
120 | }
121 |
122 | async deleteSku(id: string, skuId: string) {
123 | return await this.productModel.updateOne(
124 | { _id: id },
125 | {
126 | $pull: {
127 | skuDetails: { _id: skuId },
128 | },
129 | },
130 | );
131 | }
132 |
133 | async deleteAllLicences(productId: string, skuId: string) {
134 | if (productId)
135 | return await this.licenseModel.deleteMany({ product: productId });
136 | return await this.licenseModel.deleteMany({ productSku: skuId });
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/src/shared/repositories/user.repository.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { InjectModel } from '@nestjs/mongoose';
3 | import { Model } from 'mongoose';
4 | import { Users } from '../schema/users';
5 |
6 | @Injectable()
7 | export class UserRepository {
8 | constructor(
9 | @InjectModel(Users.name) private readonly userModel: Model,
10 | ) {}
11 |
12 | async findOne(query: any) {
13 | return await this.userModel.findOne(query);
14 | }
15 |
16 | async find(query: any) {
17 | return await this.userModel.find(query);
18 | }
19 |
20 | async create(data: Record) {
21 | return await this.userModel.create(data);
22 | }
23 |
24 | async updateOne(query: any, data: Record) {
25 | return await this.userModel.updateOne(query, data);
26 | }
27 |
28 | async findById(id: string) {
29 | return await this.userModel.findById(id);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/shared/schema/license.ts:
--------------------------------------------------------------------------------
1 | import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
2 | import mongoose from 'mongoose';
3 |
4 | @Schema({
5 | timestamps: {
6 | createdAt: 'createdAt',
7 | updatedAt: 'updatedAt',
8 | },
9 | })
10 | export class License {
11 | @Prop({
12 | required: true,
13 | type: mongoose.Schema.Types.ObjectId,
14 | ref: 'Products',
15 | })
16 | product: string;
17 |
18 | @Prop({
19 | required: true,
20 | type: String,
21 | })
22 | productSku: string;
23 | @Prop({
24 | required: true,
25 | type: String,
26 | })
27 | licenseKey: string;
28 | @Prop({
29 | default: false,
30 | type: Boolean,
31 | })
32 | isSold: boolean;
33 |
34 | @Prop({
35 | default: '',
36 | })
37 | orderId: string;
38 | }
39 |
40 | export const LicenseSchema = SchemaFactory.createForClass(License);
41 |
42 |
--------------------------------------------------------------------------------
/src/shared/schema/orders.ts:
--------------------------------------------------------------------------------
1 | import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
2 | import mongoose from 'mongoose';
3 | import { Users } from './users';
4 |
5 | export enum paymentStatus {
6 | pending = 'pending',
7 | paid = 'paid',
8 | failed = 'failed',
9 | }
10 |
11 | export enum orderStatus {
12 | pending = 'pending',
13 | completed = 'completed',
14 | }
15 |
16 | export class OrderedItems {
17 | @Prop({ required: true })
18 | productId: string;
19 |
20 | @Prop({ required: true })
21 | quantity: number;
22 |
23 | @Prop({ required: true })
24 | skuCode: string;
25 |
26 | @Prop({ required: true })
27 | price: number;
28 |
29 | @Prop({ required: true })
30 | lifetime: boolean;
31 |
32 | @Prop({ required: true })
33 | validity: number;
34 |
35 | @Prop({ required: true })
36 | skuPriceId: string;
37 |
38 | @Prop({ required: true })
39 | productName: string;
40 |
41 | @Prop({ default: [] })
42 | licenses: string[];
43 | }
44 |
45 | @Schema({ timestamps: true })
46 | export class Orders {
47 | @Prop({ required: true })
48 | orderId: string;
49 |
50 | @Prop({ required: true })
51 | userId: string;
52 |
53 | @Prop({ required: true, type: Object })
54 | customerAddress: {
55 | line1: string;
56 | line2: string;
57 | city: string;
58 | state: string;
59 | country: string;
60 | postal_code: string;
61 | };
62 |
63 | @Prop({ required: true })
64 | customerPhoneNumber: string;
65 |
66 | @Prop({ required: true })
67 | orderedItems: OrderedItems[];
68 |
69 | @Prop({ required: true, type: Object })
70 | paymentInfo: {
71 | paymentMethod: string;
72 | paymentStatus: paymentStatus;
73 | paymentAmount: number;
74 | paymentDate: Date;
75 | paymentIntentId: string;
76 | };
77 |
78 | @Prop({ default: orderStatus.pending })
79 | orderStatus: orderStatus;
80 |
81 | @Prop({ default: false })
82 | isOrderDelivered: boolean;
83 |
84 | @Prop({ default: null })
85 | checkoutSessionId: string;
86 | }
87 | export const OrderSchema = SchemaFactory.createForClass(Orders);
88 |
--------------------------------------------------------------------------------
/src/shared/schema/products.ts:
--------------------------------------------------------------------------------
1 | import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
2 | import mongoose from 'mongoose';
3 |
4 | export enum categoryType {
5 | operatingSystem = 'Operating System',
6 | applicationSoftware = 'Application Software',
7 | }
8 |
9 | export enum platformType {
10 | windows = 'Windows',
11 | mac = 'Mac',
12 | linux = 'Linux',
13 | android = 'Android',
14 | ios = 'iOS',
15 | }
16 |
17 | export enum baseType {
18 | computer = 'Computer',
19 | mobile = 'Mobile',
20 | }
21 |
22 | @Schema({ timestamps: true })
23 | export class Feedbackers extends mongoose.Document {
24 | @Prop({})
25 | customerId: string;
26 |
27 | @Prop({})
28 | customerName: string;
29 |
30 | @Prop({})
31 | rating: number;
32 |
33 | @Prop({})
34 | feedbackMsg: string;
35 | }
36 |
37 | export const FeedbackSchema = SchemaFactory.createForClass(Feedbackers);
38 |
39 | @Schema({ timestamps: true })
40 | export class SkuDetails extends mongoose.Document {
41 | @Prop({})
42 | skuName: string;
43 |
44 | @Prop({})
45 | price: number;
46 |
47 | @Prop({})
48 | validity: number; // in days
49 |
50 | @Prop({})
51 | lifetime: boolean;
52 |
53 | @Prop({})
54 | stripePriceId: string;
55 |
56 | @Prop({})
57 | skuCode?: string;
58 | }
59 |
60 | export const skuDetailsSchema = SchemaFactory.createForClass(SkuDetails);
61 |
62 | @Schema({ timestamps: true })
63 | export class Products {
64 | @Prop({ required: true })
65 | productName: string;
66 |
67 | @Prop({ required: true })
68 | description: string;
69 |
70 | @Prop({
71 | default:
72 | 'https://us.123rf.com/450wm/pavelstasevich/pavelstasevich1811/pavelstasevich181101027/112815900-no-image-available-icon-flat-vector.jpg?ver=6',
73 | })
74 | image?: string;
75 |
76 | @Prop({
77 | required: true,
78 | enum: [categoryType.applicationSoftware, categoryType.operatingSystem],
79 | })
80 | category: string;
81 |
82 | @Prop({
83 | required: true,
84 | enum: [
85 | platformType.android,
86 | platformType.ios,
87 | platformType.linux,
88 | platformType.mac,
89 | platformType.windows,
90 | ],
91 | })
92 | platformType: string;
93 |
94 | @Prop({ required: true, enum: [baseType.computer, baseType.mobile] })
95 | baseType: string;
96 |
97 | @Prop({ required: true })
98 | productUrl: string;
99 |
100 | @Prop({ required: true })
101 | downloadUrl: string;
102 |
103 | @Prop({})
104 | avgRating: number;
105 |
106 | @Prop([{ type: FeedbackSchema }])
107 | feedbackDetails: Feedbackers[];
108 |
109 | @Prop([{ type: skuDetailsSchema }])
110 | skuDetails: SkuDetails[];
111 |
112 | @Prop({ type: Object })
113 | imageDetails: Record;
114 |
115 | @Prop({})
116 | requirementSpecification: Record[];
117 |
118 | @Prop({})
119 | highlights: string[];
120 |
121 | @Prop({})
122 | stripeProductId: string;
123 | }
124 |
125 | export const ProductSchema = SchemaFactory.createForClass(Products);
126 |
--------------------------------------------------------------------------------
/src/shared/schema/users.ts:
--------------------------------------------------------------------------------
1 | import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
2 | import { Document } from 'mongoose';
3 |
4 | export enum userTypes {
5 | ADMIN = 'admin',
6 | CUSTOMER = 'customer',
7 | }
8 |
9 | @Schema({
10 | timestamps: true,
11 | })
12 | export class Users extends Document {
13 | @Prop({ required: true })
14 | name: string;
15 |
16 | @Prop({ required: true })
17 | email: string;
18 |
19 | @Prop({ required: true })
20 | password: string;
21 |
22 | @Prop({ required: true, enum: [userTypes.ADMIN, userTypes.CUSTOMER] })
23 | type: string;
24 |
25 | @Prop({ default: false })
26 | isVerified: boolean;
27 |
28 | @Prop({ default: null })
29 | otp: string;
30 |
31 | @Prop({ default: null })
32 | otpExpiryTime: Date;
33 | }
34 |
35 | export const UserSchema = SchemaFactory.createForClass(Users);
36 |
--------------------------------------------------------------------------------
/src/shared/utility/mail-handler.ts:
--------------------------------------------------------------------------------
1 | import FormData from 'form-data';
2 | import config from 'config';
3 | import axios from 'axios';
4 |
5 | export const sendEmail = async (
6 | to: string,
7 | templateName: string,
8 | subject: string,
9 | templateVars: Record = {},
10 | ) => {
11 | try {
12 | const form = new FormData();
13 | form.append('to', to);
14 | form.append('template', templateName);
15 | form.append('subject', subject);
16 | form.append(
17 | 'from',
18 | 'mailgun@sandbox4ea7cfe84fde4fbcb3bf6b9157156213.mailgun.org',
19 | );
20 | Object.keys(templateVars).forEach((key) => {
21 | form.append(`v:${key}`, templateVars[key]);
22 | });
23 |
24 | const username = 'api';
25 | const password = config.get('emailService.privateApiKey');
26 | const token = Buffer.from(`${username}:${password}`).toString('base64');
27 |
28 | const response = await axios({
29 | method: 'post',
30 | url: `https://api.mailgun.net/v3/${config.get(
31 | 'emailService.testDomain',
32 | )}/messages`,
33 | headers: {
34 | Authorization: `Basic ${token}`,
35 | contentType: 'multipart/form-data',
36 | },
37 | data: form,
38 | });
39 | return response;
40 | } catch (error) {
41 | console.error(error);
42 | }
43 | };
44 |
--------------------------------------------------------------------------------
/src/shared/utility/password-manager.ts:
--------------------------------------------------------------------------------
1 | import bcrypt from 'bcrypt';
2 |
3 | export const generateHashPassword = async (password: string) => {
4 | const salt = await bcrypt.genSalt(10);
5 | return await bcrypt.hash(password, salt);
6 | };
7 |
8 | export const comparePassword = async (
9 | password: string,
10 | hashPassword: string,
11 | ) => {
12 | return await bcrypt.compare(password, hashPassword);
13 | };
14 |
--------------------------------------------------------------------------------
/src/shared/utility/token-generator.ts:
--------------------------------------------------------------------------------
1 | import jwt from 'jsonwebtoken';
2 | import config from 'config';
3 |
4 | export const generateAuthToken = (id: string) => {
5 | return jwt.sign({ id }, config.get('jwtSecret'), {
6 | expiresIn: '30d',
7 | });
8 | };
9 |
10 | export const decodeAuthToken = (token: string) => {
11 | return jwt.verify(token, config.get('jwtSecret'));
12 | };
13 |
--------------------------------------------------------------------------------
/src/users/dto/create-user.dto.ts:
--------------------------------------------------------------------------------
1 | import { IsIn, IsNotEmpty, IsOptional, IsString } from 'class-validator';
2 | import { userTypes } from 'src/shared/schema/users';
3 |
4 | export class CreateUserDto {
5 | @IsNotEmpty()
6 | @IsString()
7 | name: string;
8 |
9 | @IsNotEmpty()
10 | @IsString()
11 | email: string;
12 |
13 | @IsNotEmpty()
14 | @IsString()
15 | password: string;
16 |
17 | @IsNotEmpty()
18 | @IsString()
19 | @IsIn([userTypes.ADMIN, userTypes.CUSTOMER])
20 | type: string;
21 |
22 | @IsString()
23 | @IsOptional()
24 | secretToken?: string;
25 |
26 | isVerified?: boolean;
27 | }
28 |
--------------------------------------------------------------------------------
/src/users/dto/update-user.dto.ts:
--------------------------------------------------------------------------------
1 | import { PartialType } from '@nestjs/mapped-types';
2 | import { CreateUserDto } from './create-user.dto';
3 |
4 | export class UpdateUserDto {
5 | name?: string;
6 | oldPassword?: string;
7 | newPassword?: string;
8 | }
9 |
--------------------------------------------------------------------------------
/src/users/users.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { UsersController } from './users.controller';
3 | import { UsersService } from './users.service';
4 |
5 | describe('UsersController', () => {
6 | let controller: UsersController;
7 |
8 | beforeEach(async () => {
9 | const module: TestingModule = await Test.createTestingModule({
10 | controllers: [UsersController],
11 | providers: [UsersService],
12 | }).compile();
13 |
14 | controller = module.get(UsersController);
15 | });
16 |
17 | it('should be defined', () => {
18 | expect(controller).toBeDefined();
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/src/users/users.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Controller,
3 | Get,
4 | Post,
5 | Body,
6 | Patch,
7 | Param,
8 | Delete,
9 | HttpCode,
10 | HttpStatus,
11 | Res,
12 | Put,
13 | Query,
14 | } from '@nestjs/common';
15 | import { UsersService } from './users.service';
16 | import { CreateUserDto } from './dto/create-user.dto';
17 | import { UpdateUserDto } from './dto/update-user.dto';
18 | import { Response } from 'express';
19 | import { Roles } from 'src/shared/middleware/role.decorators';
20 | import { userTypes } from 'src/shared/schema/users';
21 |
22 | @Controller('users')
23 | export class UsersController {
24 | constructor(private readonly usersService: UsersService) {}
25 |
26 | @Post()
27 | async create(@Body() createUserDto: CreateUserDto) {
28 | return this.usersService.create(createUserDto);
29 | }
30 |
31 | @Post('/login')
32 | @HttpCode(HttpStatus.OK)
33 | async login(
34 | @Body() loginUser: { email: string; password: string },
35 | @Res({ passthrough: true }) response: Response,
36 | ) {
37 | const loginRes = await this.usersService.login(
38 | loginUser.email,
39 | loginUser.password,
40 | );
41 | if (loginRes.success) {
42 | response.cookie('_digi_auth_token', loginRes.result?.token, {
43 | httpOnly: true,
44 | });
45 | }
46 | delete loginRes.result?.token;
47 | return loginRes;
48 | }
49 |
50 | @Get('/verify-email/:otp/:email')
51 | async verifyEmail(@Param('otp') otp: string, @Param('email') email: string) {
52 | return await this.usersService.verifyEmail(otp, email);
53 | }
54 |
55 | @Get('send-otp-email/:email')
56 | async sendOtpEmail(@Param('email') email: string) {
57 | return await this.usersService.sendOtpEmail(email);
58 | }
59 |
60 | @Put('/logout')
61 | async logout(@Res() res: Response) {
62 | res.clearCookie('_digi_auth_token');
63 | return res.status(HttpStatus.OK).json({
64 | success: true,
65 | message: 'Logout successfully',
66 | });
67 | }
68 |
69 | @Get('forgot-password/:email')
70 | async forgotPassword(@Param('email') email: string) {
71 | return await this.usersService.forgotPassword(email);
72 | }
73 |
74 | @Get()
75 | @Roles(userTypes.ADMIN)
76 | async findAll(@Query('type') type: string) {
77 | return await this.usersService.findAll(type);
78 | }
79 |
80 | @Patch('/update-name-password/:id')
81 | update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
82 | return this.usersService.updatePasswordOrName(id, updateUserDto);
83 | }
84 |
85 | @Delete(':id')
86 | remove(@Param('id') id: string) {
87 | return this.usersService.remove(+id);
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/users/users.module.ts:
--------------------------------------------------------------------------------
1 | import {
2 | MiddlewareConsumer,
3 | Module,
4 | NestModule,
5 | RequestMethod,
6 | } from '@nestjs/common';
7 | import { UsersService } from './users.service';
8 | import { UsersController } from './users.controller';
9 | import { UserRepository } from 'src/shared/repositories/user.repository';
10 | import { MongooseModule } from '@nestjs/mongoose';
11 | import { Users, UserSchema } from 'src/shared/schema/users';
12 | import { APP_GUARD } from '@nestjs/core';
13 | import { RolesGuard } from 'src/shared/middleware/roles.guard';
14 | import { AuthMiddleware } from 'src/shared/middleware/auth';
15 |
16 | @Module({
17 | controllers: [UsersController],
18 | providers: [
19 | UsersService,
20 | UserRepository,
21 | {
22 | provide: APP_GUARD,
23 | useClass: RolesGuard,
24 | },
25 | ],
26 | imports: [
27 | MongooseModule.forFeature([
28 | {
29 | name: Users.name,
30 | schema: UserSchema,
31 | },
32 | ]),
33 | ],
34 | })
35 | export class UsersModule implements NestModule {
36 | configure(consumer: MiddlewareConsumer) {
37 | consumer
38 | .apply(AuthMiddleware)
39 | .forRoutes({ path: '/users', method: RequestMethod.GET });
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/users/users.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { UsersService } from './users.service';
3 |
4 | describe('UsersService', () => {
5 | let service: UsersService;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [UsersService],
10 | }).compile();
11 |
12 | service = module.get(UsersService);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(service).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/users/users.service.ts:
--------------------------------------------------------------------------------
1 | import { Inject, Injectable } from '@nestjs/common';
2 | import { userTypes } from 'src/shared/schema/users';
3 | import { CreateUserDto } from './dto/create-user.dto';
4 | import { UpdateUserDto } from './dto/update-user.dto';
5 | import config from 'config';
6 | import { UserRepository } from 'src/shared/repositories/user.repository';
7 | import {
8 | comparePassword,
9 | generateHashPassword,
10 | } from 'src/shared/utility/password-manager';
11 | import { sendEmail } from 'src/shared/utility/mail-handler';
12 | import { generateAuthToken } from 'src/shared/utility/token-generator';
13 | @Injectable()
14 | export class UsersService {
15 | constructor(
16 | @Inject(UserRepository) private readonly userDB: UserRepository,
17 | ) {}
18 | async create(createUserDto: CreateUserDto) {
19 | try {
20 | // generate the hash password
21 | createUserDto.password = await generateHashPassword(
22 | createUserDto.password,
23 | );
24 |
25 | /// check is it for admin
26 | if (
27 | createUserDto.type === userTypes.ADMIN &&
28 | createUserDto.secretToken !== config.get('adminSecretToken')
29 | ) {
30 | throw new Error('Not allowed to create admin');
31 | } else if (createUserDto.type !== userTypes.CUSTOMER) {
32 | createUserDto.isVerified = true;
33 | }
34 |
35 | // user is already exist
36 | const user = await this.userDB.findOne({
37 | email: createUserDto.email,
38 | });
39 | if (user) {
40 | throw new Error('User already exist');
41 | }
42 |
43 | // generate the otp
44 | const otp = Math.floor(Math.random() * 900000) + 100000;
45 |
46 | const otpExpiryTime = new Date();
47 | otpExpiryTime.setMinutes(otpExpiryTime.getMinutes() + 10);
48 |
49 | const newUser = await this.userDB.create({
50 | ...createUserDto,
51 | otp,
52 | otpExpiryTime,
53 | });
54 | if (newUser.type !== userTypes.ADMIN) {
55 | sendEmail(
56 | newUser.email,
57 | config.get('emailService.emailTemplates.verifyEmail'),
58 | 'Email verification - Digizone',
59 | {
60 | customerName: newUser.name,
61 | customerEmail: newUser.email,
62 | otp,
63 | },
64 | );
65 | }
66 | return {
67 | success: true,
68 | message:
69 | newUser.type === userTypes.ADMIN
70 | ? 'Admin created successfully'
71 | : 'Please activate your account by verifying your email. We have sent you a wmail with the otp',
72 | result: { email: newUser.email },
73 | };
74 | } catch (error) {
75 | throw error;
76 | }
77 | }
78 |
79 | async login(email: string, password: string) {
80 | try {
81 | const userExists = await this.userDB.findOne({
82 | email,
83 | });
84 | if (!userExists) {
85 | throw new Error('Invalid email or password');
86 | }
87 | if (!userExists.isVerified) {
88 | throw new Error('Please verify your email');
89 | }
90 | const isPasswordMatch = await comparePassword(
91 | password,
92 | userExists.password,
93 | );
94 | if (!isPasswordMatch) {
95 | throw new Error('Invalid email or password');
96 | }
97 | const token = await generateAuthToken(userExists._id);
98 |
99 | return {
100 | success: true,
101 | message: 'Login successful',
102 | result: {
103 | user: {
104 | name: userExists.name,
105 | email: userExists.email,
106 | type: userExists.type,
107 | id: userExists._id.toString(),
108 | },
109 | token,
110 | },
111 | };
112 | } catch (error) {
113 | throw error;
114 | }
115 | }
116 |
117 | async verifyEmail(otp: string, email: string) {
118 | try {
119 | const user = await this.userDB.findOne({
120 | email,
121 | });
122 | if (!user) {
123 | throw new Error('User not found');
124 | }
125 | if (user.otp !== otp) {
126 | throw new Error('Invalid otp');
127 | }
128 | if (user.otpExpiryTime < new Date()) {
129 | throw new Error('Otp expired');
130 | }
131 | await this.userDB.updateOne(
132 | {
133 | email,
134 | },
135 | {
136 | isVerified: true,
137 | },
138 | );
139 |
140 | return {
141 | success: true,
142 | message: 'Email verified successfully. you can login now',
143 | };
144 | } catch (error) {
145 | throw error;
146 | }
147 | }
148 |
149 | async sendOtpEmail(email: string) {
150 | try {
151 | const user = await this.userDB.findOne({
152 | email,
153 | });
154 | if (!user) {
155 | throw new Error('User not found');
156 | }
157 | if (user.isVerified) {
158 | throw new Error('Email already verified');
159 | }
160 | const otp = Math.floor(Math.random() * 900000) + 100000;
161 |
162 | const otpExpiryTime = new Date();
163 | otpExpiryTime.setMinutes(otpExpiryTime.getMinutes() + 10);
164 |
165 | await this.userDB.updateOne(
166 | {
167 | email,
168 | },
169 | {
170 | otp,
171 | otpExpiryTime,
172 | },
173 | );
174 |
175 | sendEmail(
176 | user.email,
177 | config.get('emailService.emailTemplates.verifyEmail'),
178 | 'Email verification - Digizone',
179 | {
180 | customerName: user.name,
181 | customerEmail: user.email,
182 | otp,
183 | },
184 | );
185 |
186 | return {
187 | success: true,
188 | message: 'Otp sent successfully',
189 | result: { email: user.email },
190 | };
191 | } catch (error) {
192 | throw error;
193 | }
194 | }
195 |
196 | async forgotPassword(email: string) {
197 | try {
198 | const user = await this.userDB.findOne({
199 | email,
200 | });
201 | if (!user) {
202 | throw new Error('User not found');
203 | }
204 | let password = Math.random().toString(36).substring(2, 12);
205 | const tempPassword = password;
206 | password = await generateHashPassword(password);
207 | await this.userDB.updateOne(
208 | {
209 | _id: user._id,
210 | },
211 | {
212 | password,
213 | },
214 | );
215 |
216 | sendEmail(
217 | user.email,
218 | config.get('emailService.emailTemplates.forgotPassword'),
219 | 'Forgot password - Digizone',
220 | {
221 | customerName: user.name,
222 | customerEmail: user.email,
223 | newPassword: password,
224 | loginLink: config.get('loginLink'),
225 | },
226 | );
227 |
228 | return {
229 | success: true,
230 | message: 'Password sent to your email',
231 | result: { email: user.email, password: tempPassword },
232 | };
233 | } catch (error) {
234 | throw error;
235 | }
236 | }
237 |
238 | async findAll(type: string) {
239 | try {
240 | const users = await this.userDB.find({
241 | type,
242 | });
243 | return {
244 | success: true,
245 | message: 'Users fetched successfully',
246 | result: users,
247 | };
248 | } catch (error) {
249 | throw error;
250 | }
251 | }
252 |
253 | async updatePasswordOrName(
254 | id: string,
255 | updatePasswordOrNameDto: UpdateUserDto,
256 | ) {
257 | try {
258 | const { oldPassword, newPassword, name } = updatePasswordOrNameDto;
259 | if (!name && !newPassword) {
260 | throw new Error('Please provide name or password');
261 | }
262 | const user = await this.userDB.findOne({
263 | _id: id,
264 | });
265 | if (!user) {
266 | throw new Error('User not found');
267 | }
268 | if (newPassword) {
269 | const isPasswordMatch = await comparePassword(
270 | oldPassword,
271 | user.password,
272 | );
273 | if (!isPasswordMatch) {
274 | throw new Error('Invalid current password');
275 | }
276 | const password = await generateHashPassword(newPassword);
277 | await this.userDB.updateOne(
278 | {
279 | _id: id,
280 | },
281 | {
282 | password,
283 | },
284 | );
285 | }
286 | if (name) {
287 | await this.userDB.updateOne(
288 | {
289 | _id: id,
290 | },
291 | {
292 | name,
293 | },
294 | );
295 | }
296 | return {
297 | success: true,
298 | message: 'User updated successfully',
299 | result: {
300 | name: user.name,
301 | email: user.email,
302 | type: user.type,
303 | id: user._id.toString(),
304 | },
305 | };
306 | } catch (error) {
307 | throw error;
308 | }
309 | }
310 |
311 | remove(id: number) {
312 | return `This action removes a #${id} user`;
313 | }
314 | }
315 |
--------------------------------------------------------------------------------
/test/app.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { INestApplication } from '@nestjs/common';
3 | import * as request from 'supertest';
4 | import { AppModule } from '../src/app.module';
5 |
6 | describe('AppController (e2e)', () => {
7 | let app: INestApplication;
8 |
9 | beforeEach(async () => {
10 | const moduleFixture: TestingModule = await Test.createTestingModule({
11 | imports: [AppModule],
12 | }).compile();
13 |
14 | app = moduleFixture.createNestApplication();
15 | await app.init();
16 | });
17 |
18 | it('/ (GET)', () => {
19 | return request(app.getHttpServer())
20 | .get('/')
21 | .expect(200)
22 | .expect('Hello World!');
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/test/jest-e2e.json:
--------------------------------------------------------------------------------
1 | {
2 | "moduleFileExtensions": ["js", "json", "ts"],
3 | "rootDir": ".",
4 | "testEnvironment": "node",
5 | "testRegex": ".e2e-spec.ts$",
6 | "transform": {
7 | "^.+\\.(t|j)s$": "ts-jest"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "declaration": true,
5 | "removeComments": true,
6 | "emitDecoratorMetadata": true,
7 | "experimentalDecorators": true,
8 | "allowSyntheticDefaultImports": true,
9 | "target": "es2017",
10 | "sourceMap": true,
11 | "outDir": "./dist",
12 | "baseUrl": "./",
13 | "incremental": true,
14 | "skipLibCheck": true,
15 | "strictNullChecks": false,
16 | "noImplicitAny": false,
17 | "strictBindCallApply": false,
18 | "forceConsistentCasingInFileNames": false,
19 | "noFallthroughCasesInSwitch": false,
20 | "esModuleInterop": true
21 | }
22 | }
23 |
--------------------------------------------------------------------------------