├── .env.example ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── README.md ├── nest-cli.json ├── ormconfig.ts ├── package-lock.json ├── package.json ├── src ├── crud │ └── validation-group.ts ├── decorators │ ├── session.decorator.ts │ └── user.decorator.ts ├── entities │ ├── base.ts │ ├── media.entity.ts │ ├── post.entity.ts │ ├── session.entity.ts │ ├── user.entity.ts │ └── user_token.entity.ts ├── main.ts ├── modules │ ├── app │ │ └── app.module.ts │ ├── auth-google │ │ ├── google.controller.ts │ │ ├── google.dto.ts │ │ ├── google.module.ts │ │ └── google.service.ts │ ├── auth │ │ ├── auth.module.ts │ │ ├── auth.provider.ts │ │ ├── controllers │ │ │ ├── auth.controller.ts │ │ │ └── email.controller.ts │ │ ├── email.dto.ts │ │ ├── services │ │ │ ├── auth.service.ts │ │ │ └── email.service.ts │ │ └── strategies │ │ │ ├── jwt.strategy.ts │ │ │ └── refresh.strategy.ts │ ├── config │ │ ├── app.config.ts │ │ ├── auth.config.ts │ │ ├── database.config.ts │ │ ├── index.ts │ │ ├── mail.config.ts │ │ └── storage.config.ts │ ├── database │ │ ├── migrations │ │ │ ├── 1712332008837-migrations.ts │ │ │ ├── 1713940122716-migrations.ts │ │ │ ├── 1714030025527-migrations.ts │ │ │ └── 1714141766950-migrations.ts │ │ └── typeorm.factory.ts │ ├── mail │ │ ├── mail.interface.ts │ │ ├── mailerConfig.service.ts │ │ └── templates │ │ │ └── auth │ │ │ └── registration.hbs │ ├── media │ │ ├── local.service.ts │ │ ├── media.controller.ts │ │ ├── media.interface.ts │ │ ├── media.module.ts │ │ ├── media.service.ts │ │ ├── multer_config.service.ts │ │ └── s3.service.ts │ ├── post │ │ ├── post.controller.ts │ │ ├── post.module.ts │ │ └── post.service.ts │ ├── session │ │ ├── session.module.ts │ │ └── session.service.ts │ ├── token │ │ ├── token.module.ts │ │ └── token.service.ts │ └── user │ │ ├── user.controller.ts │ │ ├── user.dto.ts │ │ ├── user.module.ts │ │ └── user.service.ts ├── pagination │ ├── paginate.ts │ ├── pagination-response.ts │ ├── pagination.decorator.ts │ └── pagination.dto.ts ├── pipes │ └── IsIDExist.pipe.ts └── utils │ ├── bootstrap.ts │ ├── serializer.interceptor.ts │ └── validation-options.ts ├── test ├── app.e2e-spec.ts └── jest-e2e.json ├── tsconfig.build.json └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | APP_PORT=8000 3 | APP_NAME="NestJS Series" 4 | FRONTEND_DOMAIN=https://example.com 5 | 6 | # Database Configuration 7 | DATABASE_URL=postgresql://danimai:Danimai@localhost:5432/nest-series 8 | 9 | # Mail confgiuration 10 | MAIL_HOST= 11 | MAIL_PORT=2525 12 | MAIL_USER= 13 | MAIL_PASSWORD= 14 | MAIL_IGNORE_TLS=true 15 | MAIL_SECURE=false 16 | MAIL_REQUIRE_TLS=false 17 | MAIL_DEFAULT_EMAIL=noreply@example.com 18 | MAIL_DEFAULT_NAME=Danimai 19 | 20 | # JWT 21 | JWT_SECRET=random-gibberish-token-for-jwt 22 | JWT_REFRESH_TOKEN_EXPIRES_IN=90d 23 | JWT_ACCESS_TOKEN_EXPIRES_IN=10m 24 | 25 | # STORAGE 26 | STORAGE_TYPE=S3 27 | AWS_ACCESS_KEY_ID= 28 | AWS_SECRET_KEY= 29 | AWS_REGION= 30 | AWS_BUCKET= 31 | 32 | # Google auth 33 | GOOGLE_CLIENT_ID= 34 | GOOGLE_CLIENT_SECRET= -------------------------------------------------------------------------------- /.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 | "prettier/prettier": ["error", { "endOfLine": "auto" }] 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | /build 5 | /media 6 | 7 | # Logs 8 | logs 9 | *.log 10 | npm-debug.log* 11 | pnpm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | lerna-debug.log* 15 | 16 | # OS 17 | .DS_Store 18 | 19 | # Tests 20 | /coverage 21 | /.nyc_output 22 | 23 | # IDEs and editors 24 | /.idea 25 | .project 26 | .classpath 27 | .c9/ 28 | *.launch 29 | .settings/ 30 | *.sublime-workspace 31 | 32 | # IDE - VSCode 33 | .vscode/* 34 | !.vscode/settings.json 35 | !.vscode/tasks.json 36 | !.vscode/launch.json 37 | !.vscode/extensions.json 38 | 39 | # dotenv environment variable files 40 | .env 41 | .env.development.local 42 | .env.test.local 43 | .env.production.local 44 | .env.local 45 | 46 | # temp directory 47 | .temp 48 | .tmp 49 | 50 | # Runtime data 51 | pids 52 | *.pid 53 | *.seed 54 | *.pid.lock 55 | 56 | # Diagnostic reports (https://nodejs.org/api/report.html) 57 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 58 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Nest Logo 3 |

