├── .dockerignore
├── .env.ex
├── .env.test
├── .eslintrc.js
├── .gitignore
├── Dockerfile
├── README.md
├── config-env.sh
├── docker-compose.yml
├── github
└── swagger.png
├── nest-cli.json
├── package.json
├── pnpm-lock.yaml
├── prisma
└── schema.prisma
├── src
├── app.module.ts
├── configuration.ts
├── document.ts
├── main.ts
├── modules
│ ├── auth
│ │ ├── __test__
│ │ │ └── auth.service.spec.ts
│ │ ├── auth.controller.ts
│ │ ├── auth.module.ts
│ │ ├── auth.service.ts
│ │ ├── constants
│ │ │ └── jwt.constant.ts
│ │ ├── dtos
│ │ │ ├── signin.dto.ts
│ │ │ └── signup.dto.ts
│ │ └── strategy
│ │ │ └── jwt.strategy.ts
│ ├── categories
│ │ ├── categories.controller.ts
│ │ ├── categories.module.ts
│ │ ├── categories.repository.ts
│ │ ├── categories.service.ts
│ │ └── dtos
│ │ │ ├── create.dto.ts
│ │ │ └── update.dto.ts
│ ├── comments
│ │ ├── __test__
│ │ │ └── comments.service.spec.ts
│ │ ├── comments.controller.ts
│ │ ├── comments.module.ts
│ │ ├── comments.repository.ts
│ │ ├── comments.service.ts
│ │ └── dtos
│ │ │ ├── create.dto.ts
│ │ │ └── query.dto.ts
│ ├── logging
│ │ ├── __test__
│ │ │ └── loggingService.spec.ts
│ │ ├── constants
│ │ │ └── dep_keys.constant.ts
│ │ ├── loggers
│ │ │ └── console.logger.ts
│ │ ├── logging.module.ts
│ │ └── logging.service.ts
│ ├── mail
│ │ ├── mail.module.ts
│ │ └── mail.service.ts
│ ├── post
│ │ ├── __test__
│ │ │ └── post.service.spec.ts
│ │ ├── dtos
│ │ │ ├── createPost.dto.ts
│ │ │ ├── search.dto.ts
│ │ │ └── updatePost.dto.ts
│ │ ├── models
│ │ │ ├── author.model.ts
│ │ │ └── post.model.ts
│ │ ├── post.controller.ts
│ │ ├── post.module.ts
│ │ ├── post.repository.ts
│ │ ├── post.resolver.ts
│ │ ├── post.service.ts
│ │ └── post.utility.ts
│ ├── prisma
│ │ ├── prisma.module.ts
│ │ └── prisma.service.ts
│ ├── queues
│ │ ├── consumers
│ │ │ ├── delete-file.consumer.ts
│ │ │ ├── reSize-file.consumer.ts
│ │ │ └── send-welcome.consumer.ts
│ │ └── queues.module.ts
│ ├── upload
│ │ ├── filters
│ │ │ └── post.filter.ts
│ │ ├── resize.service.ts
│ │ ├── upload.controller.ts
│ │ ├── upload.module.ts
│ │ ├── upload.service.ts
│ │ └── upload.storages.ts
│ └── users
│ │ ├── __test__
│ │ └── users.service.spec.ts
│ │ ├── dtos
│ │ └── role.dto.ts
│ │ ├── users.controller.ts
│ │ ├── users.module.ts
│ │ ├── users.repository.ts
│ │ └── users.service.ts
└── shared
│ ├── config
│ └── multer.config.ts
│ ├── constants
│ ├── mail.constant.ts
│ ├── messages.constant.ts
│ └── queues.constant.ts
│ ├── decorators
│ ├── api-File.decorator.ts
│ └── req-user.decorator.ts
│ ├── guards
│ ├── auth.guard.ts
│ └── check-roles.guard.ts
│ ├── interceptors
│ └── response.interceptor.ts
│ ├── interfaces
│ ├── categories.interface.ts
│ ├── comment.interface.ts
│ ├── mail.interface.ts
│ ├── messageLogger.interface.ts
│ ├── post.interface.ts
│ ├── queues.interface.ts
│ ├── repository.interface.ts
│ ├── role.interface.ts
│ └── user.interface.ts
│ └── utils
│ └── fileValidator.util.ts
├── templates
└── welcome.ejs
├── test
├── auth.e2e-spec.ts
├── config.ts
├── data
│ └── user.ts
├── fixtures
│ ├── createJwt.fixture.ts
│ ├── createUser.fixture.ts
│ └── startapp.fixture.ts
├── jest-e2e.config.ts
├── jest-e2e.json
└── users.e2e-spec.ts
├── tsconfig.build.json
└── tsconfig.json
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .dockerignore
3 | Dockerfile
4 |
--------------------------------------------------------------------------------
/.env.ex:
--------------------------------------------------------------------------------
1 | EMAIL_HOST= # you can use https://mailtrap.io/
2 | EMAIL_PORT= # you can use https://mailtrap.io/
3 | EMAIL_USER= # you can use https://mailtrap.io/
4 | EMAIL_PASS= # you can use https://mailtrap.io/
5 | APP_MODE=development #or production
6 | DATABASE_URL="mysql://...."
7 | REDIS_URL="...."
--------------------------------------------------------------------------------
/.env.test:
--------------------------------------------------------------------------------
1 | DATABASE_URL="mysql://test:test@localhost:3308/test"
2 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: '@typescript-eslint/parser',
3 | parserOptions: {
4 | project: 'tsconfig.json',
5 | sourceType: 'module',
6 | },
7 | plugins: ['@typescript-eslint/eslint-plugin'],
8 | extends: [
9 | 'plugin:@typescript-eslint/recommended',
10 | 'plugin:prettier/recommended',
11 | ],
12 | root: true,
13 | env: {
14 | node: true,
15 | jest: true,
16 | },
17 | ignorePatterns: ['.eslintrc.js'],
18 | rules: {
19 | '@typescript-eslint/interface-name-prefix': 'off',
20 | '@typescript-eslint/explicit-function-return-type': 'off',
21 | '@typescript-eslint/explicit-module-boundary-types': 'off',
22 | '@typescript-eslint/no-explicit-any': 'off',
23 | },
24 | };
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # compiled output
2 | /dist
3 | /node_modules
4 | .vscode
5 | # Logs
6 | logs
7 | *.log
8 | npm-debug.log*
9 | pnpm-debug.log*
10 | yarn-debug.log*
11 | yarn-error.log*
12 | lerna-debug.log*
13 |
14 | # OS
15 | .DS_Store
16 |
17 | uploads/*
18 | package-lock.json
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 | db-data
40 | my-uploads
41 | .env
42 | src/schema.gql
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:16-alpine
2 |
3 | WORKDIR /usr/blog
4 |
5 | RUN apk add --update --no-cache openssl1.1-compat
6 |
7 | COPY package.json ./
8 | COPY pnpm-lock.yaml ./
9 |
10 |
11 |
12 | RUN npm install pnpm -g; \
13 | pnpm install
14 |
15 | COPY . ./
16 |
17 |
18 | CMD ["npm","run","start"]
19 |
--------------------------------------------------------------------------------
/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 |
9 | Blog - NestJS
10 |
11 |
12 | ## ⚗️technologies
13 |
14 |
15 | -
TypeScript
16 | -
NestJS
17 | -
prisma.io(ORM)
18 | -
GraphQl
19 | -
Queue system
20 | - 📒 Swagger
21 | - 📧 Nodemailer
22 | -
Docker & Docker-Compose
23 | -
Testing (Unit/E2E)
24 |
25 |
26 | # 🤗 Easy To Run !!
27 | ```bash
28 | # just run bash file
29 | $ ./config-env.sh
30 | ```
31 |
32 | # 📦 Docker
33 | ```bash
34 | docker-compose --profile product up -d
35 | > Host port 5000
36 | ```
37 |
38 |
39 | ## 📥Installation
40 |
41 | ```bash
42 | $ npm install
43 | ```
44 |
45 | ## ⚙️Running the app
46 |
47 | ```bash
48 |
49 | # development
50 | $ npm run start
51 |
52 | # watch mode
53 | $ npm run start:dev
54 |
55 | # production mode
56 | $ npm run start:prod
57 |
58 | # e2e testing
59 | $ npm run test:e2e
60 | ```
61 | # 📝documents
62 |
63 | ### Database Diagram :
64 | - https://dbdiagram.io/d/636413aec9abfc6111702982
65 |
66 |
67 | ### swagger-ui
68 | - http://localhost:{port}/api
69 |
70 | 
71 |
--------------------------------------------------------------------------------
/config-env.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | red=$(tput setaf 1)
3 | green=$(tput setaf 2)
4 | reset=$(tput sgr0)
5 | space() {
6 | for ((i = 1; i <= $1; i++)); do
7 | echo
8 | done
9 | }
10 |
11 | echo " Blog Web API with NestJs "
12 | space 5
13 | echo "config .env file"
14 | space 3
15 |
16 | echo "Config Email Service (you can use https://mailtrap.io/)"
17 | echo
18 | read -p "Enter EMAIL_HOST: " EMAIL_HOST
19 | read -p "Enter EMAIL_PORT: " EMAIL_PORT
20 | read -p "Enter EMAIL_USER: " EMAIL_USER
21 | read -p "Enter EMAIL_PASS: " EMAIL_PASS
22 |
23 | space 2
24 | echo "Config Database's"
25 | space 2
26 | read -p "Enter MySQL database name: " MYSQL_DATABASE
27 | read -p "Enter MySQL user: " MYSQL_USER
28 | read -p "Enter MySQL password: " MYSQL_PASSWORD
29 | read -p "Enter MySqL port(press Enter for default 3306):" MYSQL_PORT
30 | MYSQL_PORT=${MYSQL_PORT:-3306}
31 | MYSQL_ROOT_PASSWORD=$(openssl rand -base64 32)
32 | echo "generated Random password for root"
33 | space 3
34 | read -p "Enter Redis url: " REDIS_URL
35 | space 2
36 | read -p "Enter port number (press Enter for default 3000): " PORT
37 | PORT=${PORT:-3000}
38 | read -p "Enter app mode[development,production] (press Enter for development): " APP_MODE
39 | APP_MODE=${APP_MODE:-DEVELOPMENT}
40 |
41 | echo "EMAIL_HOST=$EMAIL_HOST" >.env
42 | echo "EMAIL_PORT=$EMAIL_PORT" >>.env
43 | echo "EMAIL_USER=$EMAIL_USER" >>.env
44 | echo "EMAIL_PASS=$EMAIL_PASS" >>.env
45 | echo "DATABASE_URL=mysql://$MYSQL_USER:$MYSQL_PASSWORD@mysqldb:$MYSQL_PORT/$MYSQL_DATABASE" >>.env
46 | echo "REDIS_URL=$REDIS_URL" >>.env
47 | echo "PORT=$PORT" >>.env
48 | echo "APP_MODE=$APP_MODE" >>.env
49 | echo "MYSQL_USER=$MYSQL_USER" >>.env
50 | echo "MYSQL_PASSWORD=$MYSQL_PASSWORD" >>.env
51 | echo "MYSQL_DATABASE=$MYSQL_DATABASE" >>.env
52 | echo "MYSQL_PORT=$MYSQL_PORT" >>.env
53 | echo "MYSQL_ROOT_PASSWORD=$(openssl rand -hex 12)" >>.env
54 | echo "JWT_SECRET=$(openssl rand -hex 12)" >>.env
55 | echo "${green}Values saved to .env file${reset}"
56 | space 3
57 |
58 | read -p "Do you want to run '${green}docker-compose --profile product up -d${reset}' automatically? [y/n]: " REPLY
59 |
60 | if [[ $REPLY =~ ^[Yy]$ ]]; then
61 | # Automatically run `docker-compose up -d`
62 | docker-compose --profile product up -d
63 | else
64 | # Prompt the user to run `docker-compose up -d` manually
65 | echo "Run 'docker-compose --profile product up -d' manually to start the application. ${red}[Exiting...]${reset}"
66 | sleep 3
67 | fi
68 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.8'
2 |
3 | services:
4 |
5 | redis:
6 | image: redis
7 | expose:
8 | - 6379
9 | profiles:
10 | - product
11 |
12 | app:
13 | build:
14 | context: .
15 | dockerfile: Dockerfile
16 | depends_on:
17 | - redis
18 | - mysqldb
19 | env_file:
20 | - .env
21 | ports:
22 | - "${PORT}:${PORT}"
23 | environment:
24 | DATABASE_URL: "mysql://${MYSQL_USER}:${MYSQL_PASSWORD}@mysqldb:3306/${MYSQL_DATABASE}"
25 | REDIS_URL: redis
26 | volumes:
27 | - ./my-uploads:/usr/blog/uploads
28 | profiles:
29 | - product
30 |
31 | mysqldb:
32 | image: mysql:5.7
33 | env_file:
34 | - .env
35 | ports:
36 | - "${MYSQL_PORT}:3306"
37 | expose:
38 | - 3306
39 | environment:
40 | MYSQL_DATABASE: "${MYSQL_DATABASE}"
41 | MYSQL_USER: "${MYSQL_USER}"
42 | MYSQL_PASSWORD: "${MYSQL_PASSWORD}"
43 | MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD}"
44 | SERVICE_TAGS: prod
45 | SERVICE_NAME: mysqldb
46 | volumes:
47 | - ./db-data:/var/lib/mysql
48 | profiles:
49 | - product
50 |
51 | mysqldb-test:
52 | image: mysql:5.7
53 | ports:
54 | - '3308:3306'
55 | environment:
56 | MYSQL_DATABASE: blog
57 | MYSQL_USER: prisma
58 | MYSQL_PASSWORD: prisma
59 | MYSQL_ROOT_PASSWORD: prisma
60 | SERVICE_TAGS: prod
61 | SERVICE_NAME: mysqldb
62 | profiles:
63 | - test
64 |
65 | volumes:
66 | mysql-data:
67 | my-uploads:
68 |
--------------------------------------------------------------------------------
/github/swagger.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sajjadmrx/Blog-nestJs/3786691be8a9aea5419fc30dd50ce2ab7f0e18ef/github/swagger.png
--------------------------------------------------------------------------------
/nest-cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "collection": "@nestjs/schematics",
3 | "sourceRoot": "src"
4 | }
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "blog-nestjs",
3 | "version": "0.0.1",
4 | "description": "",
5 | "author": "",
6 | "private": true,
7 | "license": "UNLICENSED",
8 | "scripts": {
9 | "prebuild": "rimraf dist",
10 | "build": "nest build",
11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
12 | "start": "npx prisma generate && npx prisma db push && nest start",
13 | "start:dev": "nest start --watch",
14 | "start:debug": "nest start --debug --watch",
15 | "start:prod": "node dist/main",
16 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
17 | "test": "jest",
18 | "test:watch": "jest --watch",
19 | "test:cov": "jest --coverage",
20 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
21 | "prisma:test:reset": "dotenv -e ./.env.test -- prisma migrate reset --force && dotenv -e ./.env.test -- prisma db push",
22 | "test:e2e": "dotenv -e .env.test -- npx prisma db push && dotenv -e .env.test -- jest --config ./test/jest-e2e.json"
23 | },
24 | "dependencies": {
25 | "@nestjs-modules/mailer": "^1.8.1",
26 | "@nestjs/apollo": "^10.1.6",
27 | "@nestjs/bull": "^0.6.0",
28 | "@nestjs/common": "9.3.9",
29 | "@nestjs/config": "^2.2.0",
30 | "@nestjs/core": "9.3.9",
31 | "@nestjs/graphql": "^10.1.6",
32 | "@nestjs/jwt": "^8.0.0",
33 | "@nestjs/passport": "^8.2.1",
34 | "@nestjs/platform-express": "^8.0.0",
35 | "@nestjs/swagger": "^5.2.1",
36 | "@prisma/client": "4.6.1",
37 | "apollo-server-express": "^3.11.1",
38 | "bcrypt": "^5.0.1",
39 | "best-string": "^1.0.0",
40 | "bull": "^4.8.5",
41 | "chalk": "4.1.2",
42 | "class-transformer": "^0.5.1",
43 | "class-validator": "^0.13.2",
44 | "dotenv-cli": "^6.0.0",
45 | "graphql": "^16.6.0",
46 | "multer": "1.4.5-lts.1",
47 | "nanoid": "^3.3.4",
48 | "nodemailer": "^6.7.7",
49 | "passport": "^0.5.2",
50 | "passport-jwt": "^4.0.0",
51 | "prisma": "4.2.0",
52 | "reflect-metadata": "^0.1.13",
53 | "rimraf": "^3.0.2",
54 | "rxjs": "^7.2.0",
55 | "sharp": "^0.30.4",
56 | "swagger-ui-express": "^4.3.0",
57 | "typeorm": "^0.2.44",
58 | "uuid": "^9.0.0"
59 | },
60 | "devDependencies": {
61 | "@jest/types": "^29.3.1",
62 | "@nestjs/cli": "^8.0.0",
63 | "@nestjs/schematics": "^8.0.0",
64 | "@nestjs/testing": "^8.4.7",
65 | "@types/bcrypt": "^5.0.0",
66 | "@types/bull": "^3.15.9",
67 | "@types/express": "^4.17.13",
68 | "@types/jest": "27.0.2",
69 | "@types/multer": "^1.4.7",
70 | "@types/node": "^16.0.0",
71 | "@types/sharp": "^0.30.2",
72 | "@types/supertest": "^2.0.12",
73 | "@typescript-eslint/eslint-plugin": "^5.0.0",
74 | "@typescript-eslint/parser": "^5.0.0",
75 | "eslint": "^8.0.1",
76 | "eslint-config-prettier": "^8.3.0",
77 | "eslint-plugin-prettier": "^4.0.0",
78 | "jest": "^27.2.5",
79 | "jest-mock-extended": "2.0.4",
80 | "prettier": "^2.3.2",
81 | "source-map-support": "^0.5.20",
82 | "supertest": "^6.2.4",
83 | "ts-jest": "^27.0.3",
84 | "ts-loader": "^9.2.3",
85 | "ts-node": "^10.0.0",
86 | "tsconfig-paths": "^3.10.1",
87 | "typescript": "^4.3.5"
88 | },
89 | "jest": {
90 | "moduleFileExtensions": [
91 | "js",
92 | "json",
93 | "ts"
94 | ],
95 | "rootDir": "src",
96 | "testRegex": ".*\\.spec\\.ts$",
97 | "transform": {
98 | "^.+\\.(t|j)s$": "ts-jest"
99 | },
100 | "collectCoverageFrom": [
101 | "**/*.(t|j)s"
102 | ],
103 | "moduleNameMapper": {
104 | "^src/(.*)$": "/$1"
105 | },
106 | "verbose": true,
107 | "testTimeout": 1000000,
108 | "coverageDirectory": "../coverage",
109 | "testEnvironment": "node"
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | // This is your Prisma schema file,
2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema
3 |
4 | generator client {
5 | provider = "prisma-client-js"
6 | }
7 |
8 | datasource db {
9 | provider = "mysql"
10 | url = env("DATABASE_URL")
11 | }
12 |
13 | enum Role {
14 | ADMIN
15 | MANAGE_POSTS
16 | MANAGE_COMMENTS
17 | USER
18 | }
19 |
20 | model User {
21 | id Int @id @default(autoincrement())
22 | posts Post[]
23 | username String @unique
24 | email String @unique
25 | password String
26 | role Role @default(USER)
27 | createdAt DateTime @default(now())
28 | updatedAt DateTime @updatedAt
29 | Comment Comment[]
30 | }
31 |
32 | model Post {
33 | id Int @id @default(autoincrement())
34 | title String
35 | content String
36 | author User @relation(fields: [authorId], references: [id],onDelete: Cascade, onUpdate: Cascade)
37 | authorId Int
38 | published Boolean
39 | cover String //@default("/images/default-post.png")
40 | createdAt DateTime @default(now())
41 | updatedAt DateTime @updatedAt
42 | Comment Comment[]
43 | categories CategoriesOnPosts[]
44 | tags String @default("[]")
45 | }
46 |
47 | model Category {
48 | id Int @id @unique @default(autoincrement())
49 | name String
50 | slug String @unique
51 | createdAt DateTime @default(now())
52 | updatedAt DateTime @updatedAt
53 | // Post Post? @relation(fields: [postId], references: [id])
54 | Post CategoriesOnPosts[]
55 | }
56 |
57 | model CategoriesOnPosts {
58 | post Post @relation(fields: [postId], references: [id],onDelete: Cascade, onUpdate: Cascade)
59 | postId Int // relation scalar field (used in the `@relation` attribute above)
60 | category Category @relation(fields: [categoryId], references: [id],onDelete: Cascade, onUpdate: Cascade)
61 | categoryId Int // relation scalar field (used in the `@relation` attribute above)
62 | assignedAt DateTime @default(now())
63 |
64 | @@id([postId, categoryId])
65 | }
66 |
67 | model Comment {
68 | id Int @id @default(autoincrement())
69 | text String
70 | author User @relation(fields: [authorId], references: [id],onDelete: Cascade, onUpdate: Cascade )
71 | authorId Int
72 | post Post @relation(fields: [postId], references: [id],onDelete: Cascade, onUpdate: Cascade )
73 | postId Int
74 | createdAt DateTime @default(now())
75 | updatedAt DateTime @updatedAt
76 | replyId Int?
77 | reply Comment? @relation("replies", fields: [replyId], references: [id],onDelete: Cascade, onUpdate: Cascade)
78 | childs Comment[] @relation("replies") //https://www.prisma.io/docs/concepts/components/prisma-schema/relations/self-relations
79 | }
80 |
--------------------------------------------------------------------------------
/src/app.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from "@nestjs/common";
2 |
3 | import { AuthModule } from "./modules/auth/auth.module";
4 | import { CategoriesModule } from "./modules/categories/categories.module";
5 | import { PostModule } from "./modules/post/post.module";
6 | import { PrismaModule } from "./modules/prisma/prisma.module";
7 | import { UploadModule } from "./modules/upload/upload.module";
8 | import { UserModule } from "./modules/users/users.module";
9 | import { MailModule } from "./modules/mail/mail.module";
10 | import { ConfigModule, ConfigService } from "@nestjs/config";
11 | import Configuration from "./configuration";
12 | import { QueuesModule } from "./modules/queues/queues.module";
13 | import { CommentsModule } from "./modules/comments/comments.module";
14 | import { GraphQLModule } from "@nestjs/graphql";
15 | import { ApolloDriver, ApolloDriverConfig } from "@nestjs/apollo";
16 | import { join } from "path";
17 |
18 | @Module({
19 | imports: [
20 | ConfigModule.forRoot({
21 | load: [Configuration],
22 | isGlobal: true,
23 | }),
24 | QueuesModule,
25 | PrismaModule,
26 | AuthModule,
27 | UserModule,
28 | PostModule,
29 | UploadModule,
30 | CategoriesModule,
31 | MailModule,
32 | CommentsModule,
33 | GraphQLModule.forRoot({
34 | driver: ApolloDriver,
35 | autoSchemaFile: join(process.cwd(), "src/schema.gql"),
36 | }),
37 | ],
38 | controllers: [],
39 | providers: [],
40 | })
41 | export class AppModule {}
42 |
--------------------------------------------------------------------------------
/src/configuration.ts:
--------------------------------------------------------------------------------
1 | export interface Configs {
2 | PORT: number;
3 | DATABASE_URL: string;
4 | EMAIL_HOST: string;
5 | EMAIL_PORT: string;
6 | EMAIL_USER: string;
7 | EMAIL_PASS: string;
8 | REDIS_URL: string;
9 | APP_MODE: string;
10 | JWT_SECRET: string;
11 | }
12 | export default (): Configs => ({
13 | PORT: Number(process.env.PORT),
14 | DATABASE_URL: process.env.DATABASE_URL,
15 | EMAIL_HOST: process.env.EMAIL_HOST,
16 | EMAIL_PORT: process.env.EMAIL_PORT,
17 | EMAIL_USER: process.env.EMAIL_USER,
18 | EMAIL_PASS: process.env.EMAIL_PASS,
19 | REDIS_URL: process.env.REDIS_URL,
20 | APP_MODE: process.env.APP_MODE,
21 | JWT_SECRET: process.env.JWT_SECRET,
22 | });
23 |
--------------------------------------------------------------------------------
/src/document.ts:
--------------------------------------------------------------------------------
1 | import { INestApplication } from "@nestjs/common";
2 | import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
3 |
4 | export function setupDocument(app: INestApplication, route: string) {
5 | const configDocument = new DocumentBuilder()
6 | .setTitle("Blog - NestJS")
7 | .setDescription("The Blog API description")
8 | .setVersion("1.0")
9 | .addBearerAuth({
10 | type: "http",
11 | scheme: "bearer",
12 | bearerFormat: "JWT",
13 | })
14 | .build();
15 |
16 | const document = SwaggerModule.createDocument(app, configDocument);
17 | SwaggerModule.setup(route, app, document);
18 | }
19 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { ValidationPipe } from "@nestjs/common";
2 | import { NestFactory } from "@nestjs/core";
3 | import { AppModule } from "./app.module";
4 | import { NestExpressApplication } from "@nestjs/platform-express";
5 | import { setupDocument } from "./document";
6 | import { ConfigService } from "@nestjs/config";
7 | import { Configs } from "./configuration";
8 |
9 | (async () => {
10 | const app = await NestFactory.create(AppModule, {
11 | cors: true,
12 | });
13 |
14 | app.useGlobalPipes(new ValidationPipe());
15 |
16 | app.setGlobalPrefix("api/v1");
17 |
18 | app.useStaticAssets("uploads", {
19 | prefix: "/uploads/",
20 | });
21 |
22 | const configService: ConfigService = new ConfigService();
23 |
24 | const port = configService.get("PORT") || 3000;
25 |
26 | const isDevelopmentMode: boolean =
27 | configService.get("APP_MODE").toUpperCase() == "DEVELOPMENT";
28 |
29 | const DOCUMENT_ROUTE: string = "/api";
30 |
31 | if (isDevelopmentMode) setupDocument(app, DOCUMENT_ROUTE);
32 |
33 | await app.listen(port);
34 |
35 | console.log(`Server running on ${port}`);
36 |
37 | const appUrl: string = isDevelopmentMode
38 | ? `http://127.0.0.1:${port}`
39 | : await app.getUrl();
40 |
41 | console.log(`GraphQl: ${appUrl}/graphql`);
42 |
43 | isDevelopmentMode &&
44 | console.log(`RestApi: http://localhost:${port}${DOCUMENT_ROUTE}`);
45 | })();
46 |
--------------------------------------------------------------------------------
/src/modules/auth/__test__/auth.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test } from "@nestjs/testing";
2 | import { AuthService } from "../auth.service";
3 | import { JwtModule, JwtService } from "@nestjs/jwt";
4 | import { Prisma, User } from "@prisma/client";
5 | import { UsersRepository } from "../../users/users.repository";
6 | import { Queue } from "bull";
7 | import { welcomeEmailQueue } from "../../../shared/interfaces/queues.interface";
8 | import { BadRequestException, UnauthorizedException } from "@nestjs/common";
9 | import bcrypt, { compare } from "bcrypt";
10 | const user: User = {
11 | username: "mrx",
12 | email: "mrx@gmail.com",
13 | role: "ADMIN",
14 | id: 1,
15 | password: "hash",
16 | createdAt: new Date(),
17 | updatedAt: new Date(),
18 | };
19 |
20 | const input = {
21 | username: "mrx",
22 | email: "mrx@email.com",
23 | password: "ok",
24 | };
25 |
26 | describe("AuthService", function () {
27 | let authService: AuthService;
28 | let usersRepository: UsersRepository;
29 | let jwtService: JwtService;
30 | let sendWelcomeEmailQueue;
31 | beforeEach(async () => {
32 | usersRepository = new UsersRepository(jest.fn() as unknown as any);
33 | jwtService = new JwtService();
34 | sendWelcomeEmailQueue = {
35 | add: jest.fn(),
36 | } as unknown as Queue;
37 | authService = new AuthService(
38 | usersRepository,
39 | jwtService,
40 | sendWelcomeEmailQueue,
41 | jest.fn() as any
42 | );
43 | });
44 |
45 | it("should Defined", () => {
46 | expect(authService).toBeDefined();
47 | });
48 |
49 | describe("signUp", function () {
50 | it("should throw error user exist", async () => {
51 | jest
52 | .spyOn(usersRepository, "findByEmailOrUsername")
53 | .mockImplementation(async (email: string, username: string) => [user]);
54 |
55 | await expect(authService.signUp(input)).rejects.toEqual(
56 | new BadRequestException("Email or username already exist")
57 | );
58 | });
59 |
60 | it("should create User & return jwt code", async () => {
61 | jest
62 | .spyOn(usersRepository, "findByEmailOrUsername")
63 | .mockImplementation(async (email: string, username: string) => []);
64 | jest
65 | .spyOn(usersRepository, "create")
66 | .mockImplementation(async () => user);
67 | const token = "abcdvdfsminewrhnfqbetwetfiw";
68 | jest.spyOn(jwtService, "sign").mockReturnValue(token);
69 |
70 | await expect(authService.signUp(input)).resolves.toBe(token);
71 | });
72 |
73 | it("should add welcome email to queue", async () => {
74 | jest
75 | .spyOn(usersRepository, "findByEmailOrUsername")
76 | .mockImplementation(async (email: string, username: string) => []);
77 | jest
78 | .spyOn(usersRepository, "create")
79 | .mockImplementation(async () => user);
80 | const token = "abcdvdfsminewrhnfqbetwetfiw";
81 | jest.spyOn(jwtService, "sign").mockReturnValue(token);
82 |
83 | await authService.signUp(input);
84 | await expect(sendWelcomeEmailQueue.add).toBeCalled();
85 | });
86 | });
87 |
88 | describe("signIn", function () {
89 | it("should reject 'invalid credentials' when user not found", async () => {
90 | jest
91 | .spyOn(usersRepository, "findOneByUsername")
92 | .mockImplementation(() => null);
93 | await expect(
94 | authService.signIn({
95 | username: input.username,
96 | password: input.password,
97 | })
98 | ).rejects.toEqual(new UnauthorizedException("invalid credentials"));
99 | });
100 | it("should reject 'invalid credentials' when password not Matching", async () => {
101 | jest
102 | .spyOn(usersRepository, "findOneByUsername")
103 | .mockImplementation(() => Promise.resolve(user));
104 | jest
105 | .spyOn(bcrypt, "compare")
106 | .mockImplementation(async (pass: string, hash: string) =>
107 | Promise.resolve(false)
108 | );
109 | await expect(
110 | authService.signIn({
111 | username: input.username,
112 | password: input.password,
113 | })
114 | ).rejects.toEqual(new UnauthorizedException("invalid credentials"));
115 | });
116 | it("should return jwt code when password is Matching", async () => {
117 | jest
118 | .spyOn(usersRepository, "findOneByUsername")
119 | .mockImplementation(async () => user);
120 |
121 | jest.spyOn(bcrypt, "compare").mockImplementation(async () => true);
122 |
123 | jest.spyOn(jwtService, "sign").mockImplementation(() => "token");
124 |
125 | await expect(authService.signIn(input)).resolves.toBe("token");
126 | });
127 | });
128 | });
129 |
--------------------------------------------------------------------------------
/src/modules/auth/auth.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Body,
3 | Controller,
4 | HttpCode,
5 | Post,
6 | UseInterceptors,
7 | } from "@nestjs/common";
8 | import { ApiOperation, ApiTags } from "@nestjs/swagger";
9 | import { SignInDto } from "src/modules/auth/dtos/signin.dto";
10 | import { AuthService } from "./auth.service";
11 | import { SignUpDto } from "./dtos/signup.dto";
12 | import { ResponseInterceptor } from "../../shared/interceptors/response.interceptor";
13 |
14 | @ApiTags("Auth")
15 | @UseInterceptors(ResponseInterceptor)
16 | @Controller("auth")
17 | export class AuthController {
18 | constructor(private authService: AuthService) {}
19 |
20 | @ApiOperation({
21 | summary: "signup",
22 | })
23 | @Post("signup")
24 | async signup(@Body() body: SignUpDto): Promise