├── .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 | Nest Logo 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 | NPM Version 11 | Package License 12 | NPM Downloads 13 | CircleCI 14 | Coverage 15 | Discord 16 | Backers on Open Collective 17 | Sponsors on Open Collective 18 | 19 | Support us 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 | --------------------------------------------------------------------------------