├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── README.md ├── index.html ├── nest-cli.json ├── package.json ├── src ├── app.controller.spec.ts ├── app.controller.ts ├── app.create.ts ├── app.endpoints.http ├── app.module.ts ├── app.service.ts ├── auth │ ├── auth.controller.ts │ ├── auth.module.ts │ ├── config │ │ └── jwt.config.ts │ ├── constants │ │ └── auth.constants.ts │ ├── decorators │ │ ├── active-user.decorator.ts │ │ └── auth.decorator.ts │ ├── dtos │ │ ├── refresh-token.dto.ts │ │ └── signin.dto.ts │ ├── enums │ │ └── auth-type.enum.ts │ ├── guards │ │ ├── access-token │ │ │ └── access-token.guard.ts │ │ └── authentication │ │ │ └── authentication.guard.ts │ ├── http │ │ ├── refresh-tokens.endpoints.http │ │ └── signin.endpoints.http │ ├── interfaces │ │ └── active-user-data.interface.ts │ ├── providers │ │ ├── auth.service.ts │ │ ├── bcrypt.provider.ts │ │ ├── generate-tokens.provider.ts │ │ ├── hashing.provider.ts │ │ ├── refresh-tokens.provider.ts │ │ └── sign-in.provider.ts │ └── social │ │ ├── dtos │ │ └── google-token.dto.ts │ │ ├── google-authentication.controller.ts │ │ └── providers │ │ └── google-authentication.service.ts ├── common │ ├── interceptors │ │ └── data-response │ │ │ └── data-response.interceptor.ts │ └── pagination │ │ ├── dtos │ │ └── pagination-query.dto.ts │ │ ├── interfaces │ │ └── paginated.interface.ts │ │ ├── pagination.module.ts │ │ └── providers │ │ └── pagination.provider.ts ├── config │ ├── app.config.ts │ ├── database.config.ts │ └── enviroment.validation.ts ├── mail │ ├── mail.module.ts │ ├── providers │ │ └── mail.service.ts │ └── templates │ │ └── welcome.ejs ├── main.ts ├── meta-options │ ├── dtos │ │ └── create-post-meta-options.dto.ts │ ├── http │ │ └── meta-options.post.endpoints.http │ ├── meta-option.entity.ts │ ├── meta-options.controller.ts │ ├── meta-options.module.ts │ └── providers │ │ └── meta-options.service.ts ├── posts │ ├── dtos │ │ ├── create-post-meta-options.dto.ts │ │ ├── create-post.dto.ts │ │ ├── get-post.dto.ts │ │ └── patch-post.dto.ts │ ├── enums │ │ ├── post-status.enum.ts │ │ ├── post-type.enum.ts │ │ ├── postStatus.enum.ts │ │ └── postType.enum.ts │ ├── http │ │ ├── posts.delete.endpoints.http │ │ ├── posts.get.endpoints.http │ │ ├── posts.patch.endpoints.http │ │ └── posts.post.endpoints.http │ ├── post.entity.ts │ ├── posts.controller.ts │ ├── posts.module.ts │ └── providers │ │ ├── create-post.provider.ts │ │ └── posts.service.ts ├── tags │ ├── dtos │ │ └── create-tag.dto.ts │ ├── http │ │ ├── tags.delete.endpoints.http │ │ └── tags.post.endpoints.http │ ├── providers │ │ └── tags.service.ts │ ├── tag.entity.ts │ ├── tags.controller.ts │ └── tags.module.ts ├── uploads │ ├── enums │ │ └── file-types.enum.ts │ ├── http │ │ ├── test-image.jpeg │ │ └── uploads.post.endpoints.http │ ├── interfaces │ │ └── upload-file.interface.ts │ ├── providers │ │ ├── upload-to-aws.provider.ts │ │ └── uploads.service.ts │ ├── upload.entity.ts │ ├── uploads.controller.ts │ └── uploads.module.ts └── users │ ├── config │ └── profile.config.ts │ ├── dtos │ ├── create-many-users.dto.ts │ ├── create-user.dto.ts │ ├── get-users-param.dto.ts │ └── patch-user.dto.ts │ ├── http │ ├── users.get.endpoints.http │ ├── users.patch.endpoints.http │ └── users.post.enpoints.http │ ├── interfaces │ └── google-user.inerface.ts │ ├── providers │ ├── create-google-user.provider.ts │ ├── create-user.provider.spec.ts │ ├── create-user.provider.ts │ ├── find-one-by-google-id.provider.ts │ ├── find-one-user-by-email.provider.ts │ ├── users-create-many.provider.ts │ ├── users.service.spec.ts │ └── users.service.ts │ ├── user.entity.ts │ ├── users.controller.ts │ └── users.module.ts ├── test ├── app.e2e-spec.ts ├── helpers │ ├── bootstrap-nest-application.helper.ts │ └── drop-database.helper.ts ├── jest-e2e.json └── users │ ├── users.post.e2e-spec.sample-data.ts │ └── users.post.e2e-spec.ts ├── tsconfig.build.json ├── tsconfig.json └── typeorm-cli.sample.config.ts /.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 | /build 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | pnpm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | lerna-debug.log* 14 | 15 | # OS 16 | .DS_Store 17 | 18 | # Tests 19 | /coverage 20 | /.nyc_output 21 | 22 | # IDEs and editors 23 | /.idea 24 | .project 25 | .classpath 26 | .c9/ 27 | *.launch 28 | .settings/ 29 | *.sublime-workspace 30 | 31 | # IDE - VSCode 32 | .vscode/* 33 | !.vscode/settings.json 34 | !.vscode/tasks.json 35 | !.vscode/launch.json 36 | !.vscode/extensions.json 37 | 38 | # dotenv environment variable files 39 | .env 40 | .env.development.local 41 | .env.test.local 42 | .env.production.local 43 | .env.local 44 | .env.development 45 | .env.test 46 | .env.production 47 | 48 | # temp directory 49 | .temp 50 | .tmp 51 | 52 | # Runtime data 53 | pids 54 | *.pid 55 | *.seed 56 | *.pid.lock 57 | 58 | # Diagnostic reports (https://nodejs.org/api/report.html) 59 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 60 | 61 | ## ignoring documentation 62 | /documentation 63 | 64 | ## ignoring .vscode 65 | /.vscode 66 | package-lock.json 67 | typeorm-cli.config.ts -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "httpyac.responseViewMode": "open", 3 | "workbench.colorTheme": "Material Theme Darker High Contrast", 4 | "workbench.preferredDarkColorTheme": "Material Theme Darker High Contrast", 5 | "editor.fontSize": 16, 6 | "editor.tabSize": 2, 7 | "window.zoomLevel": 1.3, 8 | "javascript.preferences.importModuleSpecifier": "shortest", 9 | "typescript.preferences.importModuleSpecifier": "shortest", 10 | "typescript.preferences.importModuleSpecifierEnding": "auto", 11 | "javascript.preferences.importModuleSpecifierEnding": "auto" 12 | } 13 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Local index - HTTrack Website Copier 8 | 9 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 |
HTTrack Website Copier - Open Source offline browser
86 | 87 | 88 | 131 | 132 |
89 | 90 | 91 | 128 | 129 |
92 | 93 | 94 | 125 | 126 |
95 | 96 | 97 | 98 | Local index - HTTrack 99 | 100 | 101 |

Index of locally available sites:

