├── .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 |
3 |
4 |
5 | [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
6 | [circleci-url]: https://circleci.com/gh/nestjs/nest
7 |
8 | A progressive Node.js framework for building efficient and scalable server-side applications.
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
24 |
25 | ## Description
26 |
27 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
28 |
29 | ## Installation
30 |
31 | ```bash
32 | $ npm install
33 | ```
34 |
35 | ## Running the app
36 |
37 | ```bash
38 | # development
39 | $ npm run start
40 |
41 | # watch mode
42 | $ npm run start:dev
43 |
44 | # production mode
45 | $ npm run start:prod
46 | ```
47 |
48 | ## Test
49 |
50 | ```bash
51 | # unit tests
52 | $ npm run test
53 |
54 | # e2e tests
55 | $ npm run test:e2e
56 |
57 | # test coverage
58 | $ npm run test:cov
59 | ```
60 |
61 | ## Support
62 |
63 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
64 |
65 | ## Stay in touch
66 |
67 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
68 | - Website - [https://nestjs.com](https://nestjs.com/)
69 | - Twitter - [@nestframework](https://twitter.com/nestframework)
70 |
71 | ## License
72 |
73 | Nest is [MIT licensed](LICENSE).
74 |
--------------------------------------------------------------------------------
/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 |
14 | {{app_name}}
15 | |
16 |
17 |
18 |
19 | Thank You for registration, Please verify to activate account.
20 | |
21 |
22 |
23 |
24 | {{actionTitle}}
28 | |
29 |
30 |
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 |
--------------------------------------------------------------------------------