4 | 5 | [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 6 | [circleci-url]: https://circleci.com/gh/nestjs/nest 7 | 8 |

A progressive Node.js framework for building efficient and scalable server-side applications.

9 |

10 | NPM Version 11 | Package License 12 | NPM Downloads 13 | CircleCI 14 | Coverage 15 | Discord 16 | Backers on Open Collective 17 | Sponsors on Open Collective 18 | 19 | Support us 20 | 21 |

22 | 24 | 25 | ## Description 26 | 27 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. 28 | 29 | ## Installation 30 | 31 | ```bash 32 | $ npm install 33 | ``` 34 | 35 | ## Running the app 36 | 37 | ```bash 38 | # development 39 | $ npm run start 40 | 41 | # watch mode 42 | $ npm run start:dev 43 | 44 | # production mode 45 | $ npm run start:prod 46 | ``` 47 | 48 | ## Test 49 | 50 | ```bash 51 | # unit tests 52 | $ npm run test 53 | 54 | # e2e tests 55 | $ npm run test:e2e 56 | 57 | # test coverage 58 | $ npm run test:cov 59 | ``` 60 | 61 | ## Support 62 | 63 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). 64 | 65 | ## Stay in touch 66 | 67 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) 68 | - Website - [https://nestjs.com](https://nestjs.com/) 69 | - Twitter - [@nestframework](https://twitter.com/nestframework) 70 | 71 | ## License 72 | 73 | Nest is [MIT licensed](LICENSE). 74 | -------------------------------------------------------------------------------- /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 | } 8 | } 9 | -------------------------------------------------------------------------------- /ormconfig.ts: -------------------------------------------------------------------------------- 1 | import { DataSource } from 'typeorm'; 2 | import * as dotenv from 'dotenv'; 3 | import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions'; 4 | 5 | dotenv.config(); 6 | 7 | export const configs: PostgresConnectionOptions = { 8 | type: 'postgres', 9 | url: process.env.DATABASE_URL, 10 | entities: [__dirname + '/src/**/*.entity.{ts,js}'], 11 | migrations: [__dirname + '/src/modules/database/migrations/*{.ts,.js}'], 12 | dropSchema: false, 13 | logging: false, 14 | }; 15 | const dataSource = new DataSource(configs); 16 | 17 | export default dataSource; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nest-series", 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": "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:watch": "jest --watch", 18 | "test:cov": "jest --coverage", 19 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 20 | "test:e2e": "jest --config ./test/jest-e2e.json", 21 | "typeorm": "ts-node -r tsconfig-paths/register ./node_modules/.bin/typeorm", 22 | "migration:generate": "npm run typeorm -- migration:generate src/modules/database/migrations/migrations -d ormconfig.ts", 23 | "migration:run": "npm run typeorm -- -d ormconfig.ts migration:run", 24 | "migration:revert": "npm run typeorm -- -d ormconfig.ts migration:revert" 25 | }, 26 | "dependencies": { 27 | "@aws-sdk/client-s3": "^3.554.0", 28 | "@nestjs-modules/mailer": "^1.11.2", 29 | "@nestjs/common": "^10.0.0", 30 | "@nestjs/config": "^3.2.1", 31 | "@nestjs/core": "^10.0.0", 32 | "@nestjs/jwt": "^10.2.0", 33 | "@nestjs/passport": "^10.0.3", 34 | "@nestjs/platform-express": "^10.0.0", 35 | "@nestjs/swagger": "^7.3.1", 36 | "@nestjs/typeorm": "^10.0.2", 37 | "bcryptjs": "^2.4.3", 38 | "class-transformer": "^0.5.1", 39 | "class-validator": "^0.14.1", 40 | "googleapis": "^134.0.0", 41 | "morgan": "^1.10.0", 42 | "multer-s3": "^3.0.1", 43 | "passport-jwt": "^4.0.1", 44 | "pg": "^8.11.5", 45 | "reflect-metadata": "^0.2.0", 46 | "rxjs": "^7.8.1", 47 | "typeorm": "^0.3.20" 48 | }, 49 | "devDependencies": { 50 | "@nestjs/cli": "^10.0.0", 51 | "@nestjs/schematics": "^10.0.0", 52 | "@nestjs/testing": "^10.0.0", 53 | "@types/bcryptjs": "^2.4.6", 54 | "@types/express": "^4.17.17", 55 | "@types/jest": "^29.5.2", 56 | "@types/morgan": "^1.9.9", 57 | "@types/multer": "^1.4.11", 58 | "@types/multer-s3": "^3.0.3", 59 | "@types/node": "^20.3.1", 60 | "@types/passport-jwt": "^4.0.1", 61 | "@types/supertest": "^6.0.0", 62 | "@typescript-eslint/eslint-plugin": "^6.0.0", 63 | "@typescript-eslint/parser": "^6.0.0", 64 | "eslint": "^8.42.0", 65 | "eslint-config-prettier": "^9.0.0", 66 | "eslint-plugin-prettier": "^5.0.0", 67 | "jest": "^29.5.0", 68 | "prettier": "^3.0.0", 69 | "source-map-support": "^0.5.21", 70 | "supertest": "^6.3.3", 71 | "ts-jest": "^29.1.0", 72 | "ts-loader": "^9.4.3", 73 | "ts-node": "^10.9.1", 74 | "tsconfig-paths": "^4.2.0", 75 | "typescript": "^5.1.3" 76 | }, 77 | "jest": { 78 | "moduleFileExtensions": [ 79 | "js", 80 | "json", 81 | "ts" 82 | ], 83 | "rootDir": "src", 84 | "testRegex": ".*\\.spec\\.ts$", 85 | "transform": { 86 | "^.+\\.(t|j)s$": "ts-jest" 87 | }, 88 | "collectCoverageFrom": [ 89 | "**/*.(t|j)s" 90 | ], 91 | "coverageDirectory": "../coverage", 92 | "testEnvironment": "node" 93 | } 94 | } -------------------------------------------------------------------------------- /src/crud/validation-group.ts: -------------------------------------------------------------------------------- 1 | export enum ValidationGroup { 2 | CREATE = 'CREATE', 3 | UPDATE = 'UPDATE', 4 | } 5 | -------------------------------------------------------------------------------- /src/decorators/session.decorator.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator, ExecutionContext } from '@nestjs/common'; 2 | 3 | export const SessionParam = createParamDecorator( 4 | (data: unknown, ctx: ExecutionContext) => { 5 | const request = ctx.switchToHttp().getRequest(); 6 | return request.user; 7 | }, 8 | ); 9 | -------------------------------------------------------------------------------- /src/decorators/user.decorator.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator, ExecutionContext } from '@nestjs/common'; 2 | 3 | export const UserParam = createParamDecorator( 4 | (data: unknown, ctx: ExecutionContext) => { 5 | const request = ctx.switchToHttp().getRequest(); 6 | return request.user.user; 7 | }, 8 | ); 9 | -------------------------------------------------------------------------------- /src/entities/base.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { 3 | BaseEntity as _BaseEntity, 4 | CreateDateColumn, 5 | DeleteDateColumn, 6 | PrimaryGeneratedColumn, 7 | UpdateDateColumn, 8 | } from 'typeorm'; 9 | 10 | export abstract class BaseEntity extends _BaseEntity { 11 | @ApiProperty({ format: 'uuid' }) 12 | @PrimaryGeneratedColumn('uuid') 13 | id: string; 14 | 15 | @ApiProperty() 16 | @CreateDateColumn() 17 | created_at: Date; 18 | 19 | @ApiProperty() 20 | @UpdateDateColumn() 21 | updated_at: Date; 22 | 23 | @DeleteDateColumn() 24 | deleted_at: Date; 25 | } 26 | -------------------------------------------------------------------------------- /src/entities/media.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, OneToMany } from 'typeorm'; 2 | import { BaseEntity } from './base'; 3 | import { User } from './user.entity'; 4 | 5 | export enum StorageType { 6 | LOCAL = 'LOCAL', 7 | S3 = 'S3', 8 | } 9 | 10 | @Entity({ name: 'media' }) 11 | export class Media extends BaseEntity { 12 | @Column({ type: 'varchar', length: 150 }) 13 | filename: string; 14 | 15 | @Column({ type: 'varchar', length: 255 }) 16 | url: string; 17 | 18 | @Column({ type: 'varchar', length: 150 }) 19 | mimetype: string; 20 | 21 | @Column({ type: 'enum', enum: StorageType, default: StorageType.LOCAL }) 22 | storage_type: StorageType; 23 | 24 | @Column({ type: 'int' }) 25 | size: number; 26 | 27 | @OneToMany(() => User, (user) => user.avatar) 28 | avatars: User; 29 | } 30 | -------------------------------------------------------------------------------- /src/entities/post.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, JoinColumn, ManyToOne } from 'typeorm'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | import { BaseEntity } from './base'; 4 | import { User } from './user.entity'; 5 | import { IsBoolean, IsOptional, IsString, MaxLength } from 'class-validator'; 6 | import { ValidationGroup } from 'src/crud/validation-group'; 7 | 8 | @Entity({ name: 'posts' }) 9 | export class Post extends BaseEntity { 10 | @ApiProperty({ example: 'Here is my title.' }) 11 | @IsOptional({ groups: [ValidationGroup.UPDATE] }) 12 | @IsString({ always: true }) 13 | @MaxLength(255, { always: true }) 14 | @Column({ type: 'varchar', length: 255 }) 15 | title: string; 16 | 17 | @ApiProperty({ example: 'My content' }) 18 | @IsOptional({ groups: [ValidationGroup.UPDATE] }) 19 | @IsString({ always: true }) 20 | @Column({ type: 'text' }) 21 | content: string; 22 | 23 | @IsOptional({ groups: [ValidationGroup.UPDATE] }) 24 | @ApiProperty() 25 | @IsBoolean({ always: true }) 26 | @Column({ type: 'boolean', default: false }) 27 | is_published: boolean; 28 | 29 | @ApiProperty({ type: () => User }) 30 | @ManyToOne(() => User, (user) => user.posts) 31 | @JoinColumn({ name: 'user_id' }) 32 | user: User; 33 | 34 | @ApiProperty() 35 | @Column({ type: 'uuid' }) 36 | user_id: string; 37 | } 38 | -------------------------------------------------------------------------------- /src/entities/session.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, Index, JoinColumn, ManyToOne } from 'typeorm'; 2 | import { BaseEntity } from './base'; 3 | import { User } from './user.entity'; 4 | 5 | @Entity({ name: 'sessions' }) 6 | export class Session extends BaseEntity { 7 | @ManyToOne(() => User, (user) => user.sessions, { eager: true }) 8 | @JoinColumn({ name: 'user_id' }) 9 | user: User; 10 | 11 | @Column({ type: 'uuid' }) 12 | @Index() 13 | user_id: string; 14 | } 15 | -------------------------------------------------------------------------------- /src/entities/user.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AfterLoad, 3 | BeforeInsert, 4 | BeforeUpdate, 5 | Column, 6 | Entity, 7 | JoinColumn, 8 | ManyToOne, 9 | OneToMany, 10 | } from 'typeorm'; 11 | import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; 12 | import { BaseEntity } from './base'; 13 | import { Token } from './user_token.entity'; 14 | import * as bcrypt from 'bcryptjs'; 15 | import { Exclude } from 'class-transformer'; 16 | import { Media } from './media.entity'; 17 | import { Session } from './session.entity'; 18 | import { Post } from './post.entity'; 19 | import { AuthProvider } from 'src/modules/auth/auth.provider'; 20 | 21 | @Entity({ name: 'users' }) 22 | export class User extends BaseEntity { 23 | @ApiProperty({ example: 'Danimai' }) 24 | @Column({ type: 'varchar', length: 50 }) 25 | first_name: string; 26 | 27 | @ApiProperty({ example: 'Mandal' }) 28 | @Column({ type: 'varchar', length: 50, nullable: true }) 29 | last_name: string; 30 | 31 | @ApiProperty({ example: 'example@danimai.com' }) 32 | @Column({ type: 'varchar', length: 255, unique: true }) 33 | email: string; 34 | 35 | @ApiProperty({ example: 'Password@123' }) 36 | @Column({ type: 'varchar', length: 255, nullable: true }) 37 | @Exclude() 38 | password: string; 39 | 40 | @ApiHideProperty() 41 | @Column({ type: 'timestamp with time zone', nullable: true }) 42 | email_verified_at: Date; 43 | 44 | @ApiHideProperty() 45 | @Column({ type: 'boolean', default: false }) 46 | is_active: boolean; 47 | 48 | @ApiHideProperty() 49 | @Column({ default: AuthProvider.EMAIL, enum: AuthProvider }) 50 | provider: AuthProvider; 51 | 52 | @ApiHideProperty() 53 | @OneToMany(() => Token, (token) => token.user) 54 | tokens: Token[]; 55 | 56 | @ApiHideProperty() 57 | @Exclude() 58 | previousPassword: string; 59 | 60 | @ApiHideProperty() 61 | @ManyToOne(() => Media, (media) => media.avatars) 62 | @JoinColumn({ name: 'avatar_id' }) 63 | avatar: Media; 64 | 65 | @ApiHideProperty() 66 | @Column({ type: 'uuid', nullable: true }) 67 | avatar_id: string; 68 | 69 | @ApiHideProperty() 70 | @OneToMany(() => Session, (session) => session.user) 71 | sessions: Session[]; 72 | 73 | @ApiHideProperty() 74 | @OneToMany(() => Post, (post) => post.user) 75 | posts: Post[]; 76 | 77 | @AfterLoad() 78 | storePasswordInCache() { 79 | this.previousPassword = this.password; 80 | } 81 | 82 | @BeforeInsert() 83 | @BeforeUpdate() 84 | async setPassword() { 85 | if (this.previousPassword !== this.password && this.password) { 86 | const salt = await bcrypt.genSalt(); 87 | this.password = await bcrypt.hash(this.password, salt); 88 | } 89 | this.email = this.email.toLowerCase(); 90 | } 91 | 92 | comparePassword(password: string) { 93 | return bcrypt.compareSync(password, this.password); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/entities/user_token.entity.ts: -------------------------------------------------------------------------------- 1 | import { randomStringGenerator } from '@nestjs/common/utils/random-string-generator.util'; 2 | import { BeforeInsert, Column, Entity, JoinColumn, ManyToOne } from 'typeorm'; 3 | import { User } from './user.entity'; 4 | import { BaseEntity } from './base'; 5 | 6 | export enum TokenType { 7 | REGISTER_VERIFY = 'REGISTER_VERIFY', 8 | RESET_PASSWORD = 'RESET_PASSWORD', 9 | } 10 | 11 | @Entity({ name: 'user_tokens' }) 12 | export class Token extends BaseEntity { 13 | @Column({ type: 'varchar', length: 100 }) 14 | token: string; 15 | 16 | @Column({ type: 'boolean', default: false }) 17 | is_used: boolean; 18 | 19 | @Column({ type: 'enum', enum: TokenType }) 20 | type: TokenType; 21 | 22 | @Column({ type: 'timestamp' }) 23 | expires_at: Date; 24 | 25 | @Column({ type: 'uuid' }) 26 | user_id: string; 27 | 28 | @ManyToOne(() => User, (user) => user.tokens) 29 | @JoinColumn({ name: 'user_id' }) 30 | user: User; 31 | 32 | @BeforeInsert() 33 | async generateToken() { 34 | this.token = `${randomStringGenerator()}-${randomStringGenerator()}`; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './modules/app/app.module'; 3 | import { ConfigService } from '@nestjs/config'; 4 | import { createApplication, documentationBuilder } from './utils/bootstrap'; 5 | 6 | async function bootstrap() { 7 | const app = await NestFactory.create(AppModule); 8 | const configService = app.get(ConfigService); 9 | 10 | // bootstrapped functions 11 | createApplication(app); 12 | documentationBuilder(app, configService); 13 | 14 | await app.listen(configService.get('app.port') || 8000); 15 | } 16 | bootstrap(); 17 | -------------------------------------------------------------------------------- /src/modules/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigModule } from '@nestjs/config'; 3 | import { configLoads } from '../config'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { TypeORMConfigFactory } from '../database/typeorm.factory'; 6 | import { AuthModule } from '../auth/auth.module'; 7 | import { UserModule } from '../user/user.module'; 8 | import { MailerModule } from '@nestjs-modules/mailer'; 9 | import { MailerConfigClass } from '../mail/mailerConfig.service'; 10 | import { GoogleAuthModule } from '../auth-google/google.module'; 11 | import { PostModule } from '../post/post.module'; 12 | 13 | const modules = [AuthModule, UserModule, GoogleAuthModule, PostModule]; 14 | 15 | export const global_modules = [ 16 | ConfigModule.forRoot({ 17 | load: configLoads, 18 | isGlobal: true, 19 | envFilePath: ['.env'], 20 | }), 21 | TypeOrmModule.forRootAsync({ 22 | useClass: TypeORMConfigFactory, 23 | }), 24 | MailerModule.forRootAsync({ 25 | useClass: MailerConfigClass, 26 | }), 27 | ]; 28 | 29 | @Module({ 30 | imports: [...global_modules, ...modules], 31 | }) 32 | export class AppModule {} 33 | -------------------------------------------------------------------------------- /src/modules/auth-google/google.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Post, Body } from '@nestjs/common'; 2 | import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; 3 | import { GoogleOAuthDto } from './google.dto'; 4 | import { GoogleService } from './google.service'; 5 | 6 | @Controller({ path: 'google-auth', version: '1' }) 7 | @ApiTags('Auth Google') 8 | export class GoogleController { 9 | constructor(private readonly googleService: GoogleService) {} 10 | 11 | @Post() 12 | @ApiOkResponse({ 13 | description: 'Register/Login with google', 14 | }) 15 | async authenticate(@Body() tokenData: GoogleOAuthDto) { 16 | return this.googleService.authenticate(tokenData.token); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/modules/auth-google/google.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IsString, IsNotEmpty } from 'class-validator'; 3 | 4 | export class GoogleOAuthDto { 5 | @ApiProperty() 6 | @IsString() 7 | @IsNotEmpty() 8 | token: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/modules/auth-google/google.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { User } from 'src/entities/user.entity'; 4 | import { GoogleController } from './google.controller'; 5 | import { GoogleService } from './google.service'; 6 | import { AuthModule } from '../auth/auth.module'; 7 | import { UserModule } from '../user/user.module'; 8 | 9 | @Module({ 10 | imports: [TypeOrmModule.forFeature([User]), UserModule, AuthModule], 11 | controllers: [GoogleController], 12 | providers: [GoogleService], 13 | }) 14 | export class GoogleAuthModule {} 15 | -------------------------------------------------------------------------------- /src/modules/auth-google/google.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { google, Auth } from 'googleapis'; 4 | import { UserService } from '../user/user.service'; 5 | import { Repository } from 'typeorm'; 6 | import { InjectRepository } from '@nestjs/typeorm'; 7 | import { User } from 'src/entities/user.entity'; 8 | import { AuthService } from '../auth/services/auth.service'; 9 | import { AuthProvider } from '../auth/auth.provider'; 10 | 11 | @Injectable() 12 | export class GoogleService { 13 | oauthClient: Auth.OAuth2Client; 14 | 15 | constructor( 16 | private readonly userService: UserService, 17 | private readonly configService: ConfigService, 18 | private readonly authService: AuthService, 19 | @InjectRepository(User) 20 | private readonly userRepository: Repository, 21 | ) { 22 | const clientID = this.configService.get('google.auth.client_id'); 23 | const clientSecret = this.configService.get('google.auth.client_secret'); 24 | 25 | this.oauthClient = new google.auth.OAuth2(clientID, clientSecret); 26 | } 27 | 28 | async authenticate(token: string) { 29 | const { email } = await this.oauthClient.getTokenInfo(token); 30 | 31 | const user = await this.userRepository.findOneBy({ email }); 32 | 33 | if (user) { 34 | return this.handleRegisteredUser(user); 35 | } else { 36 | return this.registerUser(token, email); 37 | } 38 | } 39 | 40 | async registerUser(token: string, email: string) { 41 | const { given_name, family_name } = await this.getUserData(token); 42 | const user = await this.userService.create({ 43 | email, 44 | email_verified_at: new Date(), 45 | first_name: given_name, 46 | last_name: family_name, 47 | is_active: true, 48 | provider: AuthProvider.GOOGLE, 49 | }); 50 | return this.handleRegisteredUser(user); 51 | } 52 | 53 | async getUserData(token: string) { 54 | const userInfoClient = google.oauth2('v2').userinfo; 55 | 56 | this.oauthClient.setCredentials({ 57 | access_token: token, 58 | }); 59 | 60 | const userInfoResponse = await userInfoClient.get({ 61 | auth: this.oauthClient, 62 | }); 63 | 64 | return userInfoResponse.data; 65 | } 66 | 67 | async handleRegisteredUser(user: User) { 68 | return this.authService.createJwtToken(user); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/modules/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { EmailController } from './controllers/email.controller'; 3 | import { EmailService } from './services/email.service'; 4 | import { AuthService } from './services/auth.service'; 5 | import { UserModule } from '../user/user.module'; 6 | import { TypeOrmModule } from '@nestjs/typeorm'; 7 | import { User } from 'src/entities/user.entity'; 8 | import { JwtModule } from '@nestjs/jwt'; 9 | import { ConfigService } from '@nestjs/config'; 10 | import { TokenModule } from '../token/token.module'; 11 | import { JwtStrategy } from './strategies/jwt.strategy'; 12 | import { SessionModule } from '../session/session.module'; 13 | import { RefreshJwtStrategy } from './strategies/refresh.strategy'; 14 | import { AuthController } from './controllers/auth.controller'; 15 | 16 | @Module({ 17 | imports: [ 18 | UserModule, 19 | TokenModule, 20 | SessionModule, 21 | TypeOrmModule.forFeature([User]), 22 | JwtModule.registerAsync({ 23 | inject: [ConfigService], 24 | useFactory: async (configService: ConfigService) => ({ 25 | secret: configService.get('auth.secret'), 26 | signOptions: { 27 | expiresIn: configService.get('auth.refreshTokenExpiresIn'), 28 | }, 29 | }), 30 | }), 31 | ], 32 | controllers: [EmailController, AuthController], 33 | providers: [EmailService, AuthService, JwtStrategy, RefreshJwtStrategy], 34 | exports: [AuthService], 35 | }) 36 | export class AuthModule {} 37 | -------------------------------------------------------------------------------- /src/modules/auth/auth.provider.ts: -------------------------------------------------------------------------------- 1 | export enum AuthProvider { 2 | GOOGLE = 'GOOGLE', 3 | EMAIL = 'EMAIL', 4 | } 5 | -------------------------------------------------------------------------------- /src/modules/auth/controllers/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | HttpCode, 4 | HttpStatus, 5 | Post, 6 | UseGuards, 7 | } from '@nestjs/common'; 8 | import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; 9 | import { AuthGuard } from '@nestjs/passport'; 10 | import { AuthService } from '../services/auth.service'; 11 | import { Session } from 'src/entities/session.entity'; 12 | import { SessionService } from 'src/modules/session/session.service'; 13 | import { SessionParam } from 'src/decorators/session.decorator'; 14 | 15 | @ApiTags('Auth') 16 | @ApiBearerAuth() 17 | @Controller({ 18 | path: 'auth', 19 | version: '1', 20 | }) 21 | export class AuthController { 22 | constructor( 23 | private authService: AuthService, 24 | private sessionService: SessionService, 25 | ) {} 26 | 27 | @UseGuards(AuthGuard('refresh')) 28 | @Post('/refresh-token') 29 | @ApiOperation({ summary: 'Refresh your access token.' }) 30 | @HttpCode(HttpStatus.OK) 31 | async refreshToken(@SessionParam() { id }: Session) { 32 | return this.authService.createAccessToken(id); 33 | } 34 | 35 | @UseGuards(AuthGuard('jwt')) 36 | @Post('/logout') 37 | @ApiOperation({ summary: 'Expire session key' }) 38 | @HttpCode(HttpStatus.OK) 39 | async logout(@SessionParam() { id }: Session) { 40 | return this.sessionService.delete(id); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/modules/auth/controllers/email.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common'; 2 | import { 3 | ApiAcceptedResponse, 4 | ApiBadRequestResponse, 5 | ApiCreatedResponse, 6 | ApiForbiddenResponse, 7 | ApiNoContentResponse, 8 | ApiNotFoundResponse, 9 | ApiOperation, 10 | ApiTags, 11 | } from '@nestjs/swagger'; 12 | import { 13 | EmailVerifyDto, 14 | LoginDto, 15 | RegisterDto, 16 | ResetPasswordDto, 17 | SendVerifyMailDto, 18 | } from '../email.dto'; 19 | import { EmailService } from '../services/email.service'; 20 | 21 | @ApiTags('Auth Email') 22 | @Controller({ 23 | path: 'auth/email', 24 | version: '1', 25 | }) 26 | export class EmailController { 27 | constructor(private emailService: EmailService) {} 28 | 29 | @Post('/register') 30 | @ApiOperation({ summary: 'Register by email' }) 31 | @ApiCreatedResponse({ 32 | description: 'User successfully registered.', 33 | }) 34 | @HttpCode(HttpStatus.CREATED) 35 | async register(@Body() registerDto: RegisterDto) { 36 | return this.emailService.register(registerDto); 37 | } 38 | 39 | @Post('/verify') 40 | @ApiOperation({ summary: 'Verify Email address.' }) 41 | @ApiAcceptedResponse({ 42 | description: 'Email verified successfully.', 43 | }) 44 | @HttpCode(HttpStatus.ACCEPTED) 45 | async verify(@Body() emailVerifyDto: EmailVerifyDto) { 46 | return this.emailService.verify(emailVerifyDto); 47 | } 48 | 49 | @Post('/login') 50 | @ApiOperation({ summary: 'Log in with Email.' }) 51 | @HttpCode(HttpStatus.OK) 52 | async login(@Body() loginDto: LoginDto) { 53 | return this.emailService.login(loginDto); 54 | } 55 | 56 | @Post('/send-verify-email') 57 | @ApiOperation({ summary: 'Send Verification mail.' }) 58 | @ApiNoContentResponse({ 59 | description: 'Sent Verification mail.', 60 | }) 61 | @ApiForbiddenResponse({ 62 | description: 'User already verified.', 63 | }) 64 | @ApiNotFoundResponse({ 65 | description: 'User not found.', 66 | }) 67 | @HttpCode(HttpStatus.NO_CONTENT) 68 | async sendVerifyMail(@Body() sendVerifyMailDto: SendVerifyMailDto) { 69 | return this.emailService.sendVerifyMail(sendVerifyMailDto); 70 | } 71 | 72 | @Post('/reset-password-request') 73 | @ApiOperation({ summary: 'Send Reset Password mail.' }) 74 | @ApiNoContentResponse({ 75 | description: 'Sent Reset Password mail.', 76 | }) 77 | @ApiForbiddenResponse({ 78 | description: 'Please verify email first.', 79 | }) 80 | @ApiNotFoundResponse({ 81 | description: 'User not found.', 82 | }) 83 | @HttpCode(HttpStatus.NO_CONTENT) 84 | async sendForgotMail(@Body() sendForgotMailDto: SendVerifyMailDto) { 85 | return this.emailService.sendForgotMail(sendForgotMailDto); 86 | } 87 | 88 | @Post('/reset-password') 89 | @ApiOperation({ summary: 'Password Reset.' }) 90 | @ApiNoContentResponse({ 91 | description: 'Password Reset Successfully.', 92 | }) 93 | @ApiBadRequestResponse({ 94 | description: 'Invalid Reset token', 95 | }) 96 | @HttpCode(HttpStatus.NO_CONTENT) 97 | async resetPassword(@Body() resetPasswordDto: ResetPasswordDto) { 98 | return this.emailService.resetPassword(resetPasswordDto); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/modules/auth/email.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { Transform } from 'class-transformer'; 3 | import { 4 | IsEmail, 5 | IsNotEmpty, 6 | IsString, 7 | IsStrongPassword, 8 | } from 'class-validator'; 9 | 10 | const strongPasswordConfig = { 11 | minLength: 8, 12 | minLowercase: 1, 13 | minNumbers: 1, 14 | minSymbols: 1, 15 | minUppercase: 1, 16 | }; 17 | 18 | export class RegisterDto { 19 | @ApiProperty({ example: 'example@danimai.com' }) 20 | @IsEmail() 21 | @Transform(({ value }) => 22 | typeof value === 'string' ? value.toLowerCase() : value, 23 | ) 24 | email: string; 25 | 26 | @ApiProperty({ example: 'Password@123' }) 27 | @IsString() 28 | @IsStrongPassword(strongPasswordConfig) 29 | password: string; 30 | 31 | @ApiProperty({ example: 'Danimai' }) 32 | @IsString() 33 | @IsNotEmpty() 34 | first_name: string; 35 | 36 | @ApiProperty({ example: 'Mandal' }) 37 | @IsString() 38 | @IsNotEmpty() 39 | last_name: string; 40 | } 41 | 42 | export class EmailVerifyDto { 43 | @ApiProperty({ example: 'vhsbdjsdfsd-dfmsdfjsd-sdfnsdk' }) 44 | @IsString() 45 | verify_token: string; 46 | } 47 | 48 | export class LoginDto { 49 | @ApiProperty({ example: 'example@danimai.com' }) 50 | @IsEmail() 51 | @Transform(({ value }) => 52 | typeof value === 'string' ? value.toLowerCase() : value, 53 | ) 54 | email: string; 55 | 56 | @ApiProperty({ example: 'Password@123' }) 57 | @IsString() 58 | @IsStrongPassword(strongPasswordConfig) 59 | password: string; 60 | } 61 | 62 | export class SendVerifyMailDto { 63 | @ApiProperty({ example: 'example@danimai.com' }) 64 | @IsEmail() 65 | @Transform(({ value }) => 66 | typeof value === 'string' ? value.toLowerCase() : value, 67 | ) 68 | email: string; 69 | } 70 | 71 | export class ResetPasswordDto { 72 | @ApiProperty({ example: 'Password@123' }) 73 | @IsString() 74 | @IsStrongPassword(strongPasswordConfig) 75 | password: string; 76 | 77 | @ApiProperty({ example: 'vhsbdjsdfsd-dfmsdfjsd-sdfnsdk' }) 78 | @IsString() 79 | reset_token: string; 80 | } 81 | -------------------------------------------------------------------------------- /src/modules/auth/services/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { User } from 'src/entities/user.entity'; 3 | import { JwtService } from '@nestjs/jwt'; 4 | import { MailerService } from '@nestjs-modules/mailer'; 5 | import { ConfigService } from '@nestjs/config'; 6 | import { MailData } from 'src/modules/mail/mail.interface'; 7 | import { SessionService } from 'src/modules/session/session.service'; 8 | 9 | @Injectable() 10 | export class AuthService { 11 | constructor( 12 | private jwtService: JwtService, 13 | private mailerService: MailerService, 14 | private configService: ConfigService, 15 | private sessionService: SessionService, 16 | ) {} 17 | 18 | async createJwtToken(user: User) { 19 | const refreshTokenExpiresIn = this.configService.get( 20 | 'auth.refreshTokenExpiresIn', 21 | ); 22 | const session = await this.sessionService.create(user); 23 | 24 | const accessToken = await this.createAccessToken(session.id); 25 | const refreshToken = this.jwtService.sign( 26 | { 27 | id: session.id, 28 | type: 'REFRESH', 29 | }, 30 | { 31 | expiresIn: refreshTokenExpiresIn, 32 | }, 33 | ); 34 | 35 | return { 36 | accessToken, 37 | refreshToken, 38 | }; 39 | } 40 | 41 | async createAccessToken(sessionId: string) { 42 | const accessTokenExpiresIn = this.configService.get( 43 | 'auth.accessTokenExpiresIn', 44 | ); 45 | 46 | const payload = { 47 | id: sessionId, 48 | type: 'ACCESS', 49 | }; 50 | const accessToken = this.jwtService.sign(payload, { 51 | expiresIn: accessTokenExpiresIn, 52 | }); 53 | 54 | return accessToken; 55 | } 56 | 57 | async userRegisterEmail( 58 | mailData: MailData<{ 59 | hash: string; 60 | }>, 61 | ) { 62 | await this.mailerService.sendMail({ 63 | to: mailData.to, 64 | subject: 'Thank You For Registration, Verify Your Account.', 65 | text: `${this.configService.get( 66 | 'app.frontendDomain', 67 | )}/auth/verify?token=${mailData.data.hash}`, 68 | template: 'auth/registration', 69 | context: { 70 | url: `${this.configService.get( 71 | 'app.frontendDomain', 72 | )}/auth/verify?token=${mailData.data.hash}`, 73 | app_name: this.configService.get('app.name'), 74 | title: 'Thank You For Registration, Verify Your Account.', 75 | actionTitle: 'Verify Your Account', 76 | }, 77 | }); 78 | } 79 | 80 | async forgotPasswordEmail( 81 | mailData: MailData<{ 82 | hash: string; 83 | }>, 84 | ) { 85 | await this.mailerService.sendMail({ 86 | to: mailData.to, 87 | subject: 'Here is your Link for Reset Password.', 88 | text: `${this.configService.get( 89 | 'app.frontendDomain', 90 | )}/auth/reset-password?token=${mailData.data.hash}`, 91 | template: 'auth/registration', 92 | context: { 93 | url: `${this.configService.get( 94 | 'app.frontendDomain', 95 | )}/auth/reset-password?token=${mailData.data.hash}`, 96 | app_name: this.configService.get('app.name'), 97 | title: 'Here is your Link for Reset Password.', 98 | actionTitle: 'Reset Password', 99 | }, 100 | }); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/modules/auth/services/email.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Injectable, 3 | NotFoundException, 4 | UnprocessableEntityException, 5 | } from '@nestjs/common'; 6 | import { 7 | EmailVerifyDto, 8 | LoginDto, 9 | RegisterDto, 10 | ResetPasswordDto, 11 | SendVerifyMailDto, 12 | } from '../email.dto'; 13 | import { UserService } from '../../user/user.service'; 14 | import { TokenService } from '../../token/token.service'; 15 | import { InjectRepository } from '@nestjs/typeorm'; 16 | import { User } from 'src/entities/user.entity'; 17 | import { Repository } from 'typeorm'; 18 | import { AuthService } from './auth.service'; 19 | import { AuthProvider } from '../auth.provider'; 20 | 21 | @Injectable() 22 | export class EmailService { 23 | constructor( 24 | private authService: AuthService, 25 | private userService: UserService, 26 | private tokenService: TokenService, 27 | @InjectRepository(User) private userRepository: Repository, 28 | ) {} 29 | 30 | async register(registerDto: RegisterDto) { 31 | const user = await this.userService.create(registerDto); 32 | const token = await this.tokenService.create(user, 'REGISTER_VERIFY'); 33 | await this.authService.userRegisterEmail({ 34 | to: user.email, 35 | data: { 36 | hash: token.token, 37 | }, 38 | }); 39 | } 40 | 41 | async verify(verifyDto: EmailVerifyDto) { 42 | try { 43 | const user = await this.tokenService.verify( 44 | verifyDto.verify_token, 45 | 'REGISTER_VERIFY', 46 | ); 47 | user.email_verified_at = new Date(); 48 | user.is_active = true; 49 | await user.save(); 50 | } catch (e) { 51 | throw new UnprocessableEntityException({ verify_token: e.message }); 52 | } 53 | } 54 | 55 | async login(loginDto: LoginDto) { 56 | const user = await this.userRepository.findOne({ 57 | where: { email: loginDto.email.toLowerCase() }, 58 | }); 59 | if (user.provider !== AuthProvider.EMAIL) { 60 | throw new UnprocessableEntityException({ 61 | email: `User is registered with ${user.provider}`, 62 | }); 63 | } 64 | if (!user) { 65 | throw new UnprocessableEntityException({ email: 'User not found' }); 66 | } 67 | if (!user.is_active) { 68 | throw new UnprocessableEntityException({ email: 'User not active' }); 69 | } 70 | if (!user.email_verified_at) { 71 | throw new UnprocessableEntityException({ email: 'User not verified' }); 72 | } 73 | if (!user.comparePassword(loginDto.password)) { 74 | throw new UnprocessableEntityException({ 75 | password: 'Password is incorrect', 76 | }); 77 | } 78 | return this.authService.createJwtToken(user); 79 | } 80 | 81 | async sendVerifyMail(sendVerifyMailDto: SendVerifyMailDto) { 82 | const user = await this.userRepository.findOne({ 83 | where: { email: sendVerifyMailDto.email.toLowerCase() }, 84 | }); 85 | 86 | if (!user) { 87 | throw new NotFoundException({ email: 'User not found' }); 88 | } 89 | if (user.email_verified_at) { 90 | throw new UnprocessableEntityException({ 91 | email: 'User already verified', 92 | }); 93 | } 94 | const token = await this.tokenService.create(user, 'REGISTER_VERIFY'); 95 | await this.authService.userRegisterEmail({ 96 | to: user.email, 97 | data: { 98 | hash: token.token, 99 | }, 100 | }); 101 | } 102 | 103 | async sendForgotMail(sendForgotMailDto: SendVerifyMailDto) { 104 | const user = await this.userRepository.findOne({ 105 | where: { email: sendForgotMailDto.email.toLowerCase() }, 106 | }); 107 | 108 | if (!user) { 109 | throw new UnprocessableEntityException({ email: 'User not found' }); 110 | } 111 | 112 | if (!user.email_verified_at) { 113 | throw new UnprocessableEntityException({ 114 | email: 'Please verify email first.', 115 | }); 116 | } 117 | 118 | const token = await this.tokenService.create(user, 'RESET_PASSWORD'); 119 | await this.authService.forgotPasswordEmail({ 120 | to: user.email, 121 | data: { 122 | hash: token.token, 123 | }, 124 | }); 125 | } 126 | 127 | async resetPassword(resetPasswordDto: ResetPasswordDto) { 128 | try { 129 | const user = await this.tokenService.verify( 130 | resetPasswordDto.reset_token, 131 | 'RESET_PASSWORD', 132 | ); 133 | user.password = resetPasswordDto.password; 134 | await user.save(); 135 | } catch (e) { 136 | throw new UnprocessableEntityException({ reset_token: e.message }); 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/modules/auth/strategies/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { ExtractJwt, Strategy } from 'passport-jwt'; 2 | import { 3 | Injectable, 4 | UnauthorizedException, 5 | ForbiddenException, 6 | } from '@nestjs/common'; 7 | import { PassportStrategy } from '@nestjs/passport'; 8 | import { ConfigService } from '@nestjs/config'; 9 | import { SessionService } from 'src/modules/session/session.service'; 10 | 11 | export type JwtPayload = { 12 | id: string; 13 | type: 'ACCESS' | 'REFRESH'; 14 | iat: number; 15 | exp: number; 16 | }; 17 | 18 | @Injectable() 19 | export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { 20 | constructor( 21 | configService: ConfigService, 22 | private sessionService: SessionService, 23 | ) { 24 | super({ 25 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 26 | secretOrKey: configService.get('auth.secret'), 27 | ignoreExpiration: false, 28 | }); 29 | } 30 | 31 | public async validate(payload: JwtPayload) { 32 | try { 33 | if (payload.type !== 'ACCESS') { 34 | throw new UnauthorizedException('Invalid token provided.'); 35 | } 36 | const session = await this.sessionService.get(payload.id); 37 | 38 | if (!session) { 39 | throw new UnauthorizedException('Invalid token provided.'); 40 | } 41 | 42 | const { user } = session; 43 | if (!user.email_verified_at) { 44 | throw new UnauthorizedException('Please verify your email.'); 45 | } 46 | 47 | if (!user.is_active) { 48 | throw new ForbiddenException('Your account is not active.'); 49 | } 50 | return session; 51 | } catch { 52 | throw new UnauthorizedException('User is not authorized.'); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/modules/auth/strategies/refresh.strategy.ts: -------------------------------------------------------------------------------- 1 | import { ExtractJwt, Strategy } from 'passport-jwt'; 2 | import { 3 | ForbiddenException, 4 | Injectable, 5 | UnauthorizedException, 6 | } from '@nestjs/common'; 7 | import { PassportStrategy } from '@nestjs/passport'; 8 | import { ConfigService } from '@nestjs/config'; 9 | import { SessionService } from 'src/modules/session/session.service'; 10 | import { JwtPayload } from './jwt.strategy'; 11 | 12 | @Injectable() 13 | export class RefreshJwtStrategy extends PassportStrategy(Strategy, 'refresh') { 14 | constructor( 15 | configService: ConfigService, 16 | private sessionService: SessionService, 17 | ) { 18 | super({ 19 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 20 | secretOrKey: configService.get('auth.secret'), 21 | ignoreExpiration: false, 22 | }); 23 | } 24 | 25 | public async validate(payload: JwtPayload) { 26 | if (payload.type !== 'REFRESH') { 27 | throw new UnauthorizedException('Invalid token provided.'); 28 | } 29 | 30 | const session = await this.sessionService.get(payload.id); 31 | if (!session) { 32 | throw new ForbiddenException('Invalid token provided.'); 33 | } 34 | return session; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/modules/config/app.config.ts: -------------------------------------------------------------------------------- 1 | import { registerAs } from '@nestjs/config'; 2 | 3 | export default registerAs('app', () => ({ 4 | nodeEnv: process.env.NODE_ENV, 5 | name: process.env.APP_NAME, 6 | workingDirectory: process.env.PWD || process.cwd(), 7 | frontendDomain: process.env.FRONTEND_DOMAIN, 8 | port: process.env.APP_PORT, 9 | })); 10 | -------------------------------------------------------------------------------- /src/modules/config/auth.config.ts: -------------------------------------------------------------------------------- 1 | import { registerAs } from '@nestjs/config'; 2 | 3 | export default registerAs('auth', () => ({ 4 | secret: process.env.JWT_SECRET, 5 | refreshTokenExpiresIn: process.env.JWT_REFRESH_TOKEN_EXPIRES_IN, 6 | accessTokenExpiresIn: process.env.JWT_ACCESS_TOKEN_EXPIRES_IN, 7 | google: { 8 | client_id: process.env.GOOGLE_CLIENT_ID, 9 | client_secret: process.env.GOOGLE_CLIENT_SECRET, 10 | }, 11 | })); 12 | -------------------------------------------------------------------------------- /src/modules/config/database.config.ts: -------------------------------------------------------------------------------- 1 | import { registerAs } from '@nestjs/config'; 2 | 3 | export default registerAs('database', () => ({ 4 | url: process.env.DATABASE_URL, 5 | })); 6 | -------------------------------------------------------------------------------- /src/modules/config/index.ts: -------------------------------------------------------------------------------- 1 | import appConfig from './app.config'; 2 | import authConfig from './auth.config'; 3 | import databaseConfig from './database.config'; 4 | import mailConfig from './mail.config'; 5 | import storageConfig from './storage.config'; 6 | 7 | export const configLoads = [ 8 | databaseConfig, 9 | appConfig, 10 | authConfig, 11 | mailConfig, 12 | storageConfig, 13 | ]; 14 | -------------------------------------------------------------------------------- /src/modules/config/mail.config.ts: -------------------------------------------------------------------------------- 1 | import { registerAs } from '@nestjs/config'; 2 | 3 | export default registerAs('mail', () => ({ 4 | port: parseInt(process.env.MAIL_PORT, 10), 5 | host: process.env.MAIL_HOST, 6 | user: process.env.MAIL_USER, 7 | password: process.env.MAIL_PASSWORD, 8 | defaultEmail: process.env.MAIL_DEFAULT_EMAIL, 9 | defaultName: process.env.MAIL_DEFAULT_NAME, 10 | ignoreTLS: process.env.MAIL_IGNORE_TLS === 'true', 11 | secure: process.env.MAIL_SECURE === 'true', 12 | requireTLS: process.env.MAIL_REQUIRE_TLS === 'true', 13 | })); 14 | -------------------------------------------------------------------------------- /src/modules/config/storage.config.ts: -------------------------------------------------------------------------------- 1 | import { registerAs } from '@nestjs/config'; 2 | 3 | export default registerAs('storage', () => ({ 4 | type: process.env.STORAGE_TYPE, 5 | accessKeyId: process.env.AWS_ACCESS_KEY_ID, 6 | secretAccessKey: process.env.AWS_SECRET_KEY, 7 | region: process.env.AWS_REGION, 8 | bucket: process.env.AWS_BUCKET, 9 | })); 10 | -------------------------------------------------------------------------------- /src/modules/database/migrations/1712332008837-migrations.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class Migrations1712332008837 implements MigrationInterface { 4 | name = 'Migrations1712332008837'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `CREATE TYPE "public"."user_tokens_type_enum" AS ENUM('REGISTER_VERIFY', 'RESET_PASSWORD')`, 9 | ); 10 | await queryRunner.query( 11 | `CREATE TABLE "user_tokens" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP, "token" character varying(100) NOT NULL, "is_used" boolean NOT NULL DEFAULT false, "type" "public"."user_tokens_type_enum" NOT NULL, "expires_at" TIMESTAMP NOT NULL, "user_id" uuid NOT NULL, CONSTRAINT "PK_63764db9d9aaa4af33e07b2f4bf" PRIMARY KEY ("id"))`, 12 | ); 13 | await queryRunner.query( 14 | `CREATE TABLE "users" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP, "first_name" character varying(50) NOT NULL, "last_name" character varying(50), "email" character varying(255) NOT NULL, "password" character varying(255), "email_verified_at" TIMESTAMP WITH TIME ZONE, "is_active" boolean NOT NULL DEFAULT false, CONSTRAINT "UQ_97672ac88f789774dd47f7c8be3" UNIQUE ("email"), CONSTRAINT "PK_a3ffb1c0c8416b9fc6f907b7433" PRIMARY KEY ("id"))`, 15 | ); 16 | await queryRunner.query( 17 | `ALTER TABLE "user_tokens" ADD CONSTRAINT "FK_9e144a67be49e5bba91195ef5de" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, 18 | ); 19 | } 20 | 21 | public async down(queryRunner: QueryRunner): Promise { 22 | await queryRunner.query( 23 | `ALTER TABLE "user_tokens" DROP CONSTRAINT "FK_9e144a67be49e5bba91195ef5de"`, 24 | ); 25 | await queryRunner.query(`DROP TABLE "users"`); 26 | await queryRunner.query(`DROP TABLE "user_tokens"`); 27 | await queryRunner.query(`DROP TYPE "public"."user_tokens_type_enum"`); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/modules/database/migrations/1713940122716-migrations.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm"; 2 | 3 | export class Migrations1713940122716 implements MigrationInterface { 4 | name = 'Migrations1713940122716' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`CREATE TYPE "public"."media_storage_type_enum" AS ENUM('LOCAL', 'S3')`); 8 | await queryRunner.query(`CREATE TABLE "media" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP, "filename" character varying(150) NOT NULL, "url" character varying(255) NOT NULL, "mimetype" character varying(150) NOT NULL, "storage_type" "public"."media_storage_type_enum" NOT NULL DEFAULT 'LOCAL', "size" integer NOT NULL, CONSTRAINT "PK_f4e0fcac36e050de337b670d8bd" PRIMARY KEY ("id"))`); 9 | await queryRunner.query(`ALTER TABLE "users" ADD "avatar_id" uuid`); 10 | await queryRunner.query(`ALTER TABLE "users" ADD CONSTRAINT "FK_c3401836efedec3bec459c8f818" FOREIGN KEY ("avatar_id") REFERENCES "media"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); 11 | } 12 | 13 | public async down(queryRunner: QueryRunner): Promise { 14 | await queryRunner.query(`ALTER TABLE "users" DROP CONSTRAINT "FK_c3401836efedec3bec459c8f818"`); 15 | await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "avatar_id"`); 16 | await queryRunner.query(`DROP TABLE "media"`); 17 | await queryRunner.query(`DROP TYPE "public"."media_storage_type_enum"`); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/modules/database/migrations/1714030025527-migrations.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm"; 2 | 3 | export class Migrations1714030025527 implements MigrationInterface { 4 | name = 'Migrations1714030025527' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`CREATE TABLE "sessions" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP, "user_id" uuid NOT NULL, CONSTRAINT "PK_3238ef96f18b355b671619111bc" PRIMARY KEY ("id"))`); 8 | await queryRunner.query(`CREATE INDEX "IDX_085d540d9f418cfbdc7bd55bb1" ON "sessions" ("user_id") `); 9 | await queryRunner.query(`ALTER TABLE "sessions" ADD CONSTRAINT "FK_085d540d9f418cfbdc7bd55bb19" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query(`ALTER TABLE "sessions" DROP CONSTRAINT "FK_085d540d9f418cfbdc7bd55bb19"`); 14 | await queryRunner.query(`DROP INDEX "public"."IDX_085d540d9f418cfbdc7bd55bb1"`); 15 | await queryRunner.query(`DROP TABLE "sessions"`); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/modules/database/migrations/1714141766950-migrations.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm"; 2 | 3 | export class Migrations1714141766950 implements MigrationInterface { 4 | name = 'Migrations1714141766950' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`CREATE TABLE "posts" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP, "title" character varying(255) NOT NULL, "content" text NOT NULL, "is_published" boolean NOT NULL DEFAULT false, "user_id" uuid NOT NULL, CONSTRAINT "PK_2829ac61eff60fcec60d7274b9e" PRIMARY KEY ("id"))`); 8 | await queryRunner.query(`ALTER TABLE "users" ADD "provider" character varying NOT NULL DEFAULT 'EMAIL'`); 9 | await queryRunner.query(`ALTER TABLE "posts" ADD CONSTRAINT "FK_c4f9a7bd77b489e711277ee5986" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query(`ALTER TABLE "posts" DROP CONSTRAINT "FK_c4f9a7bd77b489e711277ee5986"`); 14 | await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "provider"`); 15 | await queryRunner.query(`DROP TABLE "posts"`); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/modules/database/typeorm.factory.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { TypeOrmModuleOptions, TypeOrmOptionsFactory } from '@nestjs/typeorm'; 3 | import ORMConfig from '../../../ormconfig'; 4 | 5 | @Injectable() 6 | export class TypeORMConfigFactory implements TypeOrmOptionsFactory { 7 | createTypeOrmOptions(): TypeOrmModuleOptions { 8 | return ORMConfig.options; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/modules/mail/mail.interface.ts: -------------------------------------------------------------------------------- 1 | export interface MailData { 2 | to: string; 3 | data: T; 4 | } 5 | -------------------------------------------------------------------------------- /src/modules/mail/mailerConfig.service.ts: -------------------------------------------------------------------------------- 1 | import { MailerOptions, MailerOptionsFactory } from '@nestjs-modules/mailer'; 2 | import { Injectable } from '@nestjs/common'; 3 | import { ConfigService } from '@nestjs/config'; 4 | import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter'; 5 | import * as path from 'path'; 6 | 7 | @Injectable() 8 | export class MailerConfigClass implements MailerOptionsFactory { 9 | constructor(private configService: ConfigService) {} 10 | 11 | createMailerOptions(): MailerOptions { 12 | return { 13 | transport: { 14 | host: this.configService.get('mail.host'), 15 | port: this.configService.get('mail.port'), 16 | ignoreTLS: this.configService.get('mail.ignoreTLS'), 17 | secure: this.configService.get('mail.secure'), 18 | requireTLS: this.configService.get('mail.requireTLS'), 19 | auth: { 20 | user: this.configService.get('mail.user'), 21 | pass: this.configService.get('mail.password'), 22 | }, 23 | }, 24 | defaults: { 25 | from: `"${this.configService.get( 26 | 'mail.defaultName', 27 | )}" <${this.configService.get('mail.defaultEmail')}>`, 28 | }, 29 | template: { 30 | dir: path.join( 31 | this.configService.get('app.workingDirectory'), 32 | 'src', 33 | 'modules', 34 | 'mail', 35 | 'templates', 36 | ), 37 | adapter: new HandlebarsAdapter(), 38 | options: { 39 | strict: true, 40 | }, 41 | }, 42 | }; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/modules/mail/templates/auth/registration.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{title}} 6 | 7 | 8 | 9 | 10 | 11 | 16 | 17 | 18 | 21 | 22 | 23 | 29 | 30 |
14 | {{app_name}} 15 |
19 | Thank You for registration, Please verify to activate account.
20 |
24 | {{actionTitle}} 28 |
31 | 32 | 33 | -------------------------------------------------------------------------------- /src/modules/media/local.service.ts: -------------------------------------------------------------------------------- 1 | import { Media, StorageType } from 'src/entities/media.entity'; 2 | import { MediaServiceContract } from './media.interface'; 3 | import { Repository } from 'typeorm'; 4 | import * as fs from 'node:fs'; 5 | import * as path from 'node:path'; 6 | import { Logger, NotFoundException } from '@nestjs/common'; 7 | import { Response } from 'express'; 8 | 9 | export class LocalService implements MediaServiceContract { 10 | storageType = StorageType.LOCAL; 11 | logger = new Logger(LocalService.name); 12 | 13 | constructor(private mediaRepository: Repository) {} 14 | 15 | async get(media: Media, res: Response) { 16 | const file_path = path.join(media.url); 17 | 18 | if (!fs.existsSync(file_path)) { 19 | throw new NotFoundException(); 20 | } 21 | const file = fs.createReadStream(file_path); 22 | res.setHeader('content-type', media.mimetype); 23 | file.pipe(res); 24 | } 25 | 26 | create(file: Express.Multer.File) { 27 | return this.mediaRepository.save({ 28 | filename: file.filename, 29 | url: `/${file.destination}/${file.filename}`, 30 | mimetype: file.mimetype, 31 | size: file.size, 32 | storage_type: this.storageType, 33 | }); 34 | } 35 | 36 | async delete(media: Media) { 37 | try { 38 | fs.unlinkSync(path.join(media.url)); 39 | await this.mediaRepository.delete(media.id); 40 | } catch (e) { 41 | this.logger.error(e); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/modules/media/media.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Headers, Param, Res } from '@nestjs/common'; 2 | import { ApiTags } from '@nestjs/swagger'; 3 | import { Response } from 'express'; 4 | import { MediaService } from './media.service'; 5 | 6 | @ApiTags('Media') 7 | @Controller({ 8 | path: 'media', 9 | version: '1', 10 | }) 11 | export class MediaController { 12 | constructor(private mediaService: MediaService) {} 13 | 14 | @Get(':id') 15 | async getOne( 16 | @Res() res: Response, 17 | @Param('id') id: string, 18 | @Headers() headers?: Record, 19 | ) { 20 | await this.mediaService.get(id, res, headers.range); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/modules/media/media.interface.ts: -------------------------------------------------------------------------------- 1 | import { Response } from 'express'; 2 | import { Media } from 'src/entities/media.entity'; 3 | 4 | export interface MediaServiceContract { 5 | get: (media: Media, res: Response, range?: string) => Promise; 6 | create: (file: Express.Multer.File) => Promise; 7 | delete: (media: Media) => Promise; 8 | } 9 | 10 | export interface S3File extends Express.Multer.File { 11 | bucket: string; 12 | key: string; 13 | acl: string; 14 | contentType: string; 15 | contentDisposition: null; 16 | storageClass: string; 17 | serverSideEncryption: null; 18 | metadata: any; 19 | location: string; 20 | etag: string; 21 | } 22 | -------------------------------------------------------------------------------- /src/modules/media/media.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { MediaService } from './media.service'; 4 | import { Media } from 'src/entities/media.entity'; 5 | import { MediaController } from './media.controller'; 6 | 7 | @Module({ 8 | imports: [TypeOrmModule.forFeature([Media])], 9 | providers: [MediaService], 10 | exports: [MediaService], 11 | controllers: [MediaController], 12 | }) 13 | export class MediaModule {} 14 | -------------------------------------------------------------------------------- /src/modules/media/media.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { InjectRepository } from '@nestjs/typeorm'; 4 | import { Repository } from 'typeorm'; 5 | import { Media } from 'src/entities/media.entity'; 6 | import { MediaServiceContract } from './media.interface'; 7 | import { S3Service } from './s3.service'; 8 | import { LocalService } from './local.service'; 9 | import { Response } from 'express'; 10 | 11 | @Injectable() 12 | export class MediaService { 13 | serviceHandler: MediaServiceContract; 14 | 15 | constructor( 16 | @InjectRepository(Media) 17 | private mediaRepository: Repository, 18 | private configService: ConfigService, 19 | ) { 20 | if (this.configService.get('storage.type') === 'S3') { 21 | this.serviceHandler = new S3Service(mediaRepository, configService); 22 | } else { 23 | this.serviceHandler = new LocalService(mediaRepository); 24 | } 25 | } 26 | 27 | async get(id: string, res: Response, range: string) { 28 | const media = await this.mediaRepository.findOneBy({ id }); 29 | 30 | await this.serviceHandler.get(media, res, range); 31 | } 32 | 33 | async create(file: Express.Multer.File): Promise { 34 | return this.serviceHandler.create(file); 35 | } 36 | 37 | async update(file: Express.Multer.File, id?: string) { 38 | if (id) { 39 | const media = await this.mediaRepository.findOneBy({ id }); 40 | this.serviceHandler.delete(media); 41 | } 42 | return this.serviceHandler.create(file); 43 | } 44 | 45 | async deleteMedia(id: string) { 46 | const media = await this.mediaRepository.findOneBy({ id }); 47 | await this.serviceHandler.delete(media); 48 | await this.mediaRepository.delete(id); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/modules/media/multer_config.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { 4 | MulterModuleOptions, 5 | MulterOptionsFactory, 6 | } from '@nestjs/platform-express'; 7 | import { S3Client } from '@aws-sdk/client-s3'; 8 | import * as multerS3 from 'multer-s3'; 9 | import { randomStringGenerator } from '@nestjs/common/utils/random-string-generator.util'; 10 | import * as path from 'path'; 11 | import { StorageType } from 'src/entities/media.entity'; 12 | 13 | @Injectable() 14 | export class MulterConfigService implements MulterOptionsFactory { 15 | constructor(private configService: ConfigService) {} 16 | 17 | createMulterOptions(): MulterModuleOptions { 18 | const storageType = this.configService.get('storage.type'); 19 | const region = this.configService.get('storage.region'); 20 | const workingDirectory = this.configService.get('app.workingDirectory'); 21 | 22 | if (storageType === StorageType.S3) { 23 | const client = new S3Client({ 24 | forcePathStyle: false, 25 | region, 26 | credentials: { 27 | accessKeyId: this.configService.get('storage.accessKeyId'), 28 | secretAccessKey: this.configService.get('storage.secretAccessKey'), 29 | }, 30 | }); 31 | 32 | return { 33 | storage: multerS3({ 34 | contentType: multerS3.AUTO_CONTENT_TYPE, 35 | s3: client, 36 | bucket: this.configService.get('storage.bucket'), 37 | key: function (_, file, cb) { 38 | cb(null, `${randomStringGenerator()}${file.originalname}`); 39 | }, 40 | }), 41 | }; 42 | } else { 43 | return { 44 | dest: path.join(workingDirectory, 'media'), 45 | }; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/modules/media/s3.service.ts: -------------------------------------------------------------------------------- 1 | import { Media, StorageType } from 'src/entities/media.entity'; 2 | import { MediaServiceContract, S3File } from './media.interface'; 3 | import { Repository } from 'typeorm'; 4 | import { Logger } from '@nestjs/common'; 5 | import { S3 } from '@aws-sdk/client-s3'; 6 | import { ConfigService } from '@nestjs/config'; 7 | import { Response } from 'express'; 8 | import { Readable } from 'stream'; 9 | 10 | export class S3Service implements MediaServiceContract { 11 | storageType = StorageType.S3; 12 | logger = new Logger(S3Service.name); 13 | client: S3; 14 | bucket: string; 15 | 16 | constructor( 17 | private mediaRepository: Repository, 18 | private configService: ConfigService, 19 | ) { 20 | this.bucket = configService.get('storage.bucket'); 21 | this.client = new S3({ 22 | region: configService.get('storage.region'), 23 | credentials: { 24 | accessKeyId: configService.get('storage.accessKeyId'), 25 | secretAccessKey: configService.get('storage.secretAccessKey'), 26 | }, 27 | }); 28 | } 29 | 30 | async get(media: Media, res: Response, range?: string) { 31 | try { 32 | const response = await this.client.getObject({ 33 | Bucket: this.bucket, 34 | Key: media.filename, 35 | Range: range, 36 | ResponseContentType: media.mimetype, 37 | }); 38 | res.setHeader('content-length', response.ContentLength); 39 | res.setHeader('content-type', response.ContentType); 40 | res.setHeader('accept-ranges', response.AcceptRanges); 41 | res.setHeader('etag', response.ETag); 42 | res.status(response.$metadata.httpStatusCode); 43 | 44 | (response.Body as Readable).pipe(res); 45 | } catch { 46 | res.status(404).json({ message: 'Media not found.' }); 47 | } 48 | } 49 | 50 | create(file: S3File) { 51 | return this.mediaRepository.save({ 52 | filename: file.key, 53 | url: file.location, 54 | mimetype: file.contentType, 55 | size: file.size, 56 | storage_type: this.storageType, 57 | }); 58 | } 59 | 60 | async delete(media: Media) { 61 | try { 62 | await this.client.deleteObject({ 63 | Bucket: this.configService.get('storage.bucket'), 64 | Key: media.filename, 65 | }); 66 | } catch (e) { 67 | this.logger.error(e); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/modules/post/post.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Delete, 5 | Get, 6 | Param, 7 | ParseUUIDPipe, 8 | Patch, 9 | Post, 10 | Query, 11 | UseGuards, 12 | ValidationPipe, 13 | } from '@nestjs/common'; 14 | import { 15 | ApiBearerAuth, 16 | ApiBody, 17 | ApiCreatedResponse, 18 | ApiParam, 19 | ApiTags, 20 | PickType, 21 | } from '@nestjs/swagger'; 22 | import { ValidationGroup } from 'src/crud/validation-group'; 23 | import { UserParam } from 'src/decorators/user.decorator'; 24 | import { Post as PostEntity } from 'src/entities/post.entity'; 25 | import { User } from 'src/entities/user.entity'; 26 | import { ApiPaginatedResponse } from 'src/pagination/pagination.decorator'; 27 | import { PaginationQuery } from 'src/pagination/pagination.dto'; 28 | import { IsIDExistPipe } from 'src/pipes/IsIDExist.pipe'; 29 | import validationOptions from 'src/utils/validation-options'; 30 | import { PostService } from './post.service'; 31 | import { AuthGuard } from '@nestjs/passport'; 32 | 33 | @ApiTags('Post') 34 | @Controller({ 35 | path: 'posts', 36 | version: '1', 37 | }) 38 | @ApiBearerAuth() 39 | @UseGuards(AuthGuard('jwt')) 40 | export class PostController { 41 | constructor(private postService: PostService) {} 42 | 43 | @Post() 44 | @ApiBody({ 45 | type: PickType(PostEntity, ['content', 'title', 'is_published']), 46 | }) 47 | @ApiCreatedResponse({ 48 | type: PostEntity, 49 | }) 50 | create( 51 | @Body( 52 | new ValidationPipe({ 53 | ...validationOptions, 54 | groups: [ValidationGroup.CREATE], 55 | }), 56 | ) 57 | createDto: PostEntity, 58 | @UserParam() user: User, 59 | ) { 60 | return this.postService.create(createDto, user); 61 | } 62 | 63 | @Get() 64 | @ApiPaginatedResponse({ 65 | type: PostEntity, 66 | }) 67 | getAll(@Query() paginationDto: PaginationQuery, @UserParam() user: User) { 68 | return this.postService.getAll(user, paginationDto); 69 | } 70 | 71 | @Get(':id') 72 | @ApiCreatedResponse({ 73 | type: PostEntity, 74 | }) 75 | @ApiParam({ name: 'id', type: 'string', format: 'uuid' }) 76 | getOne( 77 | @Param( 78 | 'id', 79 | ParseUUIDPipe, 80 | IsIDExistPipe({ entity: PostEntity, relations: { user: true } }), 81 | ) 82 | post: PostEntity, 83 | ) { 84 | return post; 85 | } 86 | 87 | @Patch(':id') 88 | @ApiCreatedResponse({ 89 | type: PostEntity, 90 | }) 91 | @ApiBody({ 92 | type: PickType(PostEntity, ['content', 'title', 'is_published']), 93 | }) 94 | @ApiParam({ name: 'id', type: 'string', format: 'uuid' }) 95 | partialUpdate( 96 | @Param( 97 | 'id', 98 | ParseUUIDPipe, 99 | IsIDExistPipe({ entity: PostEntity, relations: { user: true } }), 100 | ) 101 | post: PostEntity, 102 | @Body( 103 | new ValidationPipe({ 104 | ...validationOptions, 105 | groups: [ValidationGroup.UPDATE], 106 | }), 107 | ) 108 | updateDto: PostEntity, 109 | @UserParam() user: User, 110 | ) { 111 | return this.postService.update(post, user, updateDto); 112 | } 113 | 114 | @Delete(':id') 115 | @ApiParam({ name: 'id', type: 'string', format: 'uuid' }) 116 | delete( 117 | @Param( 118 | 'id', 119 | ParseUUIDPipe, 120 | IsIDExistPipe({ entity: PostEntity, relations: { user: true } }), 121 | ) 122 | post: PostEntity, 123 | @UserParam() user: User, 124 | ) { 125 | return this.postService.delete(post, user); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/modules/post/post.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { PostService } from './post.service'; 3 | import { PostController } from './post.controller'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { Post } from 'src/entities/post.entity'; 6 | 7 | @Module({ 8 | imports: [TypeOrmModule.forFeature([Post])], 9 | providers: [PostService], 10 | controllers: [PostController], 11 | }) 12 | export class PostModule {} 13 | -------------------------------------------------------------------------------- /src/modules/post/post.service.ts: -------------------------------------------------------------------------------- 1 | import { ForbiddenException, Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { Post } from 'src/entities/post.entity'; 4 | import { User } from 'src/entities/user.entity'; 5 | import { paginate } from 'src/pagination/paginate'; 6 | import { PaginationResponse } from 'src/pagination/pagination-response'; 7 | import { PaginationQuery } from 'src/pagination/pagination.dto'; 8 | import { Repository } from 'typeorm'; 9 | 10 | @Injectable() 11 | export class PostService { 12 | constructor( 13 | @InjectRepository(Post) private postRepository: Repository, 14 | ) {} 15 | async getAll(user: User, paginationDto: PaginationQuery) { 16 | const queryBuilder = this.postRepository.createQueryBuilder('post'); 17 | queryBuilder.where('post.user_id = :user_id', { user_id: user.id }); 18 | paginate(queryBuilder, paginationDto); 19 | 20 | const [posts, total] = await queryBuilder.getManyAndCount(); 21 | 22 | return new PaginationResponse(posts, total, paginationDto); 23 | } 24 | 25 | create(createDto: Post, user: User) { 26 | return this.postRepository 27 | .create({ 28 | ...createDto, 29 | user_id: user.id, 30 | }) 31 | .save(); 32 | } 33 | 34 | async update(post: Post, user: User, updateDto: Post) { 35 | if (post.user_id !== user.id) { 36 | throw new ForbiddenException('You are now allowed to edit this post.'); 37 | } 38 | await this.postRepository.update({ id: post.id }, updateDto); 39 | 40 | return { 41 | ...post, 42 | ...updateDto, 43 | }; 44 | } 45 | 46 | async delete(post: Post, user: User) { 47 | if (post.user_id !== user.id) { 48 | throw new ForbiddenException('You are now allowed to edit this post.'); 49 | } 50 | await this.postRepository.delete(post.id); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/modules/session/session.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { SessionService } from './session.service'; 4 | import { Session } from 'src/entities/session.entity'; 5 | 6 | @Module({ 7 | imports: [TypeOrmModule.forFeature([Session])], 8 | providers: [SessionService], 9 | exports: [SessionService], 10 | }) 11 | export class SessionModule {} 12 | -------------------------------------------------------------------------------- /src/modules/session/session.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NotFoundException } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { Session } from 'src/entities/session.entity'; 4 | import { User } from 'src/entities/user.entity'; 5 | import { Repository } from 'typeorm'; 6 | 7 | @Injectable() 8 | export class SessionService { 9 | constructor( 10 | @InjectRepository(Session) 11 | private sessionRepository: Repository, 12 | ) {} 13 | 14 | create(user: User) { 15 | return this.sessionRepository 16 | .create({ 17 | user, 18 | }) 19 | .save(); 20 | } 21 | 22 | async get(id: string) { 23 | const session = await this.sessionRepository.findOneBy({ 24 | id, 25 | }); 26 | if (!session) { 27 | throw new NotFoundException('Session not found'); 28 | } 29 | return session; 30 | } 31 | 32 | async delete(id: string) { 33 | await this.sessionRepository.softDelete({ 34 | id, 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/modules/token/token.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TokenService } from '../token/token.service'; 3 | import { Token } from 'src/entities/user_token.entity'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | 6 | @Module({ 7 | imports: [TypeOrmModule.forFeature([Token])], 8 | providers: [TokenService], 9 | exports: [TokenService], 10 | }) 11 | export class TokenModule {} 12 | -------------------------------------------------------------------------------- /src/modules/token/token.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { User } from 'src/entities/user.entity'; 4 | import { Token, TokenType } from 'src/entities/user_token.entity'; 5 | import { Repository } from 'typeorm'; 6 | 7 | @Injectable() 8 | export class TokenService { 9 | constructor( 10 | @InjectRepository(Token) 11 | private readonly tokenRepository: Repository, 12 | ) {} 13 | 14 | async create( 15 | user: User, 16 | type: keyof typeof TokenType = 'REGISTER_VERIFY', 17 | expires_at: Date = new Date(Date.now() + 1000 * 60 * 60 * 24 * 7), 18 | ) { 19 | const token = Token.create({ 20 | user_id: user.id, 21 | type: TokenType[type], 22 | expires_at, 23 | }); 24 | return this.tokenRepository.save(token); 25 | } 26 | 27 | async verify(token: string, type: keyof typeof TokenType) { 28 | const tokenEntity = await this.tokenRepository.findOne({ 29 | relations: ['user'], 30 | loadEagerRelations: true, 31 | where: { token, type: TokenType[type], is_used: false }, 32 | }); 33 | if (!tokenEntity) { 34 | throw new Error('Token not found'); 35 | } 36 | if (tokenEntity.expires_at < new Date()) { 37 | throw new Error('Token expired'); 38 | } 39 | tokenEntity.is_used = true; 40 | await tokenEntity.save(); 41 | return tokenEntity.user; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/modules/user/user.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Get, 5 | HttpStatus, 6 | Patch, 7 | UploadedFile, 8 | UseGuards, 9 | UseInterceptors, 10 | } from '@nestjs/common'; 11 | import { 12 | ApiBearerAuth, 13 | ApiConsumes, 14 | ApiOperation, 15 | ApiResponse, 16 | ApiTags, 17 | } from '@nestjs/swagger'; 18 | import { UserService } from './user.service'; 19 | import { AuthGuard } from '@nestjs/passport'; 20 | import { UserParam } from '../../decorators/user.decorator'; 21 | import { User } from 'src/entities/user.entity'; 22 | import { FileInterceptor } from '@nestjs/platform-express'; 23 | import { UserUpdateDto } from './user.dto'; 24 | 25 | @ApiTags('User') 26 | @ApiBearerAuth() 27 | @Controller({ 28 | path: 'users', 29 | version: '1', 30 | }) 31 | @UseGuards(AuthGuard('jwt')) 32 | export class UserController { 33 | constructor(private userService: UserService) {} 34 | 35 | @Get('/me') 36 | @ApiOperation({ summary: 'get logged in user details' }) 37 | async me(@UserParam() user: User) { 38 | return user; 39 | } 40 | 41 | @Patch('/me') 42 | @ApiResponse({ 43 | status: HttpStatus.OK, 44 | description: 'Update logged in user', 45 | }) 46 | @ApiConsumes('multipart/form-data') 47 | @ApiOperation({ 48 | summary: 'update logged in user', 49 | }) 50 | @UseInterceptors(FileInterceptor('avatar')) 51 | async update( 52 | @UserParam() user: User, 53 | @UploadedFile() avatar: Express.Multer.File, 54 | @Body() updateDto: UserUpdateDto, 55 | ) { 56 | return this.userService.update(user, avatar, updateDto); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/modules/user/user.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; 3 | 4 | export class UserUpdateDto { 5 | @ApiProperty({ 6 | example: '34567890-jhgfghjhkjl', 7 | type: 'string', 8 | format: 'binary', 9 | required: false, 10 | }) 11 | avatar: string; 12 | 13 | @ApiProperty({ example: 'Danimai', required: false }) 14 | @IsString() 15 | @IsNotEmpty() 16 | @IsOptional() 17 | first_name: string; 18 | 19 | @ApiProperty({ example: 'Mandal', required: false }) 20 | @IsString() 21 | @IsNotEmpty() 22 | @IsOptional() 23 | last_name: string; 24 | } 25 | -------------------------------------------------------------------------------- /src/modules/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { UserService } from './user.service'; 3 | import { User } from 'src/entities/user.entity'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { UserController } from './user.controller'; 6 | import { MediaModule } from '../media/media.module'; 7 | import { MulterModule } from '@nestjs/platform-express'; 8 | import { MulterConfigService } from '../media/multer_config.service'; 9 | 10 | @Module({ 11 | imports: [ 12 | TypeOrmModule.forFeature([User]), 13 | MediaModule, 14 | MulterModule.registerAsync({ 15 | useClass: MulterConfigService, 16 | }), 17 | ], 18 | controllers: [UserController], 19 | providers: [UserService], 20 | exports: [UserService], 21 | }) 22 | export class UserModule {} 23 | -------------------------------------------------------------------------------- /src/modules/user/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { Repository } from 'typeorm'; 4 | import { User } from 'src/entities/user.entity'; 5 | import { RegisterDto } from 'src/modules/auth/email.dto'; 6 | import { UserUpdateDto } from './user.dto'; 7 | import { MediaService } from '../media/media.service'; 8 | import { plainToInstance } from 'class-transformer'; 9 | 10 | @Injectable() 11 | export class UserService { 12 | constructor( 13 | @InjectRepository(User) 14 | private userRepository: Repository, 15 | private mediaService: MediaService, 16 | ) {} 17 | 18 | async create( 19 | userCreateDto: 20 | | RegisterDto 21 | | Pick, 22 | ) { 23 | const user = User.create({ ...userCreateDto }); 24 | return this.userRepository.save(user); 25 | } 26 | 27 | async update( 28 | user: User, 29 | avatar: Express.Multer.File, 30 | updateDto: UserUpdateDto, 31 | ) { 32 | const updateData: Record = { 33 | ...updateDto, 34 | }; 35 | const previousImage = user.avatar_id; 36 | if (avatar) { 37 | updateData.avatar_id = (await this.mediaService.update(avatar)).id; 38 | } 39 | 40 | await this.userRepository.update(user.id, updateData); 41 | if (avatar && updateData.avatar_id !== previousImage) { 42 | await this.mediaService.deleteMedia(previousImage); 43 | } 44 | 45 | return plainToInstance(User, { 46 | ...user, 47 | ...updateData, 48 | }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/pagination/paginate.ts: -------------------------------------------------------------------------------- 1 | import { SelectQueryBuilder } from 'typeorm'; 2 | import { PaginationQuery } from './pagination.dto'; 3 | 4 | export const paginate = ( 5 | query: SelectQueryBuilder, 6 | paginationQuery: PaginationQuery, 7 | ) => { 8 | const { page, limit } = paginationQuery; 9 | return query.take(limit).skip((page - 1) * limit); 10 | }; 11 | -------------------------------------------------------------------------------- /src/pagination/pagination-response.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { PaginationQuery } from './pagination.dto'; 3 | 4 | export class PaginationResponse { 5 | @ApiProperty({ 6 | title: 'Data', 7 | isArray: true, 8 | }) 9 | readonly rows: T[]; 10 | 11 | @ApiProperty({ 12 | title: 'Total', 13 | }) 14 | readonly count: number = 0; 15 | 16 | @ApiProperty({ 17 | title: 'Page', 18 | }) 19 | readonly page: number = 1; 20 | 21 | @ApiProperty({ 22 | title: 'Limit', 23 | }) 24 | readonly limit: number = 10; 25 | 26 | @ApiProperty({ 27 | title: 'Has Previous Page', 28 | }) 29 | readonly hasPreviousPage: boolean = false; 30 | 31 | @ApiProperty({ 32 | title: 'Has Next Page', 33 | }) 34 | readonly hasNextPage: boolean = false; 35 | 36 | constructor(data: T[], total: number, paginationQuery: PaginationQuery) { 37 | const { limit, page } = paginationQuery; 38 | this.rows = data; 39 | this.page = page; 40 | this.limit = limit; 41 | this.count = total; 42 | if (total > page * limit) { 43 | this.hasNextPage = true; 44 | } 45 | if (page > 1) { 46 | this.hasPreviousPage = true; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/pagination/pagination.decorator.ts: -------------------------------------------------------------------------------- 1 | import { applyDecorators, Type } from '@nestjs/common'; 2 | import { ApiExtraModels, ApiOkResponse, getSchemaPath } from '@nestjs/swagger'; 3 | import { PaginationResponse } from './pagination-response'; 4 | 5 | export interface IApiPaginatedResponse { 6 | description?: string; 7 | type: Type; 8 | } 9 | export const ApiPaginatedResponse = ({ 10 | description, 11 | type, 12 | }: IApiPaginatedResponse) => { 13 | return applyDecorators( 14 | ApiExtraModels(PaginationResponse), 15 | ApiExtraModels(type), 16 | ApiOkResponse({ 17 | description: description || 'Successfully received model list', 18 | schema: { 19 | allOf: [ 20 | { $ref: getSchemaPath(PaginationResponse) }, 21 | { 22 | properties: { 23 | rows: { 24 | type: 'array', 25 | items: { $ref: getSchemaPath(type) }, 26 | }, 27 | }, 28 | }, 29 | ], 30 | }, 31 | }), 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /src/pagination/pagination.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { Transform, Type } from 'class-transformer'; 3 | import { IsInt, IsNotEmpty, Max, Min } from 'class-validator'; 4 | 5 | export class PaginationQuery { 6 | @ApiProperty({ 7 | minimum: 1, 8 | title: 'Page', 9 | exclusiveMaximum: true, 10 | exclusiveMinimum: true, 11 | default: 1, 12 | type: 'integer', 13 | required: false, 14 | }) 15 | @IsNotEmpty() 16 | @Type(() => Number) 17 | @IsInt() 18 | @Min(1) 19 | page = 1; 20 | 21 | @ApiProperty({ 22 | minimum: 10, 23 | maximum: 50, 24 | title: 'Limit', 25 | default: 10, 26 | type: 'integer', 27 | required: false, 28 | }) 29 | @IsNotEmpty() 30 | @Type(() => Number) 31 | @Transform(({ value }) => (value > 50 ? 50 : value)) 32 | @Transform(({ value }) => (value < 10 ? 10 : value)) 33 | @IsInt() 34 | @Min(10) 35 | @Max(50) 36 | limit = 10; 37 | } 38 | -------------------------------------------------------------------------------- /src/pipes/IsIDExist.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NotFoundException, PipeTransform } from '@nestjs/common'; 2 | import { InjectDataSource } from '@nestjs/typeorm'; 3 | import { EntityClassOrSchema } from '@nestjs/typeorm/dist/interfaces/entity-class-or-schema.type'; 4 | import { DataSource, FindOptionsRelations } from 'typeorm'; 5 | 6 | type IsIDExistPipeType = (options: { 7 | entity: EntityClassOrSchema; 8 | filterField?: string; 9 | relations?: FindOptionsRelations; 10 | }) => any; 11 | 12 | // To solve mixin issue of class returned by function you refer below link 13 | // https://github.com/microsoft/TypeScript/issues/30355#issuecomment-839834550 14 | // for now we are just going with any 15 | export const IsIDExistPipe: IsIDExistPipeType = ({ 16 | entity, 17 | filterField = 'id', 18 | relations, 19 | }) => { 20 | @Injectable() 21 | class IsIDExistMixinPipe implements PipeTransform { 22 | protected exceptionFactory: (error: string) => any; 23 | 24 | constructor(@InjectDataSource() private dataSource: DataSource) {} 25 | 26 | async transform(value: string) { 27 | const repository = this.dataSource.getRepository(entity); 28 | 29 | const instance = await repository.findOne({ 30 | where: { [filterField]: value }, 31 | relations, 32 | }); 33 | if (!instance) { 34 | throw new NotFoundException( 35 | `${filterField} ${value.toString()} of ${(entity as any).name} does not exists.`, 36 | ); 37 | } 38 | return instance; 39 | } 40 | } 41 | return IsIDExistMixinPipe; 42 | }; 43 | -------------------------------------------------------------------------------- /src/utils/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ClassSerializerInterceptor, 3 | INestApplication, 4 | ValidationPipe, 5 | VersioningType, 6 | } from '@nestjs/common'; 7 | import { ConfigService } from '@nestjs/config'; 8 | import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; 9 | import { Reflector } from '@nestjs/core'; 10 | import validationOptions from './validation-options'; 11 | import * as morgan from 'morgan'; 12 | import { SerializerInterceptor } from './serializer.interceptor'; 13 | 14 | export const documentationBuilder = ( 15 | app: INestApplication, 16 | configService: ConfigService, 17 | ) => { 18 | const config = new DocumentBuilder() 19 | .addBearerAuth() 20 | .setTitle(configService.get('app.name')) 21 | .setDescription('The Danimai API description') 22 | .setVersion('1') 23 | .build(); 24 | 25 | const document = SwaggerModule.createDocument(app, config); 26 | SwaggerModule.setup('docs', app, document); 27 | }; 28 | 29 | export const createApplication = (app: INestApplication) => { 30 | app.enableShutdownHooks(); 31 | app.enableVersioning({ 32 | type: VersioningType.URI, 33 | }); 34 | app.useGlobalInterceptors( 35 | new SerializerInterceptor(), 36 | new ClassSerializerInterceptor(app.get(Reflector)), 37 | ); 38 | app.useGlobalPipes(new ValidationPipe(validationOptions)); 39 | 40 | app.use(morgan('dev')); 41 | 42 | return app; 43 | }; 44 | -------------------------------------------------------------------------------- /src/utils/serializer.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Injectable, 3 | NestInterceptor, 4 | ExecutionContext, 5 | CallHandler, 6 | } from '@nestjs/common'; 7 | import { Observable } from 'rxjs'; 8 | 9 | @Injectable() 10 | export class SerializerInterceptor implements NestInterceptor { 11 | intercept(context: ExecutionContext, next: CallHandler): Observable { 12 | return next.handle().pipe(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/validation-options.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HttpException, 3 | HttpStatus, 4 | ValidationError, 5 | ValidationPipeOptions, 6 | } from '@nestjs/common'; 7 | 8 | const validationOptions: ValidationPipeOptions = { 9 | transform: true, 10 | whitelist: true, 11 | errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, 12 | exceptionFactory: (errors: ValidationError[]) => 13 | new HttpException( 14 | { 15 | status: HttpStatus.UNPROCESSABLE_ENTITY, 16 | errors: errors.reduce( 17 | (accumulator, currentValue) => ({ 18 | ...accumulator, 19 | [currentValue.property]: Object.values( 20 | currentValue.constraints, 21 | ).join(', '), 22 | }), 23 | {}, 24 | ), 25 | }, 26 | HttpStatus.UNPROCESSABLE_ENTITY, 27 | ), 28 | }; 29 | 30 | export default validationOptions; 31 | -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "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 | --------------------------------------------------------------------------------