102 | 103 | 104 | 110 | 111 |
105 | · 106 | 107 | learnworlds | Stoplight 108 | 109 |
112 |
113 |
114 |
115 |
116 | Mirror and index made by HTTrack Website Copier [XR&CO'2008] 117 |
118 | 119 | 120 | 121 | 122 | 123 | 124 |
127 |
130 |
133 | 134 | 135 | 136 | 137 | 138 |
139 | 140 | 141 | 142 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true, 7 | "assets": [ 8 | { 9 | "include": "./mail/templates", 10 | "outDir": "dist/", 11 | "watchAssets": true 12 | } 13 | ], 14 | "watchAssets": true 15 | } 16 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nestjs-intro", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "build": "nest build", 10 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 11 | "start": "nest start", 12 | "start:dev": "NODE_ENV=development nest start --watch", 13 | "start:debug": "nest start --debug --watch", 14 | "start:prod": "node dist/main", 15 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 16 | "test": "jest", 17 | "test:verbose": "jest --verbose", 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 | "doc": "npx @compodoc/compodoc -p tsconfig.json -s --port 3001 --watch -d ./documentation", 23 | "pm2:start": "pm2 start npm --name nest-blog –- start" 24 | }, 25 | "dependencies": { 26 | "@compodoc/compodoc": "^1.1.23", 27 | "@nestjs-modules/mailer": "^2.0.2", 28 | "@nestjs/common": "^10.0.0", 29 | "@nestjs/config": "^3.2.2", 30 | "@nestjs/core": "^10.0.0", 31 | "@nestjs/jwt": "^10.2.0", 32 | "@nestjs/mapped-types": "^2.0.5", 33 | "@nestjs/platform-express": "^10.0.0", 34 | "@nestjs/swagger": "^7.3.0", 35 | "@nestjs/typeorm": "^10.0.2", 36 | "aws-sdk": "^2.1643.0", 37 | "bcrypt": "^5.1.1", 38 | "class-transformer": "^0.5.1", 39 | "class-validator": "^0.14.1", 40 | "ejs": "^3.1.10", 41 | "google-auth-library": "^9.11.0", 42 | "joi": "^17.12.2", 43 | "nodemailer": "^6.9.13", 44 | "pg": "^8.11.5", 45 | "reflect-metadata": "^0.2.0", 46 | "rxjs": "^7.8.1", 47 | "typeorm": "^0.3.20", 48 | "uuid": "^10.0.0" 49 | }, 50 | "devDependencies": { 51 | "@faker-js/faker": "^8.4.1", 52 | "@nestjs/cli": "^10.0.0", 53 | "@nestjs/schematics": "^10.0.0", 54 | "@nestjs/testing": "^10.0.0", 55 | "@types/express": "^4.17.17", 56 | "@types/jest": "^29.5.2", 57 | "@types/multer": "^1.4.11", 58 | "@types/node": "^20.3.1", 59 | "@types/supertest": "^6.0.0", 60 | "@typescript-eslint/eslint-plugin": "^6.0.0", 61 | "@typescript-eslint/parser": "^6.0.0", 62 | "eslint": "^8.42.0", 63 | "eslint-config-prettier": "^9.0.0", 64 | "eslint-plugin-prettier": "^5.0.0", 65 | "jest": "^29.5.0", 66 | "prettier": "^3.0.0", 67 | "source-map-support": "^0.5.21", 68 | "supertest": "^6.3.3", 69 | "ts-jest": "^29.1.0", 70 | "ts-loader": "^9.4.3", 71 | "ts-node": "^10.9.1", 72 | "tsconfig-paths": "^4.2.0", 73 | "typescript": "^5.1.3" 74 | }, 75 | "jest": { 76 | "moduleFileExtensions": [ 77 | "js", 78 | "json", 79 | "ts" 80 | ], 81 | "rootDir": "./", 82 | "modulePaths": [ 83 | "" 84 | ], 85 | "testRegex": ".*\\.spec\\.ts$", 86 | "transform": { 87 | "^.+\\.(t|j)s$": "ts-jest" 88 | }, 89 | "collectCoverageFrom": [ 90 | "**/*.(t|j)s" 91 | ], 92 | "coverageDirectory": "../coverage", 93 | "testEnvironment": "node" 94 | } 95 | } -------------------------------------------------------------------------------- /src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | 3 | import { AppController } from './app.controller'; 4 | import { AppService } from './app.service'; 5 | 6 | describe('AppController', () => { 7 | let appController: AppController; 8 | 9 | beforeEach(async () => { 10 | const app: TestingModule = await Test.createTestingModule({ 11 | controllers: [AppController], 12 | providers: [AppService], 13 | }).compile(); 14 | 15 | appController = app.get(AppController); 16 | }); 17 | 18 | describe('root', () => { 19 | it('Should be defined', () => { 20 | expect(appController).toBeDefined(); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | 3 | import { AppService } from './app.service'; 4 | 5 | @Controller() 6 | export class AppController { 7 | constructor(private readonly appService: AppService) {} 8 | } 9 | -------------------------------------------------------------------------------- /src/app.create.ts: -------------------------------------------------------------------------------- 1 | import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; 2 | import { INestApplication, ValidationPipe } from '@nestjs/common'; 3 | 4 | import { ConfigService } from '@nestjs/config'; 5 | import { config } from 'aws-sdk'; 6 | 7 | export function appCreate(app: INestApplication): void { 8 | /* 9 | * Use validation pipes globally 10 | */ 11 | app.useGlobalPipes( 12 | new ValidationPipe({ 13 | whitelist: true, 14 | forbidNonWhitelisted: true, 15 | transform: true, 16 | transformOptions: { 17 | enableImplicitConversion: true, 18 | }, 19 | }), 20 | ); 21 | 22 | /** 23 | * swagger configuration 24 | */ 25 | const swaggerConfig = new DocumentBuilder() 26 | .setTitle('NestJs Masterclass - Blog app API') 27 | .setDescription('Use the base API URL as http://localhost:3000') 28 | .setTermsOfService('http://localhost:3000/terms-of-service') 29 | .setLicense( 30 | 'MIT License', 31 | 'https://github.com/git/git-scm.com/blob/main/MIT-LICENSE.txt', 32 | ) 33 | .addServer('http://localhost:3000') 34 | .setVersion('1.0') 35 | .build(); 36 | 37 | // Instantiate Document 38 | const document = SwaggerModule.createDocument(app, swaggerConfig); 39 | SwaggerModule.setup('api', app, document); 40 | 41 | /* 42 | * Setup AWS SDK used for uploadingg files to AWS S3 43 | * */ 44 | const configService = app.get(ConfigService); 45 | config.update({ 46 | credentials: { 47 | accessKeyId: configService.get('appConfig.awsAccessKeyId'), 48 | secretAccessKey: configService.get( 49 | 'appConfig.awsSecretAccessKey', 50 | ), 51 | }, 52 | region: configService.get('appConfig.awsRegion'), 53 | }); 54 | 55 | // Enable CORS 56 | app.enableCors(); 57 | } 58 | -------------------------------------------------------------------------------- /src/app.endpoints.http: -------------------------------------------------------------------------------- 1 | ## Get Request For App controller 2 | GET http://localhost:3000/ -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core'; 2 | import { ConfigModule, ConfigService } from '@nestjs/config'; 3 | 4 | import { AccessTokenGuard } from './auth/guards/access-token/access-token.guard'; 5 | import { AppController } from './app.controller'; 6 | import { AppService } from './app.service'; 7 | import { AuthModule } from './auth/auth.module'; 8 | import { AuthenticationGuard } from './auth/guards/authentication/authentication.guard'; 9 | import { DataResponseInterceptor } from './common/interceptors/data-response/data-response.interceptor'; 10 | import { JwtModule } from '@nestjs/jwt'; 11 | import { MetaOptionsModule } from './meta-options/meta-options.module'; 12 | import { Module } from '@nestjs/common'; 13 | import { PaginationModule } from './common/pagination/pagination.module'; 14 | import { PostsModule } from './posts/posts.module'; 15 | import { Tag } from './tags/tag.entity'; 16 | import { TagsModule } from './tags/tags.module'; 17 | import { TypeOrmModule } from '@nestjs/typeorm'; 18 | /** 19 | * Importing Entities 20 | * */ 21 | import { User } from './users/user.entity'; 22 | import { UsersModule } from './users/users.module'; 23 | import { UploadsModule } from './uploads/uploads.module'; 24 | import { MailModule } from './mail/mail.module'; 25 | import appConfig from './config/app.config'; 26 | import databaseConfig from './config/database.config'; 27 | import enviromentValidation from './config/enviroment.validation'; 28 | import jwtConfig from './auth/config/jwt.config'; 29 | 30 | // Get the current NODE_ENV 31 | const ENV = process.env.NODE_ENV; 32 | 33 | @Module({ 34 | imports: [ 35 | UsersModule, 36 | PostsModule, 37 | AuthModule, 38 | ConfigModule.forRoot({ 39 | isGlobal: true, 40 | //envFilePath: ['.env.development', '.env'], 41 | envFilePath: !ENV ? '.env' : `.env.${ENV}`, 42 | load: [appConfig, databaseConfig], 43 | validationSchema: enviromentValidation, 44 | }), 45 | TypeOrmModule.forRootAsync({ 46 | imports: [ConfigModule], 47 | inject: [ConfigService], 48 | useFactory: (configService: ConfigService) => ({ 49 | type: 'postgres', 50 | //entities: [User], 51 | synchronize: configService.get('database.synchronize'), 52 | port: configService.get('database.port'), 53 | username: configService.get('database.user'), 54 | password: configService.get('database.password'), 55 | host: configService.get('database.host'), 56 | autoLoadEntities: configService.get('database.autoLoadEntities'), 57 | database: configService.get('database.name'), 58 | }), 59 | }), 60 | ConfigModule.forFeature(jwtConfig), 61 | JwtModule.registerAsync(jwtConfig.asProvider()), 62 | TagsModule, 63 | MetaOptionsModule, 64 | PaginationModule, 65 | UploadsModule, 66 | MailModule, 67 | ], 68 | controllers: [AppController], 69 | providers: [ 70 | AppService, 71 | { 72 | provide: APP_GUARD, 73 | useClass: AuthenticationGuard, 74 | }, 75 | { 76 | provide: APP_INTERCEPTOR, 77 | useClass: DataResponseInterceptor, 78 | }, 79 | AccessTokenGuard, 80 | ], 81 | }) 82 | export class AppModule {} 83 | -------------------------------------------------------------------------------- /src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService {} 5 | -------------------------------------------------------------------------------- /src/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common'; 2 | 3 | import { AuthService } from './providers/auth.service'; 4 | import { SignInDto } from './dtos/signin.dto'; 5 | import { Auth } from './decorators/auth.decorator'; 6 | import { AuthType } from './enums/auth-type.enum'; 7 | import { RefreshTokenDto } from './dtos/refresh-token.dto'; 8 | 9 | @Controller('auth') 10 | export class AuthController { 11 | constructor( 12 | /* 13 | * Injecting Auth Service 14 | */ 15 | private readonly authService: AuthService, 16 | ) {} 17 | 18 | @Post('sign-in') 19 | @HttpCode(HttpStatus.OK) 20 | @Auth(AuthType.None) 21 | public signIn(@Body() signInDto: SignInDto) { 22 | return this.authService.signIn(signInDto); 23 | } 24 | 25 | @Auth(AuthType.None) 26 | @HttpCode(HttpStatus.OK) // changed since the default is 201 27 | @Post('refresh-tokens') 28 | refreshTokens(@Body() refreshTokenDto: RefreshTokenDto) { 29 | return this.authService.refreshTokens(refreshTokenDto); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { ConfigModule, ConfigService } from '@nestjs/config'; 2 | import { Module, forwardRef } from '@nestjs/common'; 3 | 4 | import { AuthController } from './auth.controller'; 5 | import { AuthService } from './providers/auth.service'; 6 | import { BcryptProvider } from './providers/bcrypt.provider'; 7 | import { GenerateTokensProvider } from './providers/generate-tokens.provider'; 8 | import { HashingProvider } from './providers/hashing.provider'; 9 | import { JwtModule } from '@nestjs/jwt'; 10 | import { RefreshTokensProvider } from './providers/refresh-tokens.provider'; 11 | import { SignInProvider } from './providers/sign-in.provider'; 12 | import { UsersModule } from 'src/users/users.module'; 13 | import { GoogleAuthenticationController } from './social/google-authentication.controller'; 14 | import { GoogleAuthenticationService } from './social/providers/google-authentication.service'; 15 | import jwtConfig from './config/jwt.config'; 16 | 17 | @Module({ 18 | controllers: [AuthController, GoogleAuthenticationController], 19 | providers: [ 20 | AuthService, 21 | { 22 | provide: HashingProvider, 23 | useClass: BcryptProvider, 24 | }, 25 | SignInProvider, 26 | GenerateTokensProvider, 27 | RefreshTokensProvider, 28 | GoogleAuthenticationService, 29 | ], 30 | imports: [ 31 | forwardRef(() => UsersModule), 32 | ConfigModule.forFeature(jwtConfig), 33 | JwtModule.registerAsync(jwtConfig.asProvider()), 34 | ], 35 | exports: [AuthService, HashingProvider], 36 | }) 37 | export class AuthModule {} 38 | -------------------------------------------------------------------------------- /src/auth/config/jwt.config.ts: -------------------------------------------------------------------------------- 1 | import { registerAs } from '@nestjs/config'; 2 | 3 | export default registerAs('jwt', () => { 4 | return { 5 | secret: process.env.JWT_SECRET, 6 | audience: process.env.JWT_TOKEN_AUDIENCE, 7 | issuer: process.env.JWT_TOKEN_ISSUER, 8 | accessTokenTtl: parseInt(process.env.JWT_ACCESS_TOKEN_TTL ?? '3600', 10), 9 | refreshTokenTtl: parseInt(process.env.JWT_REFRESH_TOKEN_TTL ?? '86400', 10), 10 | googleClientId: process.env.GOOGLE_CLIENT_ID, 11 | googleClientSecret: process.env.GOOGLE_CLIENT_SECRET, 12 | }; 13 | }); 14 | -------------------------------------------------------------------------------- /src/auth/constants/auth.constants.ts: -------------------------------------------------------------------------------- 1 | export const REQUEST_USER_KEY = 'user'; 2 | -------------------------------------------------------------------------------- /src/auth/decorators/active-user.decorator.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext, createParamDecorator } from '@nestjs/common'; 2 | 3 | import { ActiveUserData } from '../interfaces/active-user-data.interface'; 4 | import { REQUEST_USER_KEY } from '../constants/auth.constants'; 5 | 6 | export const ActiveUser = createParamDecorator( 7 | (field: keyof ActiveUserData | undefined, ctx: ExecutionContext) => { 8 | const request = ctx.switchToHttp().getRequest(); 9 | const user: ActiveUserData = request[REQUEST_USER_KEY]; 10 | 11 | // If a user passes a field to the decorator use only that field 12 | return field ? user?.[field] : user; 13 | }, 14 | ); 15 | -------------------------------------------------------------------------------- /src/auth/decorators/auth.decorator.ts: -------------------------------------------------------------------------------- 1 | import { AuthType } from '../enums/auth-type.enum'; 2 | import { SetMetadata } from '@nestjs/common'; 3 | 4 | export const AUTH_TYPE_KEY = 'authType'; 5 | 6 | export const Auth = (...authTypes: AuthType[]) => 7 | SetMetadata(AUTH_TYPE_KEY, authTypes); 8 | -------------------------------------------------------------------------------- /src/auth/dtos/refresh-token.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsString } from 'class-validator'; 2 | 3 | export class RefreshTokenDto { 4 | @IsNotEmpty() 5 | @IsString() 6 | refreshToken: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/auth/dtos/signin.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsNotEmpty, MinLength } from 'class-validator'; 2 | 3 | export class SignInDto { 4 | @IsEmail() 5 | @IsNotEmpty() 6 | email: string; 7 | 8 | @MinLength(8) 9 | @IsNotEmpty() 10 | password: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/auth/enums/auth-type.enum.ts: -------------------------------------------------------------------------------- 1 | export enum AuthType { 2 | Bearer, 3 | None, 4 | } 5 | -------------------------------------------------------------------------------- /src/auth/guards/access-token/access-token.guard.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CanActivate, 3 | ExecutionContext, 4 | Inject, 5 | Injectable, 6 | UnauthorizedException, 7 | } from '@nestjs/common'; 8 | import { ConfigType } from '@nestjs/config'; 9 | import { JwtService } from '@nestjs/jwt'; 10 | import { Observable } from 'rxjs'; 11 | import jwtConfig from 'src/auth/config/jwt.config'; 12 | import { Request } from 'express'; 13 | import { REQUEST_USER_KEY } from 'src/auth/constants/auth.constants'; 14 | 15 | @Injectable() 16 | export class AccessTokenGuard implements CanActivate { 17 | constructor( 18 | private readonly jwtService: JwtService, 19 | @Inject(jwtConfig.KEY) 20 | private readonly jwtConfiguration: ConfigType, 21 | ) {} 22 | 23 | async canActivate(context: ExecutionContext): Promise { 24 | // Extract the request from the execution context 25 | const request = context.switchToHttp().getRequest(); 26 | // Extract the token from the header 27 | const token = this.extractTokenFromHeader(request); 28 | 29 | if (!token) { 30 | throw new UnauthorizedException(); 31 | } 32 | try { 33 | const payload = await this.jwtService.verifyAsync( 34 | token, 35 | this.jwtConfiguration, 36 | ); 37 | request[REQUEST_USER_KEY] = payload; 38 | } catch { 39 | throw new UnauthorizedException(); 40 | } 41 | return true; 42 | } 43 | 44 | private extractTokenFromHeader(request: Request): string | undefined { 45 | const [_, token] = request.headers.authorization?.split(' ') ?? []; 46 | return token; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/auth/guards/authentication/authentication.guard.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CanActivate, 3 | ExecutionContext, 4 | Injectable, 5 | UnauthorizedException, 6 | } from '@nestjs/common'; 7 | 8 | import { AUTH_TYPE_KEY } from 'src/auth/decorators/auth.decorator'; 9 | import { AccessTokenGuard } from '../access-token/access-token.guard'; 10 | import { AuthType } from 'src/auth/enums/auth-type.enum'; 11 | import { Observable } from 'rxjs'; 12 | import { Reflector } from '@nestjs/core'; 13 | 14 | @Injectable() 15 | export class AuthenticationGuard implements CanActivate { 16 | // Set the default Auth Type 17 | private static readonly defaultAuthType = AuthType.Bearer; 18 | 19 | // Create authTypeGuardMap 20 | private readonly authTypeGuardMap: Record< 21 | AuthType, 22 | CanActivate | CanActivate[] 23 | > = { 24 | [AuthType.Bearer]: this.accessTokenGuard, 25 | [AuthType.None]: { canActivate: () => true }, 26 | }; 27 | 28 | constructor( 29 | private readonly reflector: Reflector, 30 | private readonly accessTokenGuard: AccessTokenGuard, 31 | ) {} 32 | 33 | async canActivate(context: ExecutionContext): Promise { 34 | const authTypes = this.reflector.getAllAndOverride( 35 | AUTH_TYPE_KEY, 36 | [context.getHandler(), context.getClass()], 37 | ) ?? [AuthenticationGuard.defaultAuthType]; 38 | 39 | const guards = authTypes.map((type) => this.authTypeGuardMap[type]).flat(); 40 | 41 | // Declare the default error 42 | let error = new UnauthorizedException(); 43 | 44 | for (const instance of guards) { 45 | // Decalre a new constant 46 | const canActivate = await Promise.resolve( 47 | // Here the AccessToken Guard Will be fired and check if user has permissions to acces 48 | // Later Multiple AuthTypes can be used even if one of them returns true 49 | // The user is Authorised to access the resource 50 | instance.canActivate(context), 51 | ).catch((err) => { 52 | error = err; 53 | }); 54 | 55 | if (canActivate) { 56 | return true; 57 | } 58 | } 59 | 60 | throw error; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/auth/http/refresh-tokens.endpoints.http: -------------------------------------------------------------------------------- 1 | POST http://localhost:3000/auth/refresh-tokens 2 | Content-Type: application/json 3 | Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjE5LCJlbWFpbCI6Im1hcmtAZG9lLmNvbSIsImlhdCI6MTcxODEyODM0MCwiZXhwIjoxNzE4MTMxOTQwLCJhdWQiOiJsb2NhbGhvc3Q6MzAwMCIsImlzcyI6ImxvY2FsaG9zdDozMDAwIn0.ECLjLaOUCs1YRS-T-eLWZAf7Hp-K1InQVqb_gSivOJc 4 | 5 | { 6 | "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjE5LCJpYXQiOjE3MTgxMjgzNDAsImV4cCI6MTcxODIxNDc0MCwiYXVkIjoibG9jYWxob3N0OjMwMDAiLCJpc3MiOiJsb2NhbGhvc3Q6MzAwMCJ9.8c7whmu_AhNyKKNSBzc46pBJQVwgaaPSS8V3wuGxZ6Y" 7 | } -------------------------------------------------------------------------------- /src/auth/http/signin.endpoints.http: -------------------------------------------------------------------------------- 1 | POST http://localhost:3000/auth/sign-in 2 | Content-Type: application/json 3 | 4 | { 5 | "email": "mark@doe.com", 6 | "password": "Password123#" 7 | } -------------------------------------------------------------------------------- /src/auth/interfaces/active-user-data.interface.ts: -------------------------------------------------------------------------------- 1 | export interface ActiveUserData { 2 | /** 3 | * The ID of the user 4 | */ 5 | sub: number; 6 | 7 | /** 8 | * User's email address 9 | */ 10 | email: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/auth/providers/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { SignInProvider } from './sign-in.provider'; 2 | import { Inject, Injectable, forwardRef } from '@nestjs/common'; 3 | 4 | import { UsersService } from 'src/users/providers/users.service'; 5 | import { SignInDto } from '../dtos/signin.dto'; 6 | import { RefreshTokensProvider } from './refresh-tokens.provider'; 7 | import { RefreshTokenDto } from '../dtos/refresh-token.dto'; 8 | 9 | @Injectable() 10 | export class AuthService { 11 | constructor( 12 | // Injecting UserService 13 | @Inject(forwardRef(() => UsersService)) 14 | private readonly usersService: UsersService, 15 | 16 | /** 17 | * Inject the signInProvider 18 | */ 19 | private readonly signInProvider: SignInProvider, 20 | 21 | /** 22 | * Inject refreshTokensProvider 23 | */ 24 | private readonly refreshTokensProvider: RefreshTokensProvider, 25 | ) {} 26 | 27 | public async signIn(signInDto: SignInDto) { 28 | return await this.signInProvider.signIn(signInDto); 29 | } 30 | 31 | public async refreshTokens(refreshTokenDto: RefreshTokenDto) { 32 | return await this.refreshTokensProvider.refreshTokens(refreshTokenDto); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/auth/providers/bcrypt.provider.ts: -------------------------------------------------------------------------------- 1 | import * as bcrypt from 'bcrypt'; 2 | 3 | import { HashingProvider } from './hashing.provider'; 4 | import { Injectable } from '@nestjs/common'; 5 | 6 | @Injectable() 7 | export class BcryptProvider implements HashingProvider { 8 | public async hashPassword(data: string | Buffer): Promise { 9 | // Generate the salt 10 | const salt = await bcrypt.genSalt(); 11 | return bcrypt.hash(data, salt); 12 | } 13 | 14 | public async comparePassword( 15 | data: string | Buffer, 16 | encrypted: string, 17 | ): Promise { 18 | return bcrypt.compare(data, encrypted); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/auth/providers/generate-tokens.provider.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@nestjs/common'; 2 | import { JwtService } from '@nestjs/jwt'; 3 | import jwtConfig from '../config/jwt.config'; 4 | import { ConfigType } from '@nestjs/config'; 5 | import { User } from 'src/users/user.entity'; 6 | import { ActiveUserData } from '../interfaces/active-user-data.interface'; 7 | 8 | @Injectable() 9 | export class GenerateTokensProvider { 10 | constructor( 11 | /** 12 | * Inject jwtService 13 | */ 14 | private readonly jwtService: JwtService, 15 | 16 | /** 17 | * Inject jwtConfiguration 18 | */ 19 | @Inject(jwtConfig.KEY) 20 | private readonly jwtConfiguration: ConfigType, 21 | ) {} 22 | 23 | public async signToken(userId: number, expiresIn: number, payload?: T) { 24 | return await this.jwtService.signAsync( 25 | { 26 | sub: userId, 27 | ...payload, 28 | }, 29 | { 30 | audience: this.jwtConfiguration.audience, 31 | issuer: this.jwtConfiguration.issuer, 32 | secret: this.jwtConfiguration.secret, 33 | expiresIn, 34 | }, 35 | ); 36 | } 37 | 38 | public async generateTokens(user: User) { 39 | const [accessToken, refreshToken] = await Promise.all([ 40 | // Generate Access Token with Email 41 | this.signToken>( 42 | user.id, 43 | this.jwtConfiguration.accessTokenTtl, 44 | { email: user.email }, 45 | ), 46 | 47 | // Generate Refresh token without email 48 | this.signToken(user.id, this.jwtConfiguration.refreshTokenTtl), 49 | ]); 50 | return { 51 | accessToken, 52 | refreshToken, 53 | }; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/auth/providers/hashing.provider.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export abstract class HashingProvider { 5 | abstract hashPassword(data: string | Buffer): Promise; 6 | 7 | abstract comparePassword( 8 | data: string | Buffer, 9 | encrypted: string, 10 | ): Promise; 11 | } 12 | -------------------------------------------------------------------------------- /src/auth/providers/refresh-tokens.provider.ts: -------------------------------------------------------------------------------- 1 | import { ActiveUserData } from '../interfaces/active-user-data.interface'; 2 | import { 3 | Inject, 4 | Injectable, 5 | UnauthorizedException, 6 | forwardRef, 7 | } from '@nestjs/common'; 8 | import { RefreshTokenDto } from '../dtos/refresh-token.dto'; 9 | import { JwtService } from '@nestjs/jwt'; 10 | import jwtConfig from '../config/jwt.config'; 11 | import { ConfigType } from '@nestjs/config'; 12 | import { UsersService } from 'src/users/providers/users.service'; 13 | import { GenerateTokensProvider } from './generate-tokens.provider'; 14 | 15 | @Injectable() 16 | export class RefreshTokensProvider { 17 | constructor( 18 | /** 19 | * Inject jwtService 20 | */ 21 | private readonly jwtService: JwtService, 22 | 23 | /** 24 | * Inject jwtConfiguration 25 | */ 26 | @Inject(jwtConfig.KEY) 27 | private readonly jwtConfiguration: ConfigType, 28 | 29 | // Injecting UserService 30 | @Inject(forwardRef(() => UsersService)) 31 | private readonly usersService: UsersService, 32 | 33 | /** 34 | * Inject generateTokensProvider 35 | */ 36 | private readonly generateTokensProvider: GenerateTokensProvider, 37 | ) {} 38 | 39 | public async refreshTokens(refreshTokenDto: RefreshTokenDto) { 40 | // Verify the refresh token using jwtService 41 | try { 42 | const { sub } = await this.jwtService.verifyAsync< 43 | Pick 44 | >(refreshTokenDto.refreshToken, { 45 | secret: this.jwtConfiguration.secret, 46 | audience: this.jwtConfiguration.audience, 47 | issuer: this.jwtConfiguration.issuer, 48 | }); 49 | // Fetch the user from the database 50 | const user = await this.usersService.findOneById(sub); 51 | 52 | // Generate the tokens 53 | return await this.generateTokensProvider.generateTokens(user); 54 | } catch (error) { 55 | throw new UnauthorizedException(error); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/auth/providers/sign-in.provider.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Inject, 3 | Injectable, 4 | RequestTimeoutException, 5 | UnauthorizedException, 6 | forwardRef, 7 | } from '@nestjs/common'; 8 | import { UsersService } from 'src/users/providers/users.service'; 9 | import { SignInDto } from '../dtos/signin.dto'; 10 | import { HashingProvider } from './hashing.provider'; 11 | import { GenerateTokensProvider } from './generate-tokens.provider'; 12 | 13 | @Injectable() 14 | export class SignInProvider { 15 | constructor( 16 | // Injecting UserService 17 | @Inject(forwardRef(() => UsersService)) 18 | private readonly usersService: UsersService, 19 | 20 | /** 21 | * Inject the hashingProvider 22 | */ 23 | private readonly hashingProvider: HashingProvider, 24 | 25 | /** 26 | * Inject generateTokensProvider 27 | */ 28 | private readonly generateTokensProvider: GenerateTokensProvider, 29 | ) {} 30 | 31 | public async signIn(signInDto: SignInDto) { 32 | // find user by email ID 33 | let user = await this.usersService.findOneByEmail(signInDto.email); 34 | // Throw exception if user is not found 35 | // Above | Taken care by the findInByEmail method 36 | 37 | let isEqual: boolean = false; 38 | 39 | try { 40 | // Compare the password to hash 41 | isEqual = await this.hashingProvider.comparePassword( 42 | signInDto.password, 43 | user.password, 44 | ); 45 | } catch (error) { 46 | throw new RequestTimeoutException(error, { 47 | description: 'Could not compare the password', 48 | }); 49 | } 50 | 51 | if (!isEqual) { 52 | throw new UnauthorizedException('Password does not match'); 53 | } 54 | 55 | // Generate access token 56 | return await this.generateTokensProvider.generateTokens(user); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/auth/social/dtos/google-token.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, MaxLength } from 'class-validator'; 2 | 3 | export class GoogleTokenDto { 4 | @IsNotEmpty() 5 | token: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/auth/social/google-authentication.controller.ts: -------------------------------------------------------------------------------- 1 | import { GoogleAuthenticationService } from './providers/google-authentication.service'; 2 | import { Body, Controller, Post } from '@nestjs/common'; 3 | import { GoogleTokenDto } from './dtos/google-token.dto'; 4 | import { Auth } from '../decorators/auth.decorator'; 5 | import { AuthType } from '../enums/auth-type.enum'; 6 | 7 | @Auth(AuthType.None) 8 | @Controller('auth/google-authentication') 9 | export class GoogleAuthenticationController { 10 | constructor( 11 | /** 12 | * Inject googleAuthenticationService 13 | */ 14 | private readonly googleAuthenticationService: GoogleAuthenticationService, 15 | ) {} 16 | 17 | @Post() 18 | authenticate(@Body() googleTokenDto: GoogleTokenDto) { 19 | return this.googleAuthenticationService.authenticate(googleTokenDto); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/auth/social/providers/google-authentication.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Inject, 3 | Injectable, 4 | OnModuleInit, 5 | UnauthorizedException, 6 | forwardRef, 7 | } from '@nestjs/common'; 8 | import { ConfigType } from '@nestjs/config'; 9 | import jwtConfig from 'src/auth/config/jwt.config'; 10 | import { OAuth2Client } from 'google-auth-library'; 11 | import { UsersService } from 'src/users/providers/users.service'; 12 | import { GoogleTokenDto } from '../dtos/google-token.dto'; 13 | import { GenerateTokensProvider } from 'src/auth/providers/generate-tokens.provider'; 14 | import { InjectRepository } from '@nestjs/typeorm'; 15 | import { User } from 'src/users/user.entity'; 16 | 17 | @Injectable() 18 | export class GoogleAuthenticationService implements OnModuleInit { 19 | private oauthClient: OAuth2Client; 20 | 21 | constructor( 22 | // Injecting UserService 23 | @Inject(forwardRef(() => UsersService)) 24 | private readonly usersService: UsersService, 25 | /** 26 | * Inject jwtConfiguration 27 | */ 28 | @Inject(jwtConfig.KEY) 29 | private readonly jwtConfiguration: ConfigType, 30 | /** 31 | * Inject generateTokensProvider 32 | */ 33 | private readonly generateTokensProvider: GenerateTokensProvider, 34 | ) {} 35 | 36 | onModuleInit() { 37 | const clientId = this.jwtConfiguration.googleClientId; 38 | const clientSecret = this.jwtConfiguration.googleClientSecret; 39 | this.oauthClient = new OAuth2Client(clientId, clientSecret); 40 | } 41 | 42 | async authenticate(googleTokenDto: GoogleTokenDto) { 43 | try { 44 | // Verify the Google Token Sent By User 45 | const loginTicket = await this.oauthClient.verifyIdToken({ 46 | idToken: googleTokenDto.token, 47 | }); 48 | // Extract the payload from Google Token 49 | const { 50 | email, 51 | sub: googleId, 52 | given_name: firstName, 53 | family_name: lastName, 54 | } = loginTicket.getPayload(); 55 | // Find the user in the database using the googleId 56 | const user = await this.usersService.findOneByGoogleId(googleId); 57 | 58 | // If user id found generate the tokens 59 | if (user) { 60 | return await this.generateTokensProvider.generateTokens(user); 61 | } else { 62 | // If not create a new user and generate the tokens 63 | const newUser = await this.usersService.createGoogleUser({ 64 | email: email, 65 | firstName: firstName, 66 | lastName: lastName, 67 | googleId: googleId, 68 | }); 69 | return await this.generateTokensProvider.generateTokens(newUser); 70 | } 71 | 72 | // throw Unauthorised exception if not Authorised 73 | } catch (error) { 74 | throw new UnauthorizedException(error); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/common/interceptors/data-response/data-response.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CallHandler, 3 | ExecutionContext, 4 | Injectable, 5 | NestInterceptor, 6 | } from '@nestjs/common'; 7 | import { Observable, map, tap } from 'rxjs'; 8 | 9 | import { ConfigService } from '@nestjs/config'; 10 | 11 | @Injectable() 12 | export class DataResponseInterceptor implements NestInterceptor { 13 | constructor(private readonly configService: ConfigService) {} 14 | 15 | intercept(context: ExecutionContext, next: CallHandler): Observable { 16 | return next.handle().pipe( 17 | map((data) => ({ 18 | apiVersion: this.configService.get('appConfig.apiVersion'), 19 | data: data, 20 | })), 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/common/pagination/dtos/pagination-query.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsOptional, IsPositive } from 'class-validator'; 2 | 3 | import { Type } from 'class-transformer'; 4 | 5 | export class PaginationQueryDto { 6 | @IsOptional() 7 | @IsPositive() 8 | // Number of entries to return 9 | limit?: number = 10; 10 | 11 | @IsOptional() 12 | @IsPositive() 13 | // Number of entries to skip from start 14 | page?: number = 1; 15 | } 16 | -------------------------------------------------------------------------------- /src/common/pagination/interfaces/paginated.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Paginated { 2 | data: T[]; 3 | meta: { 4 | itemsPerPage: number; 5 | totalItems: number; 6 | currentPage: number; 7 | totalPages: number; 8 | }; 9 | links: { 10 | first?: string; 11 | previous?: string; 12 | current: string; 13 | next?: string; 14 | last?: string; 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /src/common/pagination/pagination.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { PaginationProvider } from './providers/pagination.provider'; 3 | 4 | @Module({ 5 | providers: [PaginationProvider], 6 | exports: [PaginationProvider], 7 | }) 8 | export class PaginationModule {} 9 | -------------------------------------------------------------------------------- /src/common/pagination/providers/pagination.provider.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@nestjs/common'; 2 | import { ObjectLiteral } from 'typeorm'; 3 | import { Paginated } from '../interfaces/paginated.interface'; 4 | import { PaginationQueryDto } from '../dtos/pagination-query.dto'; 5 | import { Repository } from 'typeorm'; 6 | import { REQUEST } from '@nestjs/core'; 7 | import { Request } from 'express'; 8 | import * as url from 'url'; 9 | 10 | @Injectable() 11 | export class PaginationProvider { 12 | /** 13 | * Use Constructor to Inject Request 14 | * */ 15 | constructor(@Inject(REQUEST) private readonly request: Request) {} 16 | 17 | public async paginateQuery( 18 | paginationQuery: PaginationQueryDto, 19 | repository: Repository, 20 | ): Promise> { 21 | let results = await repository.find({ 22 | skip: (paginationQuery.page - 1) * paginationQuery.limit, 23 | take: paginationQuery.limit, 24 | }); 25 | 26 | /** 27 | * Create the request URLs 28 | */ 29 | const baseURL = 30 | this.request.protocol + '://' + this.request.headers.host + '/'; 31 | const newUrl = new URL(this.request.url, baseURL); 32 | 33 | // Calculate page numbers 34 | const totalItems = await repository.count(); 35 | const totalPages = Math.ceil(totalItems / paginationQuery.limit); 36 | const nextPage = 37 | paginationQuery.page === totalPages 38 | ? paginationQuery.page 39 | : paginationQuery.page + 1; 40 | const previousPage = 41 | paginationQuery.page === 1 42 | ? paginationQuery.page 43 | : paginationQuery.page - 1; 44 | 45 | let finalResponse = { 46 | data: results, 47 | meta: { 48 | itemsPerPage: paginationQuery.limit, 49 | totalItems: totalItems, 50 | currentPage: paginationQuery.page, 51 | totalPages: Math.ceil(totalItems / paginationQuery.limit), 52 | }, 53 | links: { 54 | first: `${newUrl.origin}${newUrl.pathname}?limit=${paginationQuery.limit}&page=1`, 55 | last: `${newUrl.origin}${newUrl.pathname}?limit=${paginationQuery.limit}&page=${totalPages}`, 56 | current: `${newUrl.origin}${newUrl.pathname}?limit=${paginationQuery.limit}&page=${paginationQuery.page}`, 57 | next: `${newUrl.origin}${newUrl.pathname}?limit=${paginationQuery.limit}&page=${nextPage}`, 58 | previous: `${newUrl.origin}${newUrl.pathname}?limit=${paginationQuery.limit}&page=${previousPage}`, 59 | }, 60 | }; 61 | 62 | return finalResponse; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/config/app.config.ts: -------------------------------------------------------------------------------- 1 | import { registerAs } from '@nestjs/config'; 2 | 3 | export default registerAs('appConfig', () => ({ 4 | environment: process.env.NODE_ENV || 'production', 5 | apiVersion: process.env.API_VERSION, 6 | awsBucketName: process.env.AWS_PUBLIC_BUCKET_NAME, 7 | awsAccessKeyId: process.env.AWS_ACCESS_KEY_ID, 8 | awsSecretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, 9 | awsRegion: process.env.AWS_REGION, 10 | awsCloudfrontUrl: process.env.AWS_CLOUDFRONT_URL, 11 | mailHost: process.env.MAIL_HOST, 12 | smtpUsername: process.env.SMTP_USERNAME, 13 | smtpPassword: process.env.SMTP_PASSWORD, 14 | })); 15 | -------------------------------------------------------------------------------- /src/config/database.config.ts: -------------------------------------------------------------------------------- 1 | import { registerAs } from '@nestjs/config'; 2 | 3 | export default registerAs('database', () => ({ 4 | host: process.env.DATABASE_HOST || 'localhost', 5 | port: parseInt(process.env.DATABASE_PORT) || 5432, 6 | user: process.env.DATABASE_USER, 7 | password: process.env.DATABASE_PASSWORD, 8 | name: process.env.DATABASE_NAME, 9 | synchronize: process.env.DATABASE_SYNC === 'true' ? true : false, 10 | autoLoadEntities: process.env.DATABASE_AUTOLOAD === 'true' ? true : false, 11 | })); 12 | -------------------------------------------------------------------------------- /src/config/enviroment.validation.ts: -------------------------------------------------------------------------------- 1 | import * as Joi from 'joi'; 2 | 3 | export default Joi.object({ 4 | NODE_ENV: Joi.string() 5 | .valid('development', 'production', 'test', 'provision') 6 | .default('development'), 7 | DATABASE_PORT: Joi.number().port().default(5432), 8 | DATABASE_PASSWORD: Joi.string().required(), 9 | DATABASE_HOST: Joi.string().required(), 10 | DATABASE_NAME: Joi.string().required(), 11 | DATABASE_USER: Joi.string().required(), 12 | PROFILE_API_KEY: Joi.string().required(), 13 | JWT_SECRET: Joi.string().required(), 14 | JWT_TOKEN_AUDIENCE: Joi.required(), 15 | JWT_TOKEN_ISSUER: Joi.string().required(), 16 | JWT_ACCESS_TOKEN_TTL: Joi.number().required(), 17 | JWT_REFRESH_TOKEN_TTL: Joi.number().required(), 18 | AWS_PUBLIC_BUCKET_NAME: Joi.string().required(), 19 | AWS_ACCESS_KEY_ID: Joi.string().required(), 20 | AWS_SECRET_ACCESS_KEY: Joi.string().required(), 21 | AWS_REGION: Joi.string().required(), 22 | AWS_CLOUDFRONT_URL: Joi.string().required(), 23 | MAIL_HOST: Joi.string().required(), 24 | SMTP_USERNAME: Joi.string().required(), 25 | SMTP_PASSWORD: Joi.string().required(), 26 | }); 27 | -------------------------------------------------------------------------------- /src/mail/mail.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | 3 | import { ConfigService } from '@nestjs/config'; 4 | import { EjsAdapter } from '@nestjs-modules/mailer/dist/adapters/ejs.adapter'; 5 | import { MailService } from './providers/mail.service'; 6 | import { MailerModule } from '@nestjs-modules/mailer'; 7 | import { join } from 'path'; 8 | 9 | @Global() 10 | @Module({ 11 | imports: [ 12 | MailerModule.forRootAsync({ 13 | useFactory: async (config: ConfigService) => ({ 14 | transport: { 15 | host: config.get('appConfig.mailHost'), 16 | secure: false, 17 | port: 2525, 18 | auth: { 19 | user: config.get('appConfig.smtpUsername'), 20 | pass: config.get('appConfig.smtpPassword'), 21 | }, 22 | }, 23 | defaults: { 24 | from: `"My Blog" `, 25 | }, 26 | template: { 27 | dir: join(__dirname, 'templates'), 28 | adapter: new EjsAdapter({ inlineCssEnabled: true }), 29 | options: { 30 | strict: false, 31 | }, 32 | }, 33 | }), 34 | inject: [ConfigService], 35 | }), 36 | ], 37 | providers: [MailService], 38 | exports: [MailService], 39 | }) 40 | export class MailModule {} 41 | -------------------------------------------------------------------------------- /src/mail/providers/mail.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { MailerService } from '@nestjs-modules/mailer'; 3 | import { User } from 'src/users/user.entity'; 4 | 5 | @Injectable() 6 | export class MailService { 7 | constructor(private mailerService: MailerService) {} 8 | 9 | async sendUserWelcome(user: User): Promise { 10 | await this.mailerService.sendMail({ 11 | to: user.email, 12 | // override default from 13 | from: '"Onbaording Team" ', 14 | subject: 'Welcome to NestJs Blog', 15 | // `.ejs` extension is appended automatically to template 16 | template: './welcome', 17 | // Context is available in email template 18 | context: { 19 | name: user.firstName, 20 | email: user.email, 21 | loginUrl: 'http://localhost:3000', 22 | }, 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/mail/templates/welcome.ejs: -------------------------------------------------------------------------------- 1 |

2 | Dear <%= name %>, 3 |

4 |

5 | Thanks for signing up for Nice App. We're very excited to have you on board. 6 |

7 | 8 |

9 | Here are your account details: 10 |

11 |
    19 |
  • Email: <%= email %>
  • 20 |
  • Login URL: <%= loginUrl %>
  • 21 |
22 | 23 |

24 | Thanks!
NestJs Blog Support 25 |

26 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { AppModule } from './app.module'; 2 | import { NestFactory } from '@nestjs/core'; 3 | import { appCreate } from './app.create'; 4 | 5 | async function bootstrap() { 6 | const app = await NestFactory.create(AppModule); 7 | 8 | // Add required middleware 9 | appCreate(app); 10 | 11 | await app.listen(process.env.PORT || 3000); 12 | } 13 | bootstrap(); 14 | -------------------------------------------------------------------------------- /src/meta-options/dtos/create-post-meta-options.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsJSON, IsNotEmpty, IsString, MaxLength } from 'class-validator'; 2 | 3 | export class CreatePostMetaOptionsDto { 4 | @IsNotEmpty() 5 | @IsJSON() 6 | metaValue: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/meta-options/http/meta-options.post.endpoints.http: -------------------------------------------------------------------------------- 1 | POST http://localhost:3000/meta-options 2 | Content-Type: application/json 3 | 4 | { 5 | "metaValue": "{ \"sidebarEnabled\": true, \"footerActive\": true }" 6 | } -------------------------------------------------------------------------------- /src/meta-options/meta-option.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Column, 3 | CreateDateColumn, 4 | DeleteDateColumn, 5 | Entity, 6 | JoinColumn, 7 | OneToOne, 8 | PrimaryGeneratedColumn, 9 | UpdateDateColumn, 10 | } from 'typeorm'; 11 | 12 | import { Post } from 'src/posts/post.entity'; 13 | 14 | @Entity() 15 | export class MetaOption { 16 | @PrimaryGeneratedColumn() 17 | id: number; 18 | 19 | @Column({ 20 | type: 'json', 21 | nullable: false, 22 | }) 23 | metaValue: string; 24 | 25 | @CreateDateColumn() 26 | createDate: Date; 27 | 28 | @UpdateDateColumn() 29 | updateDate: Date; 30 | 31 | @OneToOne(() => Post, (post) => post.metaOptions, { 32 | onDelete: 'CASCADE', 33 | }) 34 | @JoinColumn() 35 | post: Post; 36 | } 37 | -------------------------------------------------------------------------------- /src/meta-options/meta-options.controller.ts: -------------------------------------------------------------------------------- 1 | import { CreatePostMetaOptionsDto } from './dtos/create-post-meta-options.dto'; 2 | import { MetaOptionsService } from './providers/meta-options.service'; 3 | import { Body, Controller, Post } from '@nestjs/common'; 4 | 5 | @Controller('meta-options') 6 | export class MetaOptionsController { 7 | constructor( 8 | /** 9 | * Inject MetaOptionsService 10 | * */ 11 | private readonly MetaOptionsService: MetaOptionsService, 12 | ) {} 13 | 14 | @Post() 15 | public async create( 16 | @Body() createPostMetaOptionsDto: CreatePostMetaOptionsDto, 17 | ) { 18 | return this.MetaOptionsService.create(createPostMetaOptionsDto); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/meta-options/meta-options.module.ts: -------------------------------------------------------------------------------- 1 | import { MetaOption } from './meta-option.entity'; 2 | import { MetaOptionsController } from './meta-options.controller'; 3 | import { MetaOptionsService } from './providers/meta-options.service'; 4 | import { Module } from '@nestjs/common'; 5 | import { TypeOrmModule } from '@nestjs/typeorm'; 6 | 7 | @Module({ 8 | controllers: [MetaOptionsController], 9 | imports: [TypeOrmModule.forFeature([MetaOption])], 10 | providers: [MetaOptionsService], 11 | exports: [MetaOptionsService], 12 | }) 13 | export class MetaOptionsModule {} 14 | -------------------------------------------------------------------------------- /src/meta-options/providers/meta-options.service.ts: -------------------------------------------------------------------------------- 1 | import { CreatePostMetaOptionsDto } from '../dtos/create-post-meta-options.dto'; 2 | import { Injectable } from '@nestjs/common'; 3 | import { MetaOption } from '../meta-option.entity'; 4 | import { Repository } from 'typeorm'; 5 | import { InjectRepository } from '@nestjs/typeorm'; 6 | 7 | @Injectable() 8 | export class MetaOptionsService { 9 | constructor( 10 | /** 11 | * Injecting metaOptions repository 12 | */ 13 | @InjectRepository(MetaOption) 14 | private metaOptionsRepository: Repository, 15 | ) {} 16 | 17 | public async create(createPostMetaOptionsDto: CreatePostMetaOptionsDto) { 18 | let metaOption = this.metaOptionsRepository.create( 19 | createPostMetaOptionsDto, 20 | ); 21 | return await this.metaOptionsRepository.save(metaOption); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/posts/dtos/create-post-meta-options.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsString } from 'class-validator'; 2 | 3 | export class CreatePostMetaOptionsDto { 4 | @IsString() 5 | @IsNotEmpty() 6 | key: string; 7 | 8 | @IsNotEmpty() 9 | value: any; 10 | } 11 | -------------------------------------------------------------------------------- /src/posts/dtos/create-post.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; 2 | import { 3 | IsArray, 4 | IsDate, 5 | IsEnum, 6 | IsISO8601, 7 | IsInt, 8 | IsJSON, 9 | IsNotEmpty, 10 | IsOptional, 11 | IsString, 12 | IsUrl, 13 | Matches, 14 | Max, 15 | MaxLength, 16 | Min, 17 | MinLength, 18 | ValidateNested, 19 | isNotEmpty, 20 | } from 'class-validator'; 21 | 22 | import { CreatePostMetaOptionsDto } from '../../meta-options/dtos/create-post-meta-options.dto'; 23 | import { CreateTagDto } from 'src/tags/dtos/create-tag.dto'; 24 | import { DeepPartial } from 'typeorm'; 25 | import { Type } from 'class-transformer'; 26 | import { postStatus } from '../enums/postStatus.enum'; 27 | import { postType } from '../enums/postType.enum'; 28 | 29 | export class CreatePostDto { 30 | @ApiProperty({ 31 | example: 'This is a title', 32 | description: 'This is the title for the blog post', 33 | }) 34 | @IsString() 35 | @MinLength(4) 36 | @MaxLength(512) 37 | @IsNotEmpty() 38 | title: string; 39 | 40 | @ApiProperty({ 41 | enum: postType, 42 | description: "Possible values, 'post', 'page', 'story', 'series'", 43 | }) 44 | @IsEnum(postType) 45 | @IsNotEmpty() 46 | postType: postType; 47 | 48 | @ApiProperty({ 49 | description: "For Example - 'my-url'", 50 | example: 'my-blog-post', 51 | }) 52 | @IsString() 53 | @IsNotEmpty() 54 | @Matches(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, { 55 | message: 56 | 'A slug should be all small letters and uses only "-" and without spaces. For example "my-url"', 57 | }) 58 | @MaxLength(256) 59 | slug: string; 60 | 61 | @ApiProperty({ 62 | enum: postStatus, 63 | description: "Possible values 'draft', 'scheduled', 'review', 'published'", 64 | }) 65 | @IsEnum(postStatus) 66 | @IsNotEmpty() 67 | status: postStatus; 68 | 69 | @ApiPropertyOptional({ 70 | description: 'This is the content of the post', 71 | example: 'The post content', 72 | }) 73 | @IsString() 74 | @IsOptional() 75 | content?: string; 76 | 77 | @ApiPropertyOptional({ 78 | description: 79 | 'Serialize your JSON object else a validation error will be thrown', 80 | example: 81 | '{\r\n "@context": "https://schema.org",\r\n "@type": "Person"\r\n }', 82 | }) 83 | @IsOptional() 84 | @IsJSON() 85 | schema?: string; 86 | 87 | @ApiPropertyOptional({ 88 | description: 'Featured image for your blog post', 89 | example: 'http://localhost.com/images/image1.jpg', 90 | }) 91 | @IsOptional() 92 | @IsUrl() 93 | @MaxLength(1024) 94 | featuredImageUrl?: string; 95 | 96 | @ApiPropertyOptional({ 97 | description: 'The date on which the blog post is published', 98 | example: '2024-03-16T07:46:32+0000', 99 | }) 100 | @IsDate() 101 | @IsOptional() 102 | publishOn?: Date; 103 | 104 | @ApiPropertyOptional({ 105 | description: 'Array of ids of tags', 106 | example: [1, 2], 107 | }) 108 | @IsOptional() 109 | @IsArray() 110 | @IsInt({ each: true }) 111 | tags?: number[]; 112 | 113 | @ApiPropertyOptional({ 114 | type: 'object', 115 | required: false, 116 | items: { 117 | type: 'object', 118 | properties: { 119 | metavalue: { 120 | type: 'json', 121 | description: 'The metaValue is a JSON string', 122 | example: '{"sidebarEnabled": true}', 123 | }, 124 | }, 125 | }, 126 | }) 127 | @IsOptional() 128 | @ValidateNested({ each: true }) 129 | @Type(() => CreatePostMetaOptionsDto) 130 | metaOptions?: CreatePostMetaOptionsDto | null; 131 | } 132 | -------------------------------------------------------------------------------- /src/posts/dtos/get-post.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsDate, IsOptional } from 'class-validator'; 2 | 3 | import { IntersectionType } from '@nestjs/swagger'; 4 | import { PaginationQueryDto } from 'src/common/pagination/dtos/pagination-query.dto'; 5 | import { Type } from 'class-transformer'; 6 | 7 | class GetPostsBaseDto { 8 | @IsDate() 9 | @IsOptional() 10 | startDate?: Date; 11 | 12 | @IsDate() 13 | @IsOptional() 14 | endDate?: Date; 15 | } 16 | 17 | export class GetPostsDto extends IntersectionType( 18 | GetPostsBaseDto, 19 | PaginationQueryDto, 20 | ) {} 21 | -------------------------------------------------------------------------------- /src/posts/dtos/patch-post.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty, PartialType } from '@nestjs/swagger'; 2 | import { IsInt, IsNotEmpty } from 'class-validator'; 3 | 4 | import { CreatePostDto } from './create-post.dto'; 5 | 6 | export class PatchPostDto extends PartialType(CreatePostDto) { 7 | @ApiProperty({ 8 | description: 'The ID of the post that needs to be updated', 9 | }) 10 | @IsInt() 11 | @IsNotEmpty() 12 | id: number; 13 | } 14 | -------------------------------------------------------------------------------- /src/posts/enums/post-status.enum.ts: -------------------------------------------------------------------------------- 1 | export enum postStatus { 2 | DRAFT = 'draft', 3 | SCHEDULED = 'scheduled', 4 | REVIEW = 'review', 5 | PUBLISHED = 'published', 6 | } 7 | -------------------------------------------------------------------------------- /src/posts/enums/post-type.enum.ts: -------------------------------------------------------------------------------- 1 | export enum PostType { 2 | POST = 'post', 3 | PAGE = 'page', 4 | STORY = 'story', 5 | SERIES = 'series', 6 | } 7 | -------------------------------------------------------------------------------- /src/posts/enums/postStatus.enum.ts: -------------------------------------------------------------------------------- 1 | //draft, scheduled, review, published 2 | export enum postStatus { 3 | DRAFT = 'draft', 4 | SCHEDULED = 'scheduled', 5 | REVIEW = 'review', 6 | PUBLISHED = 'published', 7 | } 8 | -------------------------------------------------------------------------------- /src/posts/enums/postType.enum.ts: -------------------------------------------------------------------------------- 1 | export enum postType { 2 | POST = 'post', 3 | PAGE = 'page', 4 | STORY = 'story', 5 | SERIES = 'series', 6 | } 7 | -------------------------------------------------------------------------------- /src/posts/http/posts.delete.endpoints.http: -------------------------------------------------------------------------------- 1 | DELETE http://localhost:3000/posts 2 | ?id=7 -------------------------------------------------------------------------------- /src/posts/http/posts.get.endpoints.http: -------------------------------------------------------------------------------- 1 | {{ 2 | //pre request script 3 | const date = new Date(); 4 | exports.startDate = date.toString(); 5 | exports.endDate = date.toString(); 6 | }} 7 | GET http://localhost:3000/posts/ 8 | ?limit=1 9 | &page=1 -------------------------------------------------------------------------------- /src/posts/http/posts.patch.endpoints.http: -------------------------------------------------------------------------------- 1 | PATCH http://localhost:3000/posts 2 | Content-Type: application/json 3 | 4 | { 5 | "id":5, 6 | "title": "What's new with NestJS With TypeScript", 7 | "postType": "post", 8 | "status": "draft", 9 | "content": "test content", 10 | "metaOptions" : { 11 | "metaValue": "{\"sidebarEnabled\": true, \"footerActive\":true}" 12 | }, 13 | "tags": [1,2] 14 | } -------------------------------------------------------------------------------- /src/posts/http/posts.post.endpoints.http: -------------------------------------------------------------------------------- 1 | {{ 2 | exports.publishOn = new Date().toString() 3 | }} 4 | POST http://localhost:3000/posts 5 | Content-Type: application/json 6 | Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjE5LCJlbWFpbCI6Im1hcmtAZG9lLmNvbSIsImlhdCI6MTcxNzc2MTUyMSwiZXhwIjoxNzE3NzY1MTIxLCJhdWQiOiJsb2NhbGhvc3Q6MzAwMCIsImlzcyI6ImxvY2FsaG9zdDozMDAwIn0.6FENG3lIPBvph6Jxewg1C_xJKe1ChLYF4GnXHJG9yMY 7 | 8 | { 9 | "title": "What's new with NestJS", 10 | "postType": "post", 11 | "slug": "new-with-nestjs-4", 12 | "status": "draft", 13 | "content": "test content", 14 | "schema": "{\r\n \"@context\": \"https:\/\/schema.org\",\r\n \"@type\": \"Person\"\r\n }", 15 | "featuredImageUrl": "http://localhost.com/images/image1.jpg", 16 | "publishOn": "{{publishOn}}", 17 | //"tags": ["nestjs", "typescript"], 18 | "metaOptions" : { 19 | "metaValue": "{\"sidebarEnabled\": true, \"footerActive\":true}" 20 | }, 21 | "tags": [1,2] 22 | } -------------------------------------------------------------------------------- /src/posts/post.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Column, 3 | Entity, 4 | JoinTable, 5 | ManyToMany, 6 | ManyToOne, 7 | OneToOne, 8 | PrimaryGeneratedColumn, 9 | } from 'typeorm'; 10 | 11 | import { CreatePostMetaOptionsDto } from '../meta-options/dtos/create-post-meta-options.dto'; 12 | import { MetaOption } from 'src/meta-options/meta-option.entity'; 13 | import { Tag } from 'src/tags/tag.entity'; 14 | import { User } from 'src/users/user.entity'; 15 | import { postStatus } from './enums/postStatus.enum'; 16 | import { postType } from './enums/postType.enum'; 17 | 18 | @Entity() 19 | export class Post { 20 | @PrimaryGeneratedColumn() 21 | id: number; 22 | 23 | @Column({ 24 | type: 'varchar', 25 | length: 512, 26 | nullable: false, 27 | }) 28 | title: string; 29 | 30 | @Column({ 31 | type: 'enum', 32 | enum: postType, 33 | nullable: false, 34 | default: postType.POST, 35 | }) 36 | postType: postType; 37 | 38 | @Column({ 39 | type: 'varchar', 40 | length: 256, 41 | nullable: false, 42 | unique: true, 43 | }) 44 | slug: string; 45 | 46 | @Column({ 47 | type: 'enum', 48 | enum: postStatus, 49 | nullable: false, 50 | default: postStatus.DRAFT, 51 | }) 52 | status: postStatus; 53 | 54 | @Column({ 55 | type: 'text', 56 | nullable: true, 57 | }) 58 | content?: string; 59 | 60 | @Column({ 61 | type: 'text', 62 | nullable: true, 63 | }) 64 | schema?: string; 65 | 66 | @Column({ 67 | type: 'varchar', 68 | length: 1024, 69 | nullable: true, 70 | }) 71 | featuredImageUrl?: string; 72 | 73 | @Column({ 74 | type: 'timestamp', // 'datetime' in mysql 75 | nullable: true, 76 | }) 77 | publishOn?: Date; 78 | 79 | @OneToOne(() => MetaOption, (metaOptions) => metaOptions.post, { 80 | cascade: true, 81 | eager: true, 82 | }) 83 | metaOptions?: MetaOption; 84 | 85 | @ManyToOne(() => User, (user) => user.posts, { 86 | eager: true, 87 | }) 88 | author: User; 89 | 90 | @ManyToMany(() => Tag, (tag) => tag.posts, { 91 | eager: true, 92 | }) 93 | @JoinTable() 94 | tags?: Tag[]; 95 | } 96 | -------------------------------------------------------------------------------- /src/posts/posts.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Delete, 5 | Get, 6 | Param, 7 | ParseIntPipe, 8 | Patch, 9 | Post, 10 | Query, 11 | } from '@nestjs/common'; 12 | import { PostsService } from './providers/posts.service'; 13 | import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; 14 | import { CreatePostDto } from './dtos/create-post.dto'; 15 | import { PatchPostDto } from './dtos/patch-post.dto'; 16 | import { GetPostsDto } from './dtos/get-post.dto'; 17 | import { ActiveUser } from 'src/auth/decorators/active-user.decorator'; 18 | import { ActiveUserData } from 'src/auth/interfaces/active-user-data.interface'; 19 | 20 | @Controller('posts') 21 | @ApiTags('Posts') 22 | export class PostsController { 23 | constructor( 24 | /* 25 | * Injecting Posts Service 26 | */ 27 | private readonly postsService: PostsService, 28 | ) {} 29 | 30 | /* 31 | * GET localhost:3000/posts/:userId 32 | */ 33 | @Get('/:userId?') 34 | public getPosts( 35 | @Param('userId') userId: string, 36 | @Query() postQuery: GetPostsDto, 37 | ) { 38 | return this.postsService.findAll(postQuery, userId); 39 | } 40 | 41 | @ApiOperation({ 42 | summary: 'Creates a new blog post', 43 | }) 44 | @ApiResponse({ 45 | status: 201, 46 | description: 'You get a 201 response if your post is created successfully', 47 | }) 48 | @Post() 49 | public createPost( 50 | @Body() createPostDto: CreatePostDto, 51 | @ActiveUser() user: ActiveUserData, 52 | ) { 53 | console.log(user); 54 | return this.postsService.create(createPostDto, user); 55 | } 56 | 57 | @ApiOperation({ 58 | summary: 'Updates an existing blog post', 59 | }) 60 | @ApiResponse({ 61 | status: 200, 62 | description: 'A 200 response if the post is updated successfully', 63 | }) 64 | @Patch() 65 | public updatePost(@Body() patchPostDto: PatchPostDto) { 66 | return this.postsService.update(patchPostDto); 67 | } 68 | 69 | @Delete() 70 | public deletePost(@Query('id', ParseIntPipe) id: number) { 71 | return this.postsService.delete(id); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/posts/posts.module.ts: -------------------------------------------------------------------------------- 1 | import { MetaOption } from 'src/meta-options/meta-option.entity'; 2 | import { Module } from '@nestjs/common'; 3 | import { PaginationModule } from 'src/common/pagination/pagination.module'; 4 | import { Post } from './post.entity'; 5 | import { PostsController } from './posts.controller'; 6 | import { PostsService } from './providers/posts.service'; 7 | import { TagsModule } from 'src/tags/tags.module'; 8 | import { TypeOrmModule } from '@nestjs/typeorm'; 9 | import { UsersModule } from 'src/users/users.module'; 10 | import { CreatePostProvider } from './providers/create-post.provider'; 11 | 12 | @Module({ 13 | controllers: [PostsController], 14 | providers: [PostsService, CreatePostProvider], 15 | imports: [ 16 | UsersModule, 17 | TagsModule, 18 | PaginationModule, 19 | TypeOrmModule.forFeature([Post, MetaOption]), 20 | ], 21 | }) 22 | export class PostsModule {} 23 | -------------------------------------------------------------------------------- /src/posts/providers/create-post.provider.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BadRequestException, 3 | Body, 4 | ConflictException, 5 | Injectable, 6 | } from '@nestjs/common'; 7 | import { CreatePostDto } from '../dtos/create-post.dto'; 8 | import { UsersService } from 'src/users/providers/users.service'; 9 | import { Repository } from 'typeorm'; 10 | import { Post } from '../post.entity'; 11 | import { InjectRepository } from '@nestjs/typeorm'; 12 | import { TagsService } from 'src/tags/providers/tags.service'; 13 | import { ActiveUserData } from 'src/auth/interfaces/active-user-data.interface'; 14 | 15 | @Injectable() 16 | export class CreatePostProvider { 17 | constructor( 18 | /* 19 | * Injecting Users Service 20 | */ 21 | private readonly usersService: UsersService, 22 | /** 23 | * Inject postsRepository 24 | */ 25 | @InjectRepository(Post) 26 | private readonly postsRepository: Repository, 27 | /** 28 | * Inject TagsService 29 | */ 30 | private readonly tagsService: TagsService, 31 | ) {} 32 | 33 | public async create(createPostDto: CreatePostDto, user: ActiveUserData) { 34 | let author = undefined; 35 | let tags = undefined; 36 | 37 | try { 38 | // Find author from database based on authorId 39 | author = await this.usersService.findOneById(user.sub); 40 | // Find tags 41 | tags = await this.tagsService.findMultipleTags(createPostDto.tags); 42 | } catch (error) { 43 | throw new ConflictException(error); 44 | } 45 | 46 | if (createPostDto.tags.length !== tags.length) { 47 | throw new BadRequestException('Please check your tag Ids'); 48 | } 49 | 50 | // Create post 51 | let post = this.postsRepository.create({ 52 | ...createPostDto, 53 | author: author, 54 | tags: tags, 55 | }); 56 | 57 | try { 58 | // return the post 59 | return await this.postsRepository.save(post); 60 | } catch (error) { 61 | throw new ConflictException(error, { 62 | description: 'Ensure post slug is unique and not a duplicate', 63 | }); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/posts/providers/posts.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BadRequestException, 3 | Body, 4 | Injectable, 5 | RequestTimeoutException, 6 | } from '@nestjs/common'; 7 | import { UsersService } from 'src/users/providers/users.service'; 8 | import { CreatePostDto } from '../dtos/create-post.dto'; 9 | import { Repository } from 'typeorm'; 10 | import { Post } from '../post.entity'; 11 | import { InjectRepository } from '@nestjs/typeorm'; 12 | import { MetaOption } from 'src/meta-options/meta-option.entity'; 13 | import { TagsService } from 'src/tags/providers/tags.service'; 14 | import { PatchPostDto } from '../dtos/patch-post.dto'; 15 | import { GetPostsDto } from '../dtos/get-post.dto'; 16 | import { PaginationProvider } from 'src/common/pagination/providers/pagination.provider'; 17 | import { Paginated } from 'src/common/pagination/interfaces/paginated.interface'; 18 | import { ActiveUserData } from 'src/auth/interfaces/active-user-data.interface'; 19 | import { CreatePostProvider } from './create-post.provider'; 20 | 21 | @Injectable() 22 | export class PostsService { 23 | constructor( 24 | /** 25 | * Inject postsRepository 26 | */ 27 | @InjectRepository(Post) 28 | private readonly postsRepository: Repository, 29 | /** 30 | * inject metaOptionsRepository 31 | */ 32 | @InjectRepository(MetaOption) 33 | private readonly metaOptionsRepository: Repository, 34 | /** 35 | * Inject TagsService 36 | */ 37 | private readonly tagsService: TagsService, 38 | /** 39 | * Inject the paginationProvider 40 | */ 41 | private readonly paginationProvider: PaginationProvider, 42 | /** 43 | * Inject createPostProvider 44 | */ 45 | private readonly createPostProvider: CreatePostProvider, 46 | ) {} 47 | 48 | /** 49 | * Creating new posts 50 | */ 51 | public async create(createPostDto: CreatePostDto, user: ActiveUserData) { 52 | return await this.createPostProvider.create(createPostDto, user); 53 | } 54 | 55 | public async findAll( 56 | postQuery: GetPostsDto, 57 | userId: string, 58 | ): Promise> { 59 | let posts = await this.paginationProvider.paginateQuery( 60 | { 61 | limit: postQuery.limit, 62 | page: postQuery.page, 63 | }, 64 | this.postsRepository, 65 | ); 66 | 67 | return posts; 68 | } 69 | 70 | public async update(patchPostDto: PatchPostDto) { 71 | let tags = undefined; 72 | let post = undefined; 73 | 74 | // Find the Tags 75 | try { 76 | tags = await this.tagsService.findMultipleTags(patchPostDto.tags); 77 | } catch (error) { 78 | throw new RequestTimeoutException( 79 | 'Unable to process your request at the moment please try later', 80 | { 81 | description: 'Error connecting to the database', 82 | }, 83 | ); 84 | } 85 | 86 | /** 87 | * If tags were not found 88 | * Need to be equal number of tags 89 | */ 90 | if (!tags || tags.length !== patchPostDto.tags.length) { 91 | throw new BadRequestException( 92 | 'Please check your tag Ids and ensure they are correct', 93 | ); 94 | } 95 | 96 | // Find the Post 97 | try { 98 | // Returns null if the post does not exist 99 | post = await this.postsRepository.findOneBy({ 100 | id: patchPostDto.id, 101 | }); 102 | } catch (error) { 103 | throw new RequestTimeoutException( 104 | 'Unable to process your request at the moment please try later', 105 | { 106 | description: 'Error connecting to the database', 107 | }, 108 | ); 109 | } 110 | 111 | if (!post) { 112 | throw new BadRequestException('The post Id does not exist'); 113 | } 114 | 115 | // Update the properties 116 | post.title = patchPostDto.title ?? post.title; 117 | post.content = patchPostDto.content ?? post.content; 118 | post.status = patchPostDto.status ?? post.status; 119 | post.postType = patchPostDto.postType ?? post.postType; 120 | post.slug = patchPostDto.slug ?? post.slug; 121 | post.featuredImageUrl = 122 | patchPostDto.featuredImageUrl ?? post.featuredImageUrl; 123 | post.publishOn = patchPostDto.publishOn ?? post.publishOn; 124 | 125 | // Assign the new tags 126 | post.tags = tags; 127 | 128 | // Save the post and return 129 | try { 130 | await this.postsRepository.save(post); 131 | } catch (error) { 132 | throw new RequestTimeoutException( 133 | 'Unable to process your request at the moment please try later', 134 | { 135 | description: 'Error connecting to the database', 136 | }, 137 | ); 138 | } 139 | return post; 140 | } 141 | 142 | public async delete(id: number) { 143 | // Deleting the post 144 | await this.postsRepository.delete(id); 145 | // confirmation 146 | return { deleted: true, id }; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/tags/dtos/create-tag.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; 2 | import { 3 | IsJSON, 4 | IsNotEmpty, 5 | IsOptional, 6 | IsString, 7 | IsUrl, 8 | Matches, 9 | MaxLength, 10 | MinLength, 11 | } from 'class-validator'; 12 | 13 | export class CreateTagDto { 14 | @ApiProperty() 15 | @IsString() 16 | @MinLength(3) 17 | @IsNotEmpty() 18 | @MaxLength(256) 19 | name: string; 20 | 21 | @ApiProperty() 22 | @IsString() 23 | @IsNotEmpty() 24 | @Matches(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, { 25 | message: 26 | 'A slug should be all small letters and uses only "-" and without spaces. For example "my-url"', 27 | }) 28 | @MaxLength(512) 29 | slug: string; 30 | 31 | @ApiPropertyOptional() 32 | @IsOptional() 33 | @IsString() 34 | description: string; 35 | 36 | @ApiPropertyOptional() 37 | @IsOptional() 38 | @IsJSON() 39 | schema: string; 40 | 41 | @ApiPropertyOptional() 42 | @IsOptional() 43 | @IsUrl() 44 | @MaxLength(1024) 45 | featuredImage: string; 46 | } 47 | -------------------------------------------------------------------------------- /src/tags/http/tags.delete.endpoints.http: -------------------------------------------------------------------------------- 1 | DELETE http://localhost:3000/tags 2 | ?id=6 3 | 4 | DELETE http://localhost:3000/tags/soft-delete 5 | ?id=7 6 | -------------------------------------------------------------------------------- /src/tags/http/tags.post.endpoints.http: -------------------------------------------------------------------------------- 1 | POST http://localhost:3000/tags 2 | Content-Type: application/json 3 | 4 | { 5 | "name": "javascript", 6 | "slug": "javascript", 7 | "description": "All posts javascript" 8 | } -------------------------------------------------------------------------------- /src/tags/providers/tags.service.ts: -------------------------------------------------------------------------------- 1 | import { In, Repository } from 'typeorm'; 2 | import { CreateTagDto } from '../dtos/create-tag.dto'; 3 | import { Injectable } from '@nestjs/common'; 4 | import { Tag } from '../tag.entity'; 5 | import { InjectRepository } from '@nestjs/typeorm'; 6 | 7 | @Injectable() 8 | export class TagsService { 9 | constructor( 10 | /** 11 | * Inject tagsRepository 12 | */ 13 | @InjectRepository(Tag) 14 | private readonly tagsRepository: Repository, 15 | ) {} 16 | 17 | public async create(createTagDto: CreateTagDto) { 18 | let tag = this.tagsRepository.create(createTagDto); 19 | return await this.tagsRepository.save(tag); 20 | } 21 | 22 | public async findMultipleTags(tags: number[]) { 23 | let results = await this.tagsRepository.find({ 24 | where: { 25 | id: In(tags), 26 | }, 27 | }); 28 | 29 | return results; 30 | } 31 | 32 | public async delete(id: number) { 33 | await this.tagsRepository.delete(id); 34 | 35 | return { 36 | deleted: true, 37 | id, 38 | }; 39 | } 40 | 41 | public async softRemove(id: number) { 42 | await this.tagsRepository.softDelete(id); 43 | 44 | return { 45 | softDeleted: true, 46 | id, 47 | }; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/tags/tag.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Column, 3 | CreateDateColumn, 4 | DeleteDateColumn, 5 | Entity, 6 | ManyToMany, 7 | PrimaryGeneratedColumn, 8 | UpdateDateColumn, 9 | } from 'typeorm'; 10 | 11 | import { Post } from 'src/posts/post.entity'; 12 | 13 | @Entity() 14 | export class Tag { 15 | @PrimaryGeneratedColumn() 16 | id: number; 17 | 18 | @Column({ 19 | type: 'varchar', 20 | length: 256, 21 | nullable: false, 22 | unique: true, 23 | }) 24 | name: string; 25 | 26 | @Column({ 27 | type: 'varchar', 28 | length: 512, 29 | nullable: false, 30 | unique: true, 31 | }) 32 | slug: string; 33 | 34 | @Column({ 35 | type: 'text', 36 | nullable: true, 37 | }) 38 | description: string; 39 | 40 | @Column({ 41 | type: 'text', 42 | nullable: true, 43 | }) 44 | schema: string; 45 | 46 | @Column({ 47 | type: 'varchar', 48 | length: 1024, 49 | nullable: true, 50 | }) 51 | featuredImage: string; 52 | 53 | @ManyToMany(() => Post, (post) => post.tags, { 54 | onDelete: 'CASCADE', 55 | }) 56 | posts: Post[]; 57 | 58 | // https://orkhan.gitbook.io/typeorm/docs/decorator-reference 59 | @CreateDateColumn() 60 | createDate: Date; 61 | 62 | @UpdateDateColumn() 63 | updateDate: Date; 64 | 65 | // Add this decorartor and column enables soft delete 66 | @DeleteDateColumn() 67 | deletedAt: Date; 68 | } 69 | -------------------------------------------------------------------------------- /src/tags/tags.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Delete, 5 | ParseIntPipe, 6 | Post, 7 | Query, 8 | } from '@nestjs/common'; 9 | import { CreateTagDto } from './dtos/create-tag.dto'; 10 | import { TagsService } from './providers/tags.service'; 11 | 12 | @Controller('tags') 13 | export class TagsController { 14 | constructor( 15 | /** 16 | * Inject tagsService 17 | */ 18 | private readonly tagsService: TagsService, 19 | ) {} 20 | @Post() 21 | public create(@Body() createTagDto: CreateTagDto) { 22 | return this.tagsService.create(createTagDto); 23 | } 24 | 25 | @Delete() 26 | public delete(@Query('id', ParseIntPipe) id: number) { 27 | return this.tagsService.delete(id); 28 | } 29 | 30 | @Delete('soft-delete') 31 | public softDelete(@Query('id', ParseIntPipe) id: number) { 32 | return this.tagsService.softRemove(id); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/tags/tags.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { Tag } from './tag.entity'; 3 | import { TagsController } from './tags.controller'; 4 | import { TagsService } from './providers/tags.service'; 5 | import { TypeOrmModule } from '@nestjs/typeorm'; 6 | 7 | @Module({ 8 | controllers: [TagsController], 9 | imports: [TypeOrmModule.forFeature([Tag])], 10 | providers: [TagsService], 11 | exports: [TagsService], 12 | }) 13 | export class TagsModule {} 14 | -------------------------------------------------------------------------------- /src/uploads/enums/file-types.enum.ts: -------------------------------------------------------------------------------- 1 | export enum fileTypes { 2 | IMAGE = 'image', 3 | } 4 | -------------------------------------------------------------------------------- /src/uploads/http/test-image.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manikbajaj/nestjs-intro/1677dad85e26f2cf329395b8848b9bc755ca7e96/src/uploads/http/test-image.jpeg -------------------------------------------------------------------------------- /src/uploads/http/uploads.post.endpoints.http: -------------------------------------------------------------------------------- 1 | POST http://localhost:3000/uploads/file 2 | Content-Type: multipart/form-data; boundary=WebKitFormBoundary 3 | 4 | file 5 | --WebKitFormBoundary 6 | Content-Disposition: form-data; name="file"; filename="test-image.jpeg" 7 | Content-Type: image/jpg 8 | 9 | < ./test-image.jpeg 10 | --WebKitFormBoundary-- -------------------------------------------------------------------------------- /src/uploads/interfaces/upload-file.interface.ts: -------------------------------------------------------------------------------- 1 | export interface UploadFile { 2 | name: string; 3 | path: string; 4 | type: string; 5 | mime: string; 6 | size: number; 7 | } 8 | -------------------------------------------------------------------------------- /src/uploads/providers/upload-to-aws.provider.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import { Injectable, RequestTimeoutException } from '@nestjs/common'; 4 | 5 | import { ConfigService } from '@nestjs/config'; 6 | import { Express } from 'express'; 7 | import { S3 } from 'aws-sdk'; 8 | import { v4 as uuidv4 } from 'uuid'; 9 | 10 | @Injectable() 11 | export class UploadToAwsProvider { 12 | constructor(private readonly configService: ConfigService) {} 13 | 14 | public async fileupload(file: Express.Multer.File) { 15 | const s3 = new S3(); 16 | try { 17 | const uploadResult = await s3 18 | .upload({ 19 | Bucket: this.configService.get('appConfig.awsBucketName'), 20 | Body: file.buffer, 21 | Key: this.generateFileName(file), 22 | ContentType: file.mimetype, 23 | }) 24 | .promise(); // Promisify the request 25 | 26 | // Return the file name 27 | return uploadResult.Key; 28 | } catch (error) { 29 | throw new RequestTimeoutException(error); 30 | } 31 | } 32 | 33 | private generateFileName(file: Express.Multer.File) { 34 | // extract file name 35 | let name = file.originalname.split('.')[0]; 36 | // Remove spaces in the file name 37 | name.replace(/\s/g, '').trim(); 38 | // extract file extension 39 | let extension = path.extname(file.originalname); 40 | // Generate a timestamp 41 | let timeStamp = new Date().getTime().toString().trim(); 42 | // Return new fileName 43 | return `${name}-${timeStamp}-${uuidv4()}${extension}`; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/uploads/providers/uploads.service.ts: -------------------------------------------------------------------------------- 1 | import { ConfigService } from '@nestjs/config'; 2 | import { 3 | BadRequestException, 4 | ConflictException, 5 | Injectable, 6 | } from '@nestjs/common'; 7 | import { UploadFile } from '../interfaces/upload-file.interface'; 8 | import { UploadToAwsProvider } from './upload-to-aws.provider'; 9 | import { fileTypes } from '../enums/file-types.enum'; 10 | import { InjectRepository } from '@nestjs/typeorm'; 11 | import { Upload } from '../upload.entity'; 12 | import { Repository } from 'typeorm'; 13 | 14 | @Injectable() 15 | export class UploadsService { 16 | constructor( 17 | /** 18 | * Inject uploadToAwsProvider 19 | */ 20 | private readonly uploadToAwsProvider: UploadToAwsProvider, 21 | /** 22 | * inject configService 23 | */ 24 | private readonly configService: ConfigService, 25 | /** 26 | * inject uploadsRepository 27 | */ 28 | @InjectRepository(Upload) 29 | private uploadsRepository: Repository, 30 | ) {} 31 | public async uploadFile(file: Express.Multer.File) { 32 | // throw error for unsupported file types 33 | if ( 34 | !['image/gif', 'image/jpeg', 'image/jpg', 'image/png'].includes( 35 | file.mimetype, 36 | ) 37 | ) { 38 | throw new BadRequestException('MIME type not supported'); 39 | } 40 | 41 | try { 42 | // Upload file to AWS S3 bucket 43 | const path = await this.uploadToAwsProvider.fileupload(file); 44 | // Generate a new record in upload table 45 | const uploadFile: UploadFile = { 46 | name: path, 47 | path: `https://${this.configService.get('appConfig.awsCloudfrontUrl')}/${path}`, 48 | type: fileTypes.IMAGE, 49 | mime: file.mimetype, 50 | size: file.size, 51 | }; 52 | // create an upload 53 | const upload = this.uploadsRepository.create(uploadFile); 54 | // save the details to database 55 | return await this.uploadsRepository.save(upload); 56 | } catch (error) { 57 | throw new ConflictException(error); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/uploads/upload.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Column, 3 | CreateDateColumn, 4 | DeleteDateColumn, 5 | Entity, 6 | PrimaryGeneratedColumn, 7 | UpdateDateColumn, 8 | } from 'typeorm'; 9 | 10 | import { fileTypes } from './enums/file-types.enum'; 11 | 12 | @Entity() 13 | export class Upload { 14 | @PrimaryGeneratedColumn() 15 | id: number; 16 | 17 | @Column({ 18 | type: 'varchar', 19 | length: 1024, 20 | nullable: false, 21 | }) 22 | name: string; 23 | 24 | @Column({ 25 | type: 'varchar', 26 | length: 1024, 27 | nullable: false, 28 | }) 29 | path: string; 30 | 31 | @Column({ 32 | type: 'enum', 33 | enum: fileTypes, 34 | default: fileTypes.IMAGE, 35 | nullable: false, 36 | }) 37 | type: string; 38 | 39 | @Column({ 40 | type: 'varchar', 41 | length: 128, 42 | nullable: false, 43 | }) 44 | mime: string; 45 | 46 | @Column({ 47 | type: 'varchar', 48 | length: 1024, 49 | nullable: false, 50 | }) 51 | size: number; 52 | 53 | @CreateDateColumn() 54 | createDate: Date; 55 | 56 | @UpdateDateColumn() 57 | updateDate: Date; 58 | } 59 | -------------------------------------------------------------------------------- /src/uploads/uploads.controller.ts: -------------------------------------------------------------------------------- 1 | import { ApiHeaders, ApiOperation } from '@nestjs/swagger'; 2 | import { 3 | Controller, 4 | Post, 5 | UploadedFile, 6 | UseInterceptors, 7 | } from '@nestjs/common'; 8 | import { Express } from 'express'; 9 | import { FileInterceptor } from '@nestjs/platform-express'; 10 | import { UploadsService } from './providers/uploads.service'; 11 | import { Auth } from 'src/auth/decorators/auth.decorator'; 12 | import { AuthType } from 'src/auth/enums/auth-type.enum'; 13 | 14 | @Auth(AuthType.None) 15 | @Controller('uploads') 16 | export class UploadsController { 17 | constructor( 18 | /** 19 | * inject uploadsService 20 | */ 21 | private readonly uploadsService: UploadsService, 22 | ) {} 23 | 24 | // File is the field name 25 | @UseInterceptors(FileInterceptor('file')) 26 | @ApiHeaders([ 27 | { name: 'Content-Type', description: 'multipart/form-data' }, 28 | { name: 'Authorization', description: 'Bearer Token' }, 29 | ]) 30 | @ApiOperation({ 31 | summary: `Upload a new image to the server`, 32 | }) 33 | @Post('file') 34 | public uploadFile(@UploadedFile() file: Express.Multer.File) { 35 | return this.uploadsService.uploadFile(file); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/uploads/uploads.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { Upload } from './upload.entity'; 4 | import { UploadToAwsProvider } from './providers/upload-to-aws.provider'; 5 | import { UploadsController } from './uploads.controller'; 6 | import { UploadsService } from './providers/uploads.service'; 7 | 8 | @Module({ 9 | controllers: [UploadsController], 10 | providers: [UploadsService, UploadToAwsProvider], 11 | imports: [TypeOrmModule.forFeature([Upload])], 12 | }) 13 | export class UploadsModule {} 14 | -------------------------------------------------------------------------------- /src/users/config/profile.config.ts: -------------------------------------------------------------------------------- 1 | import { registerAs } from '@nestjs/config'; 2 | 3 | export default registerAs('profileConfig', () => ({ 4 | apiKey: process.env.PROFILE_API_KEY, 5 | })); 6 | -------------------------------------------------------------------------------- /src/users/dtos/create-many-users.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsArray, IsNotEmpty, ValidateNested } from 'class-validator'; 2 | 3 | import { ApiProperty } from '@nestjs/swagger'; 4 | import { CreateUserDto } from './create-user.dto'; 5 | import { Type } from 'class-transformer'; 6 | 7 | export class CreateManyUsersDto { 8 | @ApiProperty({ 9 | type: 'array', 10 | required: true, 11 | items: { 12 | type: 'User', 13 | }, 14 | }) 15 | @IsNotEmpty() 16 | @IsArray() 17 | @ValidateNested({ each: true }) 18 | @Type(() => CreateUserDto) 19 | users: CreateUserDto[]; 20 | } 21 | -------------------------------------------------------------------------------- /src/users/dtos/create-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IsEmail, 3 | IsNotEmpty, 4 | IsOptional, 5 | IsString, 6 | Matches, 7 | MaxLength, 8 | MinLength, 9 | } from 'class-validator'; 10 | 11 | export class CreateUserDto { 12 | @IsString() 13 | @IsNotEmpty() 14 | @MinLength(3) 15 | @MaxLength(96) 16 | firstName: string; 17 | 18 | @IsString() 19 | @IsOptional() 20 | @MinLength(3) 21 | @MaxLength(96) 22 | lastName?: string; 23 | 24 | @IsEmail() 25 | @IsNotEmpty() 26 | @MaxLength(96) 27 | email: string; 28 | 29 | @IsString() 30 | @IsNotEmpty() 31 | @MinLength(8) 32 | @MaxLength(96) 33 | @Matches(/^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/, { 34 | message: 35 | 'Minimum eight characters, at least one letter, one number and one special character', 36 | }) 37 | password: string; 38 | } 39 | -------------------------------------------------------------------------------- /src/users/dtos/get-users-param.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsInt, IsOptional } from 'class-validator'; 2 | 3 | import { ApiPropertyOptional } from '@nestjs/swagger'; 4 | import { Type } from 'class-transformer'; 5 | 6 | export class GetUsersParamDto { 7 | @ApiPropertyOptional({ 8 | description: 'Get user with a specific id', 9 | example: 1234, 10 | }) 11 | @IsOptional() 12 | @IsInt() 13 | @Type(() => Number) 14 | id?: number; 15 | } 16 | -------------------------------------------------------------------------------- /src/users/dtos/patch-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { CreateUserDto } from './create-user.dto'; 2 | import { PartialType } from '@nestjs/swagger'; 3 | 4 | export class PatchUserDto extends PartialType(CreateUserDto) {} 5 | -------------------------------------------------------------------------------- /src/users/http/users.get.endpoints.http: -------------------------------------------------------------------------------- 1 | GET http://localhost:3000/users/?limit=10&page=1 2 | Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjE5LCJlbWFpbCI6Im1hcmtAZG9lLmNvbSIsImlhdCI6MTcxODQzMjk5MiwiZXhwIjoxNzE4NDM2NTkyLCJhdWQiOiJsb2NhbGhvc3Q6MzAwMCIsImlzcyI6ImxvY2FsaG9zdDozMDAwIn0.XXWEkJE2f1l4xJX4H5UzSu7u32GZcoXlFU1Jestxp7M 3 | 4 | -------------------------------------------------------------------------------- /src/users/http/users.patch.endpoints.http: -------------------------------------------------------------------------------- 1 | PATCH http://localhost:3000/users 2 | Content-Type: application/json 3 | 4 | { 5 | "email": "john@email.com", 6 | "password": "Password123#" 7 | } 8 | -------------------------------------------------------------------------------- /src/users/http/users.post.enpoints.http: -------------------------------------------------------------------------------- 1 | POST http://localhost:3000/users 2 | Content-Type: application/json 3 | 4 | { 5 | "firstName": "Mark", 6 | "lastName": "Doe", 7 | "email": "mark15@doe.com", 8 | "password": "Password123#" 9 | } 10 | 11 | 12 | POST http://localhost:3000/users/create-many 13 | Content-Type: application/json 14 | Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjE5LCJlbWFpbCI6Im1hcmtAZG9lLmNvbSIsImlhdCI6MTcxNzQ4NTE5MiwiZXhwIjoxNzE3NDg4NzkyLCJhdWQiOiJsb2NhbGhvc3Q6MzAwMCIsImlzcyI6ImxvY2FsaG9zdDozMDAwIn0.swZSpmMZYkDcQaKBHUN0ity0jsLK2ejhYMdpyTNTmHs 15 | 16 | { 17 | "users": [ 18 | { 19 | "firstName": "Mark", 20 | "lastName": "Doe", 21 | "email": "mark@doe.com", 22 | "password": "Password123#" 23 | }, 24 | { 25 | "firstName": "Tom", 26 | "lastName": "Doe", 27 | "email": "tom@doe.com", 28 | "password": "Password123#" 29 | } 30 | ] 31 | } 32 | 33 | -------------------------------------------------------------------------------- /src/users/interfaces/google-user.inerface.ts: -------------------------------------------------------------------------------- 1 | export interface GoogleUser { 2 | email: string; 3 | firstName: string; 4 | lastName: string; 5 | googleId: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/users/providers/create-google-user.provider.ts: -------------------------------------------------------------------------------- 1 | import { ConflictException, Injectable } from '@nestjs/common'; 2 | import { Repository } from 'typeorm'; 3 | import { User } from '../user.entity'; 4 | import { InjectRepository } from '@nestjs/typeorm'; 5 | import { GoogleUser } from '../interfaces/google-user.inerface'; 6 | 7 | @Injectable() 8 | export class CreateGoogleUserProvider { 9 | constructor( 10 | /** 11 | * Inject usersRepository 12 | */ 13 | @InjectRepository(User) 14 | private readonly usersRepository: Repository, 15 | ) {} 16 | 17 | public async createGoogleUser(googleUser: GoogleUser) { 18 | try { 19 | const user = this.usersRepository.create({ 20 | firstName: googleUser.firstName, 21 | lastName: googleUser.lastName, 22 | googleId: googleUser.googleId, 23 | email: googleUser.email, 24 | }); 25 | return await this.usersRepository.save(user); 26 | } catch (error) { 27 | throw new ConflictException(error, { 28 | description: 'Could not create a new user', 29 | }); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/users/providers/create-user.provider.spec.ts: -------------------------------------------------------------------------------- 1 | import { DataSource, Repository } from 'typeorm'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | 4 | import { BadRequestException } from '@nestjs/common'; 5 | import { CreateUserProvider } from './create-user.provider'; 6 | import { HashingProvider } from 'src/auth/providers/hashing.provider'; 7 | import { MailService } from 'src/mail/providers/mail.service'; 8 | import { User } from '../user.entity'; 9 | import { getRepositoryToken } from '@nestjs/typeorm'; 10 | 11 | type MockRepository = Partial, jest.Mock>>; 12 | const createMockRepository = (): MockRepository => ({ 13 | findOne: jest.fn(), 14 | create: jest.fn(), 15 | save: jest.fn(), 16 | }); 17 | 18 | describe('CreateUserProvider', () => { 19 | let provider: CreateUserProvider; 20 | let usersRepository: MockRepository; 21 | const user = { 22 | firstName: 'John', 23 | lastName: 'Doe', 24 | email: 'john@doe.com', 25 | password: 'password', 26 | }; 27 | 28 | beforeEach(async () => { 29 | const module: TestingModule = await Test.createTestingModule({ 30 | providers: [ 31 | CreateUserProvider, 32 | { provide: DataSource, useValue: {} }, 33 | { provide: getRepositoryToken(User), useValue: createMockRepository() }, 34 | { 35 | provide: HashingProvider, 36 | useValue: { hashPassword: jest.fn(() => user.password) }, 37 | }, 38 | { 39 | provide: MailService, 40 | useValue: { sendUserWelcome: jest.fn(() => Promise.resolve()) }, 41 | }, 42 | ], 43 | }).compile(); 44 | 45 | provider = module.get(CreateUserProvider); 46 | usersRepository = module.get(getRepositoryToken(User)); 47 | }); 48 | 49 | it('Should Be Defined', () => { 50 | expect(provider).toBeDefined(); 51 | }); 52 | 53 | describe('createUser', () => { 54 | describe('When User Does Not Exist', () => { 55 | it('Should create a new user', async () => { 56 | usersRepository.findOne.mockResolvedValue(null); 57 | usersRepository.create.mockReturnValue(user); 58 | usersRepository.save.mockResolvedValue(user); 59 | 60 | const newUser = await provider.createUser(user); 61 | 62 | expect(usersRepository.findOne).toHaveBeenCalledWith({ 63 | where: { email: user.email }, 64 | }); 65 | expect(usersRepository.create).toHaveBeenCalledWith(user); 66 | expect(usersRepository.save).toHaveBeenCalledWith(user); 67 | }); 68 | }); 69 | describe('When Same User Exist', () => { 70 | it('Should Throw BadRequestException', async () => { 71 | usersRepository.findOne.mockResolvedValue(user.email); 72 | usersRepository.create.mockReturnValue(user); 73 | usersRepository.save.mockResolvedValue(user); 74 | try { 75 | const newUser = await provider.createUser(user); 76 | } catch (error) { 77 | expect(error).toBeInstanceOf(BadRequestException); 78 | } 79 | }); 80 | }); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /src/users/providers/create-user.provider.ts: -------------------------------------------------------------------------------- 1 | import { BcryptProvider } from './../../auth/providers/bcrypt.provider'; 2 | import { 3 | BadRequestException, 4 | Inject, 5 | Injectable, 6 | RequestTimeoutException, 7 | forwardRef, 8 | } from '@nestjs/common'; 9 | import { CreateUserDto } from '../dtos/create-user.dto'; 10 | import { InjectRepository } from '@nestjs/typeorm'; 11 | import { User } from '../user.entity'; 12 | import { Repository } from 'typeorm'; 13 | import { HashingProvider } from 'src/auth/providers/hashing.provider'; 14 | import { MailService } from 'src/mail/providers/mail.service'; 15 | 16 | @Injectable() 17 | export class CreateUserProvider { 18 | constructor( 19 | /** 20 | * Injecting usersRepository 21 | */ 22 | @InjectRepository(User) 23 | private usersRepository: Repository, 24 | 25 | /** 26 | * Inject BCrypt Provider 27 | */ 28 | @Inject(forwardRef(() => HashingProvider)) 29 | private readonly hashingProvider: HashingProvider, 30 | 31 | /** 32 | * Inject mailService 33 | */ 34 | private readonly mailService: MailService, 35 | ) {} 36 | 37 | public async createUser(createUserDto: CreateUserDto) { 38 | let existingUser = undefined; 39 | 40 | try { 41 | // Check is user exists with same email 42 | existingUser = await this.usersRepository.findOne({ 43 | where: { email: createUserDto.email }, 44 | }); 45 | } catch (error) { 46 | // Might save the details of the exception 47 | // Information which is sensitive 48 | throw new RequestTimeoutException( 49 | 'Unable to process your request at the moment please try later', 50 | { 51 | description: 'Error connecting to the database', 52 | }, 53 | ); 54 | } 55 | 56 | // Handle exception 57 | if (existingUser) { 58 | throw new BadRequestException( 59 | 'The user already exists, please check your email.', 60 | ); 61 | } 62 | 63 | // Create a new user 64 | let newUser = this.usersRepository.create({ 65 | ...createUserDto, 66 | password: await this.hashingProvider.hashPassword(createUserDto.password), 67 | }); 68 | 69 | try { 70 | newUser = await this.usersRepository.save(newUser); 71 | } catch (error) { 72 | throw new RequestTimeoutException( 73 | 'Unable to process your request at the moment please try later', 74 | { 75 | description: 'Error connecting to the the datbase', 76 | }, 77 | ); 78 | } 79 | 80 | try { 81 | await this.mailService.sendUserWelcome(newUser); 82 | } catch (error) { 83 | throw new RequestTimeoutException(error); 84 | } 85 | 86 | return newUser; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/users/providers/find-one-by-google-id.provider.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { User } from '../user.entity'; 4 | import { Repository } from 'typeorm'; 5 | 6 | @Injectable() 7 | export class FindOneByGoogleIdProvider { 8 | constructor( 9 | /** 10 | * Inject usersRepository 11 | */ 12 | @InjectRepository(User) 13 | private readonly usersRepository: Repository, 14 | ) {} 15 | 16 | public async findOneByGoogleId(googleId: string) { 17 | return await this.usersRepository.findOneBy({ googleId }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/users/providers/find-one-user-by-email.provider.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Injectable, 3 | RequestTimeoutException, 4 | UnauthorizedException, 5 | } from '@nestjs/common'; 6 | import { Repository } from 'typeorm'; 7 | import { User } from '../user.entity'; 8 | import { InjectRepository } from '@nestjs/typeorm'; 9 | 10 | @Injectable() 11 | export class FindOneUserByEmailProvider { 12 | constructor( 13 | /** 14 | * Inject usersRepository 15 | */ 16 | @InjectRepository(User) 17 | private readonly usersRepository: Repository, 18 | ) {} 19 | 20 | public async findOneByEmail(email: string) { 21 | let user: User | undefined = undefined; 22 | 23 | try { 24 | // This will return null if the user is not found 25 | user = await this.usersRepository.findOneBy({ 26 | email: email, 27 | }); 28 | } catch (error) { 29 | throw new RequestTimeoutException(error, { 30 | description: 'Could not fetch the user', 31 | }); 32 | } 33 | 34 | if (!user) { 35 | throw new UnauthorizedException('User does not exists'); 36 | } 37 | 38 | return user; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/users/providers/users-create-many.provider.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ConflictException, 3 | Injectable, 4 | RequestTimeoutException, 5 | } from '@nestjs/common'; 6 | 7 | import { CreateManyUsersDto } from '../dtos/create-many-users.dto'; 8 | import { DataSource } from 'typeorm'; 9 | import { User } from '../user.entity'; 10 | 11 | @Injectable() 12 | export class UsersCreateManyProvider { 13 | constructor( 14 | /** 15 | * Inject the datasource 16 | */ 17 | private dataSource: DataSource, 18 | ) {} 19 | 20 | public async createMany(createManyUsersDto: CreateManyUsersDto) { 21 | let newUsers: User[] = []; 22 | 23 | // Create Query Runner Instance 24 | const queryRunner = this.dataSource.createQueryRunner(); 25 | 26 | try { 27 | // Connect the query ryunner to the datasource 28 | await queryRunner.connect(); 29 | // Start the transaction 30 | await queryRunner.startTransaction(); 31 | } catch (error) { 32 | throw new RequestTimeoutException('Could not connect to the database'); 33 | } 34 | 35 | try { 36 | for (let user of createManyUsersDto.users) { 37 | let newUser = queryRunner.manager.create(User, user); 38 | let result = await queryRunner.manager.save(newUser); 39 | newUsers.push(result); 40 | } 41 | await queryRunner.commitTransaction(); 42 | } catch (error) { 43 | // since we have errors lets rollback the changes we made 44 | await queryRunner.rollbackTransaction(); 45 | throw new ConflictException('Could not complete the transaction', { 46 | description: String(error), 47 | }); 48 | } finally { 49 | try { 50 | // you need to release a queryRunner which was manually instantiated 51 | await queryRunner.release(); 52 | } catch (error) { 53 | throw new RequestTimeoutException( 54 | 'Could not release the query runner connection', 55 | ); 56 | } 57 | } 58 | 59 | return newUsers; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/users/providers/users.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | 3 | import { CreateGoogleUserProvider } from './create-google-user.provider'; 4 | import { CreateUserDto } from '../dtos/create-user.dto'; 5 | import { CreateUserProvider } from './create-user.provider'; 6 | import { DataSource } from 'typeorm'; 7 | import { FindOneByGoogleIdProvider } from './find-one-by-google-id.provider'; 8 | import { FindOneUserByEmailProvider } from './find-one-user-by-email.provider'; 9 | import { User } from '../user.entity'; 10 | import { UsersCreateManyProvider } from './users-create-many.provider'; 11 | import { UsersService } from './users.service'; 12 | import { create } from 'domain'; 13 | import { getRepositoryToken } from '@nestjs/typeorm'; 14 | 15 | describe('UsersService', () => { 16 | let service: UsersService; 17 | 18 | beforeEach(async () => { 19 | const mockCreateUsersProvider: Partial = { 20 | createUser: (createUserDto: CreateUserDto) => 21 | Promise.resolve({ 22 | id: 12, 23 | firstName: createUserDto.firstName, 24 | lastName: createUserDto.lastName, 25 | email: createUserDto.email, 26 | password: createUserDto.password, 27 | }), 28 | }; 29 | 30 | const module: TestingModule = await Test.createTestingModule({ 31 | providers: [ 32 | UsersService, 33 | { provide: DataSource, useValue: {} }, 34 | { provide: getRepositoryToken(User), useValue: {} }, 35 | { provide: CreateUserProvider, useValue: mockCreateUsersProvider }, 36 | { provide: UsersCreateManyProvider, useValue: {} }, 37 | { provide: FindOneUserByEmailProvider, useValue: {} }, 38 | { provide: FindOneByGoogleIdProvider, useValue: {} }, 39 | { provide: CreateGoogleUserProvider, useValue: {} }, 40 | ], 41 | }).compile(); 42 | 43 | service = module.get(UsersService); 44 | }); 45 | 46 | it('Should be defined', () => { 47 | expect(service).toBeDefined(); 48 | }); 49 | 50 | describe('createUser', () => { 51 | it('Should be defined', () => { 52 | expect(service.createUser).toBeDefined(); 53 | }); 54 | 55 | it('Should call createUser on createUserProvider', async () => { 56 | let user = await service.createUser({ 57 | firstName: 'john', 58 | lastName: 'Doe', 59 | email: 'john@doe.com', 60 | password: 'password', 61 | }); 62 | expect(user.firstName).toEqual('john'); 63 | }); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /src/users/providers/users.service.ts: -------------------------------------------------------------------------------- 1 | import { CreateUserDto } from './../dtos/create-user.dto'; 2 | import { DataSource, Repository } from 'typeorm'; 3 | import { GetUsersParamDto } from '../dtos/get-users-param.dto'; 4 | import { 5 | BadRequestException, 6 | HttpException, 7 | HttpStatus, 8 | Inject, 9 | Injectable, 10 | RequestTimeoutException, 11 | forwardRef, 12 | } from '@nestjs/common'; 13 | import { User } from '../user.entity'; 14 | import { InjectRepository } from '@nestjs/typeorm'; 15 | import { ConfigService, ConfigType } from '@nestjs/config'; 16 | import profileConfig from '../config/profile.config'; 17 | import { UsersCreateManyProvider } from './users-create-many.provider'; 18 | import { CreateManyUsersDto } from '../dtos/create-many-users.dto'; 19 | import { CreateUserProvider } from './create-user.provider'; 20 | import { FindOneUserByEmailProvider } from './find-one-user-by-email.provider'; 21 | import { FindOneByGoogleIdProvider } from './find-one-by-google-id.provider'; 22 | import { CreateGoogleUserProvider } from './create-google-user.provider'; 23 | import { GoogleUser } from '../interfaces/google-user.inerface'; 24 | 25 | /** 26 | * Controller class for '/users' API endpoint 27 | */ 28 | @Injectable() 29 | export class UsersService { 30 | constructor( 31 | /** 32 | * Injecting usersRepository 33 | */ 34 | @InjectRepository(User) 35 | private usersRepository: Repository, 36 | 37 | /** 38 | * Inject UsersCreateMany provider 39 | */ 40 | private readonly usersCreateManyProvider: UsersCreateManyProvider, 41 | /** 42 | * Inject Create Users Provider 43 | */ 44 | private readonly createUserProvider: CreateUserProvider, 45 | 46 | /** 47 | * Inject findOneUserByEmailProvider 48 | */ 49 | private readonly findOneUserByEmailProvider: FindOneUserByEmailProvider, 50 | 51 | /** 52 | * Inject findOneByGoogleIdProvider 53 | */ 54 | private readonly findOneByGoogleIdProvider: FindOneByGoogleIdProvider, 55 | /** 56 | * Inject createGooogleUserProvider 57 | */ 58 | private readonly createGooogleUserProvider: CreateGoogleUserProvider, 59 | ) {} 60 | 61 | /** 62 | * Method to create a new user 63 | */ 64 | public async createUser(createUserDto: CreateUserDto) { 65 | return await this.createUserProvider.createUser(createUserDto); 66 | } 67 | 68 | /** 69 | * Public method responsible for handling GET request for '/users' endpoint 70 | */ 71 | public findAll( 72 | getUserParamDto: GetUsersParamDto, 73 | limt: number, 74 | page: number, 75 | ) { 76 | throw new HttpException( 77 | { 78 | status: HttpStatus.MOVED_PERMANENTLY, 79 | error: 'The API endpoint does not exist', 80 | fileName: 'users.service.ts', 81 | lineNumber: 88, 82 | }, 83 | HttpStatus.MOVED_PERMANENTLY, 84 | { 85 | cause: new Error(), 86 | description: 'Occured because the API endpoint was permanently moved', 87 | }, 88 | ); 89 | } 90 | 91 | /** 92 | * Public method used to find one user using the ID of the user 93 | */ 94 | public async findOneById(id: number) { 95 | let user = undefined; 96 | 97 | try { 98 | user = await this.usersRepository.findOneBy({ 99 | id, 100 | }); 101 | } catch (error) { 102 | throw new RequestTimeoutException( 103 | 'Unable to process your request at the moment please try later', 104 | { 105 | description: 'Error connecting to the the datbase', 106 | }, 107 | ); 108 | } 109 | 110 | /** 111 | * Handle the user does not exist 112 | */ 113 | if (!user) { 114 | throw new BadRequestException('The user id does not exist'); 115 | } 116 | 117 | return user; 118 | } 119 | 120 | public async createMany(createManyUsersDto: CreateManyUsersDto) { 121 | return await this.usersCreateManyProvider.createMany(createManyUsersDto); 122 | } 123 | 124 | // Finds one user by email 125 | public async findOneByEmail(email: string) { 126 | return await this.findOneUserByEmailProvider.findOneByEmail(email); 127 | } 128 | 129 | public async findOneByGoogleId(googleId: string) { 130 | return await this.findOneByGoogleIdProvider.findOneByGoogleId(googleId); 131 | } 132 | 133 | public async createGoogleUser(googleUser: GoogleUser) { 134 | return await this.createGooogleUserProvider.createGoogleUser(googleUser); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/users/user.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; 2 | 3 | import { Exclude } from 'class-transformer'; 4 | import { Post } from 'src/posts/post.entity'; 5 | 6 | @Entity() 7 | export class User { 8 | @PrimaryGeneratedColumn() 9 | id: number; 10 | 11 | @Column({ 12 | type: 'varchar', 13 | length: 96, 14 | nullable: false, 15 | }) 16 | firstName: string; 17 | 18 | @Column({ 19 | type: 'varchar', 20 | length: 96, 21 | nullable: true, 22 | }) 23 | lastName: string; 24 | 25 | @Column({ 26 | type: 'varchar', 27 | length: 96, 28 | nullable: false, 29 | unique: true, 30 | }) 31 | email: string; 32 | 33 | @Column({ 34 | type: 'varchar', 35 | length: 96, 36 | nullable: true, 37 | }) 38 | @Exclude() 39 | password?: string; 40 | 41 | @Column({ 42 | type: 'varchar', 43 | nullable: true, 44 | }) 45 | @Exclude() 46 | googleId?: string; 47 | 48 | @OneToMany(() => Post, (post) => post.author) 49 | posts?: Post[]; 50 | } 51 | -------------------------------------------------------------------------------- /src/users/users.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | Param, 5 | Patch, 6 | Post, 7 | Query, 8 | Body, 9 | ParseIntPipe, 10 | DefaultValuePipe, 11 | UseInterceptors, 12 | ClassSerializerInterceptor, 13 | } from '@nestjs/common'; 14 | import { CreateUserDto } from './dtos/create-user.dto'; 15 | import { GetUsersParamDto } from './dtos/get-users-param.dto'; 16 | import { PatchUserDto } from './dtos/patch-user.dto'; 17 | import { UsersService } from './providers/users.service'; 18 | import { ApiTags, ApiQuery, ApiOperation, ApiResponse } from '@nestjs/swagger'; 19 | import { CreateManyUsersDto } from './dtos/create-many-users.dto'; 20 | import { AccessTokenGuard } from 'src/auth/guards/access-token/access-token.guard'; 21 | import { Auth } from 'src/auth/decorators/auth.decorator'; 22 | import { AuthType } from 'src/auth/enums/auth-type.enum'; 23 | 24 | @Controller('users') 25 | @ApiTags('Users') 26 | export class UsersController { 27 | constructor( 28 | // Injecting Users Service 29 | private readonly usersService: UsersService, 30 | ) {} 31 | 32 | @Get('/:id?') 33 | @ApiOperation({ 34 | summary: 'Fetches a list of registered users on the application', 35 | }) 36 | @ApiResponse({ 37 | status: 200, 38 | description: 'Users fetched successfully based on the query', 39 | }) 40 | @ApiQuery({ 41 | name: 'limit', 42 | type: 'number', 43 | required: false, 44 | description: 'The number of entries returned per query', 45 | example: 10, 46 | }) 47 | @ApiQuery({ 48 | name: 'page', 49 | type: 'number', 50 | required: false, 51 | description: 52 | 'The position of the page number that you want the API to return', 53 | example: 1, 54 | }) 55 | public getUsers( 56 | @Param() getUserParamDto: GetUsersParamDto, 57 | @Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number, 58 | @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number, 59 | ) { 60 | return this.usersService.findAll(getUserParamDto, limit, page); 61 | } 62 | 63 | @Post() 64 | // @SetMetadata('authType', 'none') 65 | @UseInterceptors(ClassSerializerInterceptor) 66 | @Auth(AuthType.None) 67 | public createUsers(@Body() createUserDto: CreateUserDto) { 68 | return this.usersService.createUser(createUserDto); 69 | } 70 | 71 | @Post('create-many') 72 | public createManyUsers(@Body() createManyUsersDto: CreateManyUsersDto) { 73 | return this.usersService.createMany(createManyUsersDto); 74 | } 75 | 76 | @Patch() 77 | public patchUser(@Body() patchUserDto: PatchUserDto) { 78 | return patchUserDto; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/users/users.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, forwardRef } from '@nestjs/common'; 2 | 3 | import { APP_GUARD } from '@nestjs/core'; 4 | import { AccessTokenGuard } from 'src/auth/guards/access-token/access-token.guard'; 5 | import { AuthModule } from 'src/auth/auth.module'; 6 | import { ConfigModule } from '@nestjs/config'; 7 | import { CreateUserProvider } from './providers/create-user.provider'; 8 | import { FindOneUserByEmailProvider } from './providers/find-one-user-by-email.provider'; 9 | import { JwtModule } from '@nestjs/jwt'; 10 | import { TypeOrmModule } from '@nestjs/typeorm'; 11 | import { User } from './user.entity'; 12 | import { UsersController } from './users.controller'; 13 | import { UsersCreateManyProvider } from './providers/users-create-many.provider'; 14 | import { UsersService } from './providers/users.service'; 15 | import { FindOneByGoogleIdProvider } from './providers/find-one-by-google-id.provider'; 16 | import { CreateGoogleUserProvider } from './providers/create-google-user.provider'; 17 | import jwtConfig from 'src/auth/config/jwt.config'; 18 | import profileConfig from './config/profile.config'; 19 | 20 | @Module({ 21 | controllers: [UsersController], 22 | providers: [ 23 | UsersService, 24 | UsersCreateManyProvider, 25 | CreateUserProvider, 26 | FindOneUserByEmailProvider, 27 | FindOneByGoogleIdProvider, 28 | CreateGoogleUserProvider, 29 | ], 30 | exports: [UsersService], 31 | imports: [ 32 | TypeOrmModule.forFeature([User]), 33 | ConfigModule.forFeature(profileConfig), 34 | forwardRef(() => AuthModule), 35 | ], 36 | }) 37 | export class UsersModule {} 38 | -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import * as request from 'supertest'; 2 | 3 | import { Test, TestingModule } from '@nestjs/testing'; 4 | 5 | import { AppModule } from '../src/app.module'; 6 | import { INestApplication } from '@nestjs/common'; 7 | 8 | describe('AppController (e2e)', () => { 9 | let app: INestApplication; 10 | 11 | beforeEach(async () => { 12 | const moduleFixture: TestingModule = await Test.createTestingModule({ 13 | imports: [AppModule], 14 | }).compile(); 15 | 16 | app = moduleFixture.createNestApplication(); 17 | await app.init(); 18 | }); 19 | 20 | it('/ (GET)', () => { 21 | console.log(process.env.NODE_ENV); 22 | console.log(process.env.S3_BUCKET); 23 | return request(app.getHttpServer()).get('/').expect(404); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/helpers/bootstrap-nest-application.helper.ts: -------------------------------------------------------------------------------- 1 | import { ConfigModule, ConfigService } from '@nestjs/config'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | 4 | import { AppModule } from '../../src/app.module'; 5 | import { INestApplication } from '@nestjs/common'; 6 | import { appCreate } from '../../src/app.create'; 7 | 8 | export async function bootstrapNestApplication(): Promise { 9 | const moduleFixture: TestingModule = await Test.createTestingModule({ 10 | imports: [AppModule, ConfigModule], 11 | providers: [ConfigService], 12 | }).compile(); 13 | 14 | // Instatiate the app 15 | const app: INestApplication = moduleFixture.createNestApplication(); 16 | await appCreate(app); 17 | await app.init(); 18 | return app; 19 | } 20 | -------------------------------------------------------------------------------- /test/helpers/drop-database.helper.ts: -------------------------------------------------------------------------------- 1 | import { ConfigService } from '@nestjs/config'; 2 | import { DataSource } from 'typeorm'; 3 | 4 | export async function dropDatabase(config: ConfigService): Promise { 5 | const AppDataSource = await new DataSource({ 6 | type: 'postgres', 7 | synchronize: config.get('database.synchronize'), 8 | port: +config.get('database.port'), 9 | username: config.get('database.user'), 10 | password: config.get('database.password'), 11 | host: config.get('database.host'), 12 | database: config.get('database.name'), 13 | }).initialize(); 14 | // Drops All The Tables in the database 15 | await AppDataSource.dropDatabase(); 16 | // Close the connection with the database 17 | await AppDataSource.destroy(); 18 | } 19 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": [ 3 | "js", 4 | "json", 5 | "ts" 6 | ], 7 | "rootDir": "../", 8 | "modulePaths": [ 9 | "" 10 | ], 11 | "testEnvironment": "node", 12 | "testRegex": ".e2e-spec.ts$", 13 | "transform": { 14 | "^.+\\.(t|j)s$": "ts-jest" 15 | } 16 | } -------------------------------------------------------------------------------- /test/users/users.post.e2e-spec.sample-data.ts: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker'; 2 | 3 | export const completeUser = { 4 | firstName: faker.person.firstName(), 5 | lastName: faker.person.lastName(), 6 | email: faker.internet.email(), 7 | password: 'Password123#', 8 | }; 9 | 10 | export const misingFirstName = { 11 | lastName: faker.person.lastName(), 12 | email: faker.internet.email(), 13 | password: 'Password123#', 14 | }; 15 | 16 | export const missingEmail = { 17 | firstName: faker.person.firstName(), 18 | lastName: faker.person.lastName(), 19 | password: 'Password123#', 20 | }; 21 | 22 | export const missingPassword = { 23 | firstName: faker.person.firstName(), 24 | lastName: faker.person.lastName(), 25 | email: faker.internet.email(), 26 | }; 27 | -------------------------------------------------------------------------------- /test/users/users.post.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import * as request from 'supertest'; 2 | 3 | import { 4 | completeUser, 5 | misingFirstName, 6 | missingEmail, 7 | missingPassword, 8 | } from './users.post.e2e-spec.sample-data'; 9 | 10 | import { App } from 'supertest/types'; 11 | import { ConfigService } from '@nestjs/config'; 12 | import { INestApplication } from '@nestjs/common'; 13 | import { bootstrapNestApplication } from 'test/helpers/bootstrap-nest-application.helper'; 14 | import { dropDatabase } from 'test/helpers/drop-database.helper'; 15 | 16 | describe('[Users] @Post Endpoints', () => { 17 | let app: INestApplication; 18 | let config: ConfigService; 19 | let httpServer: App; 20 | 21 | beforeEach(async () => { 22 | // Instantiate the app 23 | app = await bootstrapNestApplication(); 24 | // Get the config 25 | config = app.get(ConfigService); 26 | // get server endpoint 27 | httpServer = app.getHttpServer(); 28 | }); 29 | 30 | afterEach(async () => { 31 | await dropDatabase(config); 32 | await app.close(); 33 | }); 34 | 35 | it('/users - Endpoint is public', () => { 36 | return request(httpServer).post('/users').send({}).expect(400); 37 | }); 38 | 39 | it('/users - firstName is mandatory', () => { 40 | return request(httpServer).post('/users').send(misingFirstName).expect(400); 41 | }); 42 | 43 | it('/users - email is mandatory', () => { 44 | return request(httpServer).post('/users').send(missingEmail).expect(400); 45 | }); 46 | 47 | it('/users - password is mandatory', () => { 48 | return request(httpServer).post('/users').send(missingPassword).expect(400); 49 | }); 50 | 51 | it('/users - Valid request successfully creates user', () => { 52 | return request(httpServer) 53 | .post('/users') 54 | .send(completeUser) 55 | .expect(201) 56 | .then(({ body }) => { 57 | expect(body.data).toBeDefined(); 58 | expect(body.data.firstName).toBe(completeUser.firstName); 59 | expect(body.data.lastName).toBe(completeUser.lastName); 60 | expect(body.data.email).toBe(completeUser.email); 61 | }); 62 | }); 63 | 64 | it('/users - password is not returned in response', () => { 65 | return request(httpServer) 66 | .post('/users') 67 | .send(completeUser) 68 | .expect(201) 69 | .then(({ body }) => { 70 | expect(body.data.password).toBeUndefined(); 71 | }); 72 | }); 73 | 74 | it('/users - googleId is not returned in response', () => { 75 | return request(httpServer) 76 | .post('/users') 77 | .send(completeUser) 78 | .expect(201) 79 | .then(({ body }) => { 80 | expect(body.data.googleId).toBeUndefined(); 81 | }); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /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": "ES2021", 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 | } 21 | } 22 | -------------------------------------------------------------------------------- /typeorm-cli.sample.config.ts: -------------------------------------------------------------------------------- 1 | import { DataSource } from 'typeorm'; 2 | 3 | export default new DataSource({ 4 | type: 'postgres', 5 | host: 'localhost', 6 | port: 5432, 7 | username: 'postgres', 8 | password: 'Password123#', 9 | database: 'nestjs-blog', 10 | entities: ['**/*.entity.js'], 11 | migrations: ['migrations/*.js'], 12 | }); 13 | --------------------------------------------------------------------------------