├── .dockerignore ├── .env.example ├── .gitignore ├── Dockerfile ├── README.md ├── __test__ └── util.test.js ├── app.js ├── controllers ├── blog.controller.js ├── category.controller.js └── user.controller.js ├── docker-compose.yml ├── docs ├── blog │ ├── commentToBlog.md │ ├── createBlog.md │ ├── deleteCommentFromBlog.md │ ├── getBlogsOfSelectedCategory.md │ ├── getDetailsOfBlog.md │ ├── getLIstofAllBlogsWithPagination.md │ └── reactToBlog.md ├── category │ ├── createCategory.md │ ├── getListOfCategories.md │ └── getListOfCategoriezedBlogs.md └── user │ ├── editUserProfile.md │ ├── getBloggersInfo.md │ ├── getLoggedInUserInfo.md │ ├── login.md │ ├── refreshTokens.md │ └── register.md ├── helpers ├── cloudinary.helper.js ├── jwt.helper.js └── mongodb.helper.js ├── middlewares └── user.middleware.js ├── models ├── blog.model.js ├── category.model.js └── user.model.js ├── package-lock.json ├── package.json ├── routes ├── blog.route.js ├── category.route.js └── user.route.js ├── server.js ├── services ├── blog.service.js ├── category.service.js └── user.service.js ├── uploads └── blogs │ └── default.jpg ├── util └── index.js └── validators └── user.validator.js /.dockerignore: -------------------------------------------------------------------------------- 1 | ./node_modules 2 | Dockerfile 3 | .dockerignore 4 | docker-compose.yml -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | PORT = 3000 2 | MONGODB_URL = yourMongoDB_URL/database_name 3 | 4 | accessTokenKey = someSuperSecretAccessTokenKey 5 | refreshTokenKey = anotherSuperSecretRefreshTokenKey 6 | 7 | CLOUD_NAME = yourcloudname 8 | CLOUDINARY_API_KEY = yourcloudinaryapikey 9 | API_SECRET = yourcloudinaryapisecret -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:alpine 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY package*.json . 6 | 7 | RUN npm ci 8 | 9 | COPY . . 10 | 11 | CMD ["npm", "run", "dev"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # blog-website-backend-nodejs-REST-API 2 | 3 | **blog-website** is one of my personal projects where registered bloggers write blogs. Bloggers can also comment or react to blogs. This repository holds the code of it's backend which is a **RESTful API**. 4 | 5 | The frontend of this project can be found [here (Angular)](https://github.com/tazbin/blog-website-frontend_Angular) 6 | 7 | Visit complete live project [lets-blog.netlify.app/all_blogs](https://lets-blog.netlify.app/all_blogs) 8 | 9 | ### Contents 10 | 11 | - [Features](#features) 12 | - [Tech used](#tech-used) 13 | - [How to get the project](#how-to-get-the-project) 14 | - [Run the project using docker](#run-the-project-using-docker) 15 | - [API endpoints](#api-endpoints) 16 | 17 | ## Features: 18 | - bloggers can create their profiles (token-based authentication) 19 | - bloggers can edit their profile 20 | - bloggers can write blogs. They can set the category of their blog (i.e. travel, medical, tech etc) 21 | - registered bloggers can comment on their own or others blog 22 | - registered bloggers can also react on others blog. They can react **like**, **love**, **sad**, **haha**, **informative** to blogs 23 | - unregistered public users can read blogs but can't comment or react n blogs 24 | - Blogs of a particular category can be viewed 25 | 26 | ## Tech used: 27 | 28 | **Runtime environment** 29 | - [x] Node.js 30 | 31 | **Database** 32 | - [x] MongoDB 33 | 34 | **Image storage service** 35 | - [x] Cloudinary 36 | 37 | **Testing framework** 38 | - [x] Jest 39 | 40 | **Containerization tool** 41 | - [x] Docker 42 | 43 | ## How to get the project: 44 | #### Using Git (recommended) 45 | 1. Navigate & open CLI into the directory where you want to put this project & Clone this project using this command. 46 | 47 | ```bash 48 | git clone https://github.com/tazbin/blog-website-backend-nodejs-REST-API.git 49 | ``` 50 | #### Using manual download ZIP 51 | 1. Download repository 52 | 2. Extract the zip file, navigate into it & copy the folder to your desired directory 53 | 54 | ## Setting up environments 55 | 1. There is a file named `.env.example` on the root directory of the project 56 | 2. Create a new file by copying & pasting the file on the root directory & rename it to just `.env` 57 | 3. The `.env` file is already ignored, so your credentials inside it won't be committed 58 | 4. Change the values of the file. Make changes of comment to the `.env.example` file while adding new constants to the `.env` file. 59 | 60 | ## Run the project using docker 61 | 1. To build **docker image** 62 | ```bash 63 | docker compose build --no-cache 64 | ``` 65 | 66 | 2. To run the **containers** in detached mode (wait for a while for database connection) 67 | ```bash 68 | docker compose up -d 69 | ``` 70 | 71 | 3. To view running **containers** 72 | ```bash 73 | docker container ps 74 | ``` 75 | 76 | 4. To view **API logs** 77 | ```bash 78 | docker logs letsblog-api-c 79 | ``` 80 | 81 | 5. To **run tests**, first enter within the API container 82 | - on windows CMD (not switching to bash) 83 | ```bash 84 | docker exec -it letsblog-api-c /bin/sh 85 | ``` 86 | - on windows CMD (after switching to bash) 87 | ```bash 88 | docker exec -it letsblog-api-c //bin//sh 89 | ``` 90 | or 91 | ```bash 92 | winpty docker exec -it letsblog-api-c //bin//sh 93 | ``` 94 | now run **test command** 95 | ```bash 96 | npm test 97 | ``` 98 | 6. To exit from **API container**, press Ctrl+D on terminal 99 | 100 | 7. To **stop** the containers 101 | ```bash 102 | docker compose down 103 | ``` 104 | 105 | ## API endpoints: 106 | 107 | #### *Indication* 108 | - [x] **Authentication required** 109 | - [ ] **Authentication not required** 110 | 111 | ### User related 112 | - [ ] [Resgister](docs/user/register.md): `POST localhost:3000/user/register` 113 | - [ ] [Login](docs/user/login.md): `GET localhost:3000/user/login` 114 | - [x] [Edit user profile](docs/user/editUserProfile.md): `PUT localhost:3000/user/editProfile` 115 | - [x] [Refresh tokens](docs/user/refreshTokens.md): `POST localhost:3000/user/me/refresToken` 116 | - [x] [Get loggedin user's info](docs/user/getLoggedInUserInfo.md): `GET localhost:3000/user/me` 117 | - [ ] [Get blogger's info](docs/user/getBloggersInfo.md): `GET localhost:3000/user/bloggerProfile/:bloggerId` 118 | 119 | ### Category related 120 | - [x] [Create a new category](docs/category/createCategory.md): `POST localhost:3000/category` 121 | - [ ] [Get list of all categories](docs/category/getListOfCategories.md): `GET localhost:3000/category` 122 | - [ ] [Get list of all categorized blog counts of user](docs/category/getListOfCategoriezedBlogs.md): `GET localhost:3000/category/categorizedBlogs/:bloggerId` 123 | 124 | ### Blog related 125 | - [ ] [Create a new blog](docs/blog/createBlog.md): `POST localhost:3000/blog` 126 | - [ ] [Get list of all blogs with pagination of certain category of a user](docs/blog/getLIstofAllBlogsWithPagination.md): `GET localhost:3000/blog/:bloggerId?/:categoryId?` 127 | - [ ] [Get details of a blog](docs/blog/getDetailsOfBlog.md): `GET localhost:3000/blog/:blogId` 128 | - [ ] [Get list of all blogs of a selected category](docs/blog/getBlogsOfSelectedCategory.md): `GET localhost:3000/blog/category/:categoryId` 129 | - [x] [React to a blog](docs/blog/reactToBlog.md): `PUT localhost:3000/blog/react` 130 | - [x] [Comment to a blog](docs/blog/commentToBlog.md): `POST localhost:3000/blog/comment` 131 | - [x] [Delete a comment](docs/blog/deleteCommentFromBlog.md): `DELETE localhost:3000/blog/comment` 132 | -------------------------------------------------------------------------------- /__test__/util.test.js: -------------------------------------------------------------------------------- 1 | const { 2 | makeObjectSelected, 3 | makeObjectExcept 4 | } = require('../util'); 5 | 6 | describe('util test suit', () => { 7 | 8 | test('should return object with selected keys', () => { 9 | details = { 10 | name: "tazbinur", 11 | address: "jashore", 12 | age: 100, 13 | hasCar: true, 14 | visited: ["dhaka", "khulna", "barishal"] 15 | } 16 | selectedDetails = { 17 | name: "tazbinur", 18 | address: "jashore", 19 | visited: ["dhaka", "khulna", "barishal"] 20 | } 21 | result = makeObjectSelected(details, ["name", "address", "visited"]); 22 | expect(result).toStrictEqual(selectedDetails); 23 | 24 | }); 25 | 26 | test('should return object without selected keys', () => { 27 | 28 | details = { 29 | name: "tazbinur", 30 | address: "jashore", 31 | age: 100, 32 | hasCar: true, 33 | visited: ["dhaka", "khulna", "barishal"] 34 | } 35 | newDetails = { 36 | name: "tazbinur", 37 | address: "jashore", 38 | visited: ["dhaka", "khulna", "barishal"] 39 | } 40 | result = makeObjectExcept(details, ["age", "hasCar"]); 41 | expect(result).toStrictEqual(newDetails); 42 | 43 | }); 44 | 45 | }); -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | // imports 2 | const express = require('express'); 3 | const createErrors = require('http-errors'); 4 | 5 | const userRoute = require('./routes/user.route'); 6 | const categoryRoute = require('./routes/category.route'); 7 | const blogRoute = require('./routes/blog.route'); 8 | const cors = require('cors'); 9 | 10 | // constants 11 | const app = express(); 12 | 13 | app.use('/uploads', express.static('uploads')); 14 | app.use(express.json()); 15 | app.use(express.urlencoded({ extended: true })); 16 | app.use(cors({ 17 | origin: '*' 18 | })); 19 | 20 | // routes 21 | app.get('/', (req, res) => { 22 | res.send('Hello heroku'); 23 | }); 24 | 25 | app.use('/user', userRoute); 26 | app.use('/category', categoryRoute); 27 | app.use('/blog', blogRoute); 28 | 29 | // handle wildcard route 30 | app.use(async(req, res, next) => { 31 | next(createErrors.NotFound('This route does not exists!')); 32 | }); 33 | 34 | // handle errors 35 | app.use((err, req, res, next) => { 36 | if (err.code === 'LIMIT_FILE_SIZE') { 37 | err.status = 400; 38 | } 39 | res.status(err.status || 500); 40 | res.send({ 41 | error: { 42 | status: err.status || 500, 43 | message: err.message || 'Internal server error' 44 | } 45 | }); 46 | }); 47 | 48 | // exports 49 | module.exports = app; -------------------------------------------------------------------------------- /controllers/blog.controller.js: -------------------------------------------------------------------------------- 1 | // imports 2 | const express = require('express'); 3 | const createErrors = require('http-errors'); 4 | const blogService = require('../services/blog.service'); 5 | const { Blog } = require('../models/blog.model'); 6 | const utils = require('../util'); 7 | const cloudinary = require('../helpers/cloudinary.helper'); 8 | 9 | const itemsPerPage = 6; 10 | 11 | const createBlog = async(req, res, next) => { 12 | try { 13 | 14 | let blogBody = req.body; 15 | 16 | if( req.file ) { 17 | blogBody.img = req.file.path; 18 | 19 | const uploadResult = await cloudinary.uploader.upload(blogBody.img, { 20 | folder: "blogs" 21 | }); 22 | 23 | if( uploadResult.secure_url ) { 24 | blogBody.img = uploadResult.secure_url; 25 | } else { 26 | throw createErrors.Forbidden("Opps, image upload failed! Try again.") 27 | } 28 | } 29 | 30 | blogBody.writter = req.body.userId; 31 | 32 | const savedblog = await blogService.createBlog(blogBody) 33 | res.send(savedblog); 34 | 35 | } catch (error) { 36 | next(error); 37 | } 38 | } 39 | 40 | const getBlogList = async(req, res, next) => { 41 | try { 42 | 43 | const bloggerId = req.params.bloggerId; 44 | const categoryId = req.params.categoryId; 45 | 46 | let searchParams = {}; 47 | if( bloggerId && bloggerId.toLowerCase() != 'all' ) { 48 | searchParams.writter = bloggerId; 49 | } 50 | if( categoryId && categoryId.toLowerCase() != 'all' ) { 51 | searchParams.category = categoryId 52 | } 53 | 54 | let selectFields = 'posted title img'; 55 | let perPage = itemsPerPage; 56 | let page = req.query.page && req.query.page > 0 ? req.query.page-1 : 0; 57 | 58 | 59 | const numBlogs = await blogService.countBlogs(searchParams); 60 | let blogs = await blogService.readBlogs(searchParams, selectFields, perPage, page); 61 | 62 | let totalPages = Math.ceil(numBlogs / perPage); 63 | let currentPage = page+1; 64 | 65 | res.send({ 66 | result: blogs, 67 | totalBlogs: numBlogs, 68 | totalPages: totalPages, 69 | currentPage: currentPage 70 | }); 71 | 72 | } catch (error) { 73 | next(error); 74 | } 75 | } 76 | 77 | const getSingleBlog = async(req, res, next) => { 78 | try { 79 | 80 | let searchParams = {_id: req.params.blogId}; 81 | let selectFields = ''; 82 | let blog = await blogService.readBlogs(searchParams, selectFields); 83 | 84 | blog = blog[0]; 85 | if( !blog ) { 86 | throw createErrors.NotFound('No blog found with this blog id'); 87 | } 88 | 89 | res.send(blog); 90 | 91 | } catch (error) { 92 | next(error); 93 | } 94 | } 95 | 96 | const reactToBlog = async(req, res, next) => { 97 | try { 98 | 99 | let reactBody = req.body; 100 | const searchParams = { _id: reactBody.blogId }; 101 | const selectFields = ''; 102 | 103 | let blog = await blogService.readBlogs(searchParams, selectFields); 104 | if( blog.length == 0 ) { 105 | throw createErrors.NotFound('This blog does not exists'); 106 | } 107 | 108 | blog = blog[0]; 109 | const updatedBlog = await blogService.reactBlog(blog, reactBody); 110 | res.send(updatedBlog); 111 | 112 | } catch (error) { 113 | next(error); 114 | } 115 | } 116 | 117 | const commentToBlog = async(req, res, next) => { 118 | try { 119 | 120 | const commentBody = req.body; 121 | if( commentBody.body.trim().length == 0 ) { 122 | throw createErrors.BadRequest('Comment must not be empty!'); 123 | } 124 | 125 | const searchParams = { _id: commentBody.blogId }; 126 | const selectFields = ''; 127 | 128 | let blog = await blogService.readBlogs(searchParams, selectFields); 129 | if( blog.length == 0 ) { 130 | throw createErrors.NotFound('This blog does not exists'); 131 | } 132 | blog = blog[0]; 133 | 134 | let updatedBlog = await blogService.postComment(blog, commentBody); 135 | res.send(updatedBlog); 136 | 137 | } catch (error) { 138 | next(error); 139 | } 140 | } 141 | 142 | const deleteComment = async(req, res, next) => { 143 | try { 144 | 145 | const commentBody = req.body; 146 | const searchParams = { _id: commentBody.blogId }; 147 | const selectFields = ''; 148 | 149 | let blog = await blogService.readBlogs(searchParams, selectFields); 150 | if( blog.length == 0 ) { 151 | throw createErrors.NotFound('This blog does not exists'); 152 | } 153 | blog = blog[0]; 154 | 155 | const updatedBlog = await blogService.deleteComment(blog, commentBody); 156 | res.send(updatedBlog); 157 | 158 | } catch (error) { 159 | next(error); 160 | } 161 | } 162 | 163 | // exports 164 | module.exports = { 165 | createBlog, 166 | getBlogList, 167 | getSingleBlog, 168 | reactToBlog, 169 | commentToBlog, 170 | deleteComment 171 | } -------------------------------------------------------------------------------- /controllers/category.controller.js: -------------------------------------------------------------------------------- 1 | // imports 2 | const express = require('express'); 3 | const createErrors = require('http-errors'); 4 | const categoryService = require('../services/category.service'); 5 | const blogService = require('../services/blog.service'); 6 | const { Category } = require('../models/category.model'); 7 | const utils = require('../util'); 8 | 9 | const createCategory = async(req, res, next) => { 10 | try { 11 | 12 | let categoryBody = req.body; 13 | 14 | const savedCategory = await categoryService.createCategory(categoryBody); 15 | res.send(savedCategory); 16 | 17 | } catch (error) { 18 | next(error); 19 | } 20 | } 21 | 22 | const getCategories = async(req, res, next) => { 23 | try { 24 | 25 | let searchParams = {}; 26 | const categories = await categoryService.readCategory(searchParams); 27 | res.send(categories); 28 | 29 | } catch (error) { 30 | next(error); 31 | } 32 | } 33 | const getCategorizedBlogCount = async(req, res, next) => { 34 | try { 35 | 36 | let searchParams = {}; 37 | 38 | let bloggerId = req.params.bloggerId; 39 | if( bloggerId && bloggerId.toLowerCase() != 'all' ) { 40 | searchParams.writter = bloggerId; 41 | } 42 | 43 | let categories = await categoryService.readCategory(); 44 | 45 | let count = []; 46 | 47 | categories.forEach(c => { 48 | searchParams.category = c._id; 49 | count.push( 50 | blogService.countBlogs(searchParams) 51 | ); 52 | }); 53 | 54 | count = await Promise.all(count); 55 | 56 | const result = utils.combineArrayObjectAndArray(categories, ['_id', 'name'], count, 'count') 57 | 58 | res.send(result); 59 | 60 | } catch (error) { 61 | next(error); 62 | } 63 | } 64 | 65 | // exports 66 | module.exports = { 67 | createCategory, 68 | getCategories, 69 | getCategorizedBlogCount 70 | } -------------------------------------------------------------------------------- /controllers/user.controller.js: -------------------------------------------------------------------------------- 1 | // imports 2 | const express = require('express'); 3 | const createErrors = require('http-errors'); 4 | const userService = require('../services/user.service'); 5 | const { User } = require('../models/user.model'); 6 | const jwtHelper = require('../helpers/jwt.helper'); 7 | const utils = require('../util'); 8 | const bcrypt = require('bcrypt'); 9 | const saltRounds = 10; 10 | const client = require('../helpers/jwt.helper'); 11 | const cloudinary = require('../helpers/cloudinary.helper'); 12 | 13 | const registerUser = async(req, res, next) => { 14 | try { 15 | 16 | let userBody = req.body; 17 | 18 | userBody.password = await bcrypt.hash(userBody.password, saltRounds); 19 | const savedUser = await userService.createUser(userBody); 20 | 21 | const user = utils.makeObjectSelected(savedUser, ['_id', 'first_name', 'role']); 22 | const accessToken = await jwtHelper.signAccessToken(savedUser._id); 23 | const refreshToken = await jwtHelper.signRefreshToken(savedUser._id); 24 | 25 | res.send({ 26 | user, 27 | accessToken, 28 | refreshToken 29 | }); 30 | 31 | } catch (error) { 32 | next(error); 33 | } 34 | } 35 | 36 | const loginUser = async(req, res, next) => { 37 | try { 38 | 39 | const userBody = req.body; 40 | 41 | const findUser = await userService.findUniqueUser({email: userBody.email}); 42 | const passMatch = await bcrypt.compare(userBody.password, findUser.password); 43 | 44 | if( !passMatch ) { 45 | throw createErrors.BadRequest('Incorrect email or password'); 46 | } 47 | 48 | const accessToken = await jwtHelper.signAccessToken(findUser._id); 49 | const refreshToken = await jwtHelper.signRefreshToken(findUser._id); 50 | 51 | const user = utils.makeObjectSelected(findUser, ['_id', 'first_name', 'role']); 52 | 53 | res.send({ 54 | user, 55 | accessToken, 56 | refreshToken 57 | }); 58 | 59 | } catch (error) { 60 | if( error.status && error.status == 404 ) { 61 | error = createErrors.BadRequest('Incorrect email or password'); 62 | next(error); 63 | } 64 | next(error); 65 | } 66 | } 67 | 68 | const editUser = async(req, res, next) => { 69 | try { 70 | 71 | let userBody = req.body; 72 | 73 | if( req.file ) { 74 | userBody.img = req.file.path; 75 | 76 | const uploadResult = await cloudinary.uploader.upload(userBody.img, { 77 | folder: "avatars" 78 | }); 79 | 80 | if( uploadResult.secure_url ) { 81 | userBody.img = uploadResult.secure_url; 82 | } else { 83 | throw createErrors.Forbidden("Opps, image upload failed! Try again.") 84 | } 85 | } 86 | 87 | await userService.updateUser(userBody); 88 | 89 | const updatedUser = await userService.findUniqueUser({_id: userBody.userId}, ['_id', 'first_name', 'role']); 90 | 91 | res.send(updatedUser); 92 | 93 | } catch (error) { 94 | next(error); 95 | } 96 | } 97 | 98 | const refreshToken = async(req, res, next) => { 99 | try { 100 | 101 | let oldRefreshToken = req.body.refreshToken; 102 | 103 | if( !oldRefreshToken ) { 104 | throw createErrors.Forbidden('No refreshToken'); 105 | } 106 | 107 | const userId = await jwtHelper.verifyRefreshToken(oldRefreshToken); 108 | if( !userId ) { 109 | throw createErrors.Forbidden('No refreshToken'); 110 | } 111 | 112 | const accessToken = await jwtHelper.signAccessToken(userId); 113 | const refreshToken = await jwtHelper.signRefreshToken(userId); 114 | res.send({accessToken, refreshToken}); 115 | 116 | } catch (error) { 117 | next(error); 118 | } 119 | } 120 | 121 | const getMyData = async(req, res, next) => { 122 | try { 123 | 124 | const userId = req.body.userId; 125 | let searchParams = { _id: userId }; 126 | let selectFields = 'first_name last_name joined role email job address about img' 127 | 128 | const user = await userService.findUniqueUser(searchParams, selectFields); 129 | 130 | res.send(user); 131 | 132 | } catch (error) { 133 | next(error); 134 | } 135 | } 136 | 137 | const getBloggerProfile = async(req, res, next) => { 138 | try { 139 | 140 | const userId = req.params.bloggerId; 141 | if( !userId ) { 142 | throw createErrors.BadRequest('No bloggerId'); 143 | } 144 | 145 | let searchParams = { _id: userId }; 146 | let selectFields = 'img first_name last_name joined role email job address about'; 147 | 148 | const user = await userService.findUniqueUser(searchParams, selectFields); 149 | res.send(user); 150 | 151 | } catch (error) { 152 | next(error); 153 | } 154 | } 155 | 156 | const logout = async(req, res, next) => { 157 | res.send('logout is not implemented yet') 158 | } 159 | 160 | // exports 161 | module.exports = { 162 | registerUser, 163 | loginUser, 164 | editUser, 165 | refreshToken, 166 | getMyData, 167 | getBloggerProfile, 168 | logout 169 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | 3 | services: 4 | # MongoDB service 5 | mongo_db: 6 | container_name: letsblog-db-c 7 | image: mongo:latest 8 | restart: always 9 | ports: 10 | - 2717:27017 11 | volumes: 12 | - mongo_db:/data/db 13 | 14 | # Node API servcie 15 | api: 16 | build: . 17 | ports: 18 | - 3000:3000 19 | volumes: 20 | - .:/usr/src/app 21 | - /usr/src/app/node_modules 22 | container_name: letsblog-api-c 23 | environment: 24 | PORT: ${PORT} 25 | MONGODB_URL: mongodb://mongo_db:27017/letsBlog 26 | accessTokenKey: ${accessTokenKey} 27 | refreshTokenKey: ${refreshTokenKey} 28 | CLOUD_NAME: ${CLOUD_NAME} 29 | CLOUDINARY_API_KEY: ${CLOUDINARY_API_KEY} 30 | API_SECRET: ${API_SECRET} 31 | depends_on: 32 | - mongo_db 33 | 34 | volumes: 35 | mongo_db: {} -------------------------------------------------------------------------------- /docs/blog/commentToBlog.md: -------------------------------------------------------------------------------- 1 | ## Comment to blog 2 | To make a comment to a particular blog 3 | 4 | **URL**: `localhost:3000/blog/comment` 5 | 6 | **Method**: `POST` 7 | 8 | **Authentication**: Required 9 | 10 | ## Request body 11 | **Required fields:** `blogId`, `body` 12 | 13 | **Optional fields:** 14 | 15 | **Data**: 16 | ```bash 17 | { 18 | "blogId": "606f2b0e17586e3480477137", 19 | "body": "This is my comment" 20 | } 21 | ``` 22 | 23 | ## Success response 24 | **Code**: `200 OK` 25 | 26 | **Content**: 27 | ```bash 28 | { 29 | "reacts": { 30 | "like": [ 31 | "609655be3bfe280015a45d04", 32 | "609656cc3bfe280015a45d07", 33 | "6094fafc8c2e3c15b4d3cb9d" 34 | ], 35 | "love": [], 36 | "funny": [], 37 | "sad": [], 38 | "informative": [] 39 | }, 40 | "img": "uploads\\blogs\\1620377310963.png", 41 | "posted": "07 May 2021", 42 | "_id": "6094fede8c2e3c15b4d3cba7", 43 | "title": "Designing a cover photo with MS PowerPoint", 44 | "category": { 45 | "_id": "6094fb9a8c2e3c15b4d3cba2", 46 | "name": "skill development" 47 | }, 48 | "body": "Lorem Ipsum is simply dummy ...", 49 | "writter": { 50 | "joined": "07 May 2021", 51 | "_id": "6094fafc8c2e3c15b4d3cb9d", 52 | "first_name": "Tazbinur", 53 | "last_name": "Rahaman" 54 | }, 55 | "comments": [ 56 | { 57 | "time": "08 May 2021", 58 | "_id": "6096562d3bfe280015a45d05", 59 | "people": { 60 | "img": "uploads/1620654288692.jpg", 61 | "_id": "609655be3bfe280015a45d04", 62 | "first_name": "Imtiaz", 63 | "last_name": "Ahmed" 64 | }, 65 | "body": "Wow! I didn't know MS powerpoint can do that much things!" 66 | }, 67 | { 68 | "time": "08 May 2021", 69 | "_id": "609657a43bfe280015a45d08", 70 | "people": { 71 | "img": "uploads/1620654239923.jpg", 72 | "_id": "609656cc3bfe280015a45d07", 73 | "first_name": "Rakibul", 74 | "last_name": "Islam" 75 | }, 76 | "body": "Thanks for sharing, very informative..." 77 | }, 78 | { 79 | "time": "11 May 2021", 80 | "_id": "609982820ddd122710b48596", 81 | "people": { 82 | "img": "uploads/1620654328698.png", 83 | "_id": "6094fafc8c2e3c15b4d3cb9d", 84 | "first_name": "Tazbinur", 85 | "last_name": "Rahaman" 86 | }, 87 | "body": "This is my comment" 88 | } 89 | ], 90 | "__v": 10 91 | } 92 | ``` 93 | 94 | ## Error response 95 | **Condition**: If any of required fields is absent, invalid `blogId` or `accessToken` is absent 96 | 97 | **Code**: `400 Bad Request` 98 | 99 | **Content**: 100 | ```bash 101 | { 102 | "error": { 103 | "status": 400, 104 | "message": "Invalied blogId" 105 | } 106 | } 107 | ``` -------------------------------------------------------------------------------- /docs/blog/createBlog.md: -------------------------------------------------------------------------------- 1 | ## Create blog 2 | To create a new blog 3 | 4 | **URL**: `localhost:3000/blog` 5 | 6 | **Method**: `POST` 7 | 8 | **Authentication**: Required 9 | 10 | ## Request body 11 | **Required fields:** `title`, `body`, `category` 12 | 13 | **Optional fields:** 14 | 15 | **Data**: 16 | ```bash 17 | { 18 | "title": "How to write API documentation", 19 | "category": "6069c9807f65a454540d1386", 20 | "body": "To write a documentation..." 21 | } 22 | ``` 23 | 24 | ## Success response 25 | **Code**: `200 OK` 26 | 27 | **Content**: 28 | ```bash 29 | { 30 | "reacts": { 31 | "like": [], 32 | "love": [], 33 | "funny": [], 34 | "sad": [], 35 | "informative": [] 36 | }, 37 | "posted": "28 Apr 2021", 38 | "_id": "6089ba31dfe5685294ab6323", 39 | "title": "How to write API documentation", 40 | "category": { 41 | "_id": "6069c9807f65a454540d1386", 42 | "name": "tech" 43 | }, 44 | "body": "To write a documentation...", 45 | "writter": { 46 | "joined": "08 Apr 2021", 47 | "_id": "606efbba17e43a04cce0286d", 48 | "first_name": "Tazbinur", 49 | "last_name": "Rahaman" 50 | }, 51 | "comments": [], 52 | "__v": 0 53 | } 54 | ``` 55 | 56 | ## Error response 57 | **Condition**: If any of the required fields is absent or `accessToken` is absent 58 | 59 | **Code**: `500 Internal Server Error` 60 | 61 | **Content**: 62 | ```bash 63 | { 64 | "error": { 65 | "status": 500, 66 | "message": "Blog validation failed: body: Path `body` is required." 67 | } 68 | } 69 | ``` -------------------------------------------------------------------------------- /docs/blog/deleteCommentFromBlog.md: -------------------------------------------------------------------------------- 1 | ## Delete comment from blog 2 | To delete a particular comment from a particular blog 3 | 4 | **URL**: `localhost:3000/blog/comment` 5 | 6 | **Method**: `DELETE` 7 | 8 | **Authentication**: Required 9 | 10 | ## Request body 11 | **Required fields:** `blogId`, `id` 12 | 13 | **Optional fields:** 14 | 15 | **Data**: 16 | ```bash 17 | { 18 | "blogId": "606f2b0e17586e3480477137", 19 | "id": "6089c40ddfe5685294ab6327" 20 | } 21 | ``` 22 | 23 | ## Success response 24 | **Code**: `200 OK` 25 | 26 | **Content**: 27 | ```bash 28 | { 29 | "reacts": { 30 | "like": [ 31 | "609655be3bfe280015a45d04", 32 | "609656cc3bfe280015a45d07", 33 | "6094fafc8c2e3c15b4d3cb9d" 34 | ], 35 | "love": [], 36 | "funny": [], 37 | "sad": [], 38 | "informative": [] 39 | }, 40 | "img": "uploads\\blogs\\1620377310963.png", 41 | "posted": "07 May 2021", 42 | "_id": "6094fede8c2e3c15b4d3cba7", 43 | "title": "Designing a cover photo with MS PowerPoint", 44 | "category": { 45 | "_id": "6094fb9a8c2e3c15b4d3cba2", 46 | "name": "skill development" 47 | }, 48 | "body": "Lorem Ipsum is simply dummy ...", 49 | "writter": { 50 | "joined": "07 May 2021", 51 | "_id": "6094fafc8c2e3c15b4d3cb9d", 52 | "first_name": "Tazbinur", 53 | "last_name": "Rahaman" 54 | }, 55 | "comments": [ 56 | { 57 | "time": "08 May 2021", 58 | "_id": "6096562d3bfe280015a45d05", 59 | "people": { 60 | "img": "uploads/1620654288692.jpg", 61 | "_id": "609655be3bfe280015a45d04", 62 | "first_name": "Imtiaz", 63 | "last_name": "Ahmed" 64 | }, 65 | "body": "Wow! I didn't know MS powerpoint can do that much things!" 66 | }, 67 | { 68 | "time": "08 May 2021", 69 | "_id": "609657a43bfe280015a45d08", 70 | "people": { 71 | "img": "uploads/1620654239923.jpg", 72 | "_id": "609656cc3bfe280015a45d07", 73 | "first_name": "Rakibul", 74 | "last_name": "Islam" 75 | }, 76 | "body": "Thanks for sharing, very informative..." 77 | } 78 | ], 79 | "__v": 11 80 | } 81 | ``` 82 | 83 | ## Error response 84 | **Condition**: If any of required fields is absent, invalid `blogId` or `accessToken` is absent 85 | 86 | **Code**: `400 Bad Request` 87 | 88 | **Content**: 89 | ```bash 90 | { 91 | "error": { 92 | "status": 400, 93 | "message": "Invalied blogId" 94 | } 95 | } 96 | ``` -------------------------------------------------------------------------------- /docs/blog/getBlogsOfSelectedCategory.md: -------------------------------------------------------------------------------- 1 | ## Get blogs of a category 2 | To get list of blogs of a particular category with information & pagination. 6 blogs in each page. 3 | 4 | **URL**: `localhost:3000/blog/category/:categoryId?page=:pageNumber` 5 | 6 | **Method**: `GET` 7 | 8 | **Authentication**: Not required 9 | 10 | ## Request body 11 | **Required fields:** `categoryId` 12 | 13 | **Optional fields:** `pageNumber` (query params: default is 1) 14 | 15 | **Data**: 16 | ```bash 17 | 18 | ``` 19 | 20 | ## Success response 21 | **Code**: `200 OK` 22 | 23 | **Content**: 24 | ```bash 25 | { 26 | "result": [ 27 | { 28 | "img": "http://localhost:3000/uploads\\blogs\\1620377411846.jpg", 29 | "posted": "07 May 2021", 30 | "_id": "6094ff438c2e3c15b4d3cba8", 31 | "title": "Travelling tips with budget money", 32 | "category": { 33 | "_id": "6094fb838c2e3c15b4d3cba0", 34 | "name": "tours & travels" 35 | }, 36 | "writter": { 37 | "joined": "07 May 2021", 38 | "_id": "6094fafc8c2e3c15b4d3cb9d", 39 | "first_name": "Tazbinur", 40 | "last_name": "Rahaman" 41 | }, 42 | "comments": [ 43 | { 44 | "people": { 45 | "img": "uploads/1620654239923.jpg", 46 | "_id": "609656cc3bfe280015a45d07", 47 | "first_name": "Rakibul", 48 | "last_name": "Islam" 49 | } 50 | } 51 | ] 52 | }, 53 | ... 54 | { 55 | "img": "http://localhost:3000/uploads/blogs/1620654888979.jpg", 56 | "posted": "10 May 2021", 57 | "_id": "60993b2e48e2930015410235", 58 | "title": "Gramer bari te shorisha ful er bagan e ekdin", 59 | "category": { 60 | "_id": "6094fb838c2e3c15b4d3cba0", 61 | "name": "tours & travels" 62 | }, 63 | "writter": { 64 | "joined": "08 May 2021", 65 | "_id": "609656cc3bfe280015a45d07", 66 | "first_name": "Rakibul", 67 | "last_name": "Islam" 68 | }, 69 | "comments": [] 70 | } 71 | ], 72 | "totalBlogs": 3, 73 | "totalPages": 1, 74 | "currentPage": 1 75 | } 76 | ``` 77 | 78 | ## Error response 79 | **Condition**: 80 | 81 | **Code**: 82 | 83 | **Content**: 84 | ```bash 85 | 86 | ``` -------------------------------------------------------------------------------- /docs/blog/getDetailsOfBlog.md: -------------------------------------------------------------------------------- 1 | ## Get details of a blog 2 | To get detailed content of a particular blog 3 | 4 | **URL**: `localhost:3000/blog/:blogId` 5 | 6 | **Method**: `GET` 7 | 8 | **Authentication**: Not required 9 | 10 | ## Request body 11 | **Required fields:** `blogId` 12 | 13 | **Optional fields:** 14 | 15 | **Data**: 16 | ```bash 17 | 18 | ``` 19 | 20 | ## Success response 21 | **Code**: `200 OK` 22 | 23 | **Content**: 24 | ```bash 25 | { 26 | "reacts": { 27 | "like": [ 28 | "609655be3bfe280015a45d04", 29 | "609656cc3bfe280015a45d07" 30 | ], 31 | "love": [], 32 | "funny": [], 33 | "sad": [], 34 | "informative": [] 35 | }, 36 | "img": "http://localhost:3000/uploads\\blogs\\1620377310963.png", 37 | "posted": "07 May 2021", 38 | "_id": "6094fede8c2e3c15b4d3cba7", 39 | "title": "Designing a cover photo with MS PowerPoint", 40 | "category": { 41 | "_id": "6094fb9a8c2e3c15b4d3cba2", 42 | "name": "skill development" 43 | }, 44 | "body": "Lorem Ipsum is simply dummy text ...", 45 | "writter": { 46 | "joined": "07 May 2021", 47 | "_id": "6094fafc8c2e3c15b4d3cb9d", 48 | "first_name": "Tazbinur", 49 | "last_name": "Rahaman" 50 | }, 51 | "comments": [ 52 | { 53 | "time": "08 May 2021", 54 | "_id": "6096562d3bfe280015a45d05", 55 | "people": { 56 | "img": "uploads/1620654288692.jpg", 57 | "_id": "609655be3bfe280015a45d04", 58 | "first_name": "Imtiaz", 59 | "last_name": "Ahmed" 60 | }, 61 | "body": "Wow! I didn't know MS powerpoint can do that much things!" 62 | }, 63 | { 64 | "time": "08 May 2021", 65 | "_id": "609657a43bfe280015a45d08", 66 | "people": { 67 | "img": "uploads/1620654239923.jpg", 68 | "_id": "609656cc3bfe280015a45d07", 69 | "first_name": "Rakibul", 70 | "last_name": "Islam" 71 | }, 72 | "body": "Thanks for sharing, very informative..." 73 | } 74 | ], 75 | "__v": 8 76 | } 77 | ``` 78 | 79 | ## Error response 80 | **Condition**: 81 | 82 | **Code**: 83 | 84 | **Content**: 85 | ```bash 86 | 87 | ``` -------------------------------------------------------------------------------- /docs/blog/getLIstofAllBlogsWithPagination.md: -------------------------------------------------------------------------------- 1 | ## Get list of all blogs with pagination of certain category of a user 2 | To get the list of all blogs of certain category of a user with information & with pagination. 6 blogs each page. BloggerId & categoryId both set to `all` returns all the blogs of all categories. 3 | 4 | **URL**: `localhost:3000/blog/:bloggerId?/:categoryId?` 5 | 6 | **Method**: `GET` 7 | 8 | **Authentication**: Not required 9 | 10 | ## Request body 11 | **Required fields:** `bloggerId`, `categoryId` (req params) 12 | 13 | **Optional fields:** `page` (query params: default is 1) 14 | 15 | **Data**: 16 | ```bash 17 | 18 | ``` 19 | 20 | ## Success response 21 | **Code**: `200 OK` 22 | 23 | **Content**: 24 | ```bash 25 | { 26 | "result": [ 27 | { 28 | "img": "http://localhost:3000/uploads\\blogs\\1620377310963.png", 29 | "posted": "07 May 2021", 30 | "_id": "6094fede8c2e3c15b4d3cba7", 31 | "title": "Designing a cover photo with MS PowerPoint", 32 | "category": { 33 | "_id": "6094fb9a8c2e3c15b4d3cba2", 34 | "name": "skill development" 35 | }, 36 | "writter": { 37 | "joined": "07 May 2021", 38 | "_id": "6094fafc8c2e3c15b4d3cb9d", 39 | "first_name": "Tazbinur", 40 | "last_name": "Rahaman" 41 | }, 42 | "comments": [ 43 | { 44 | "people": { 45 | "img": "uploads/1620654288692.jpg", 46 | "_id": "609655be3bfe280015a45d04", 47 | "first_name": "Imtiaz", 48 | "last_name": "Ahmed" 49 | } 50 | }, 51 | { 52 | "people": { 53 | "img": "uploads/1620654239923.jpg", 54 | "_id": "609656cc3bfe280015a45d07", 55 | "first_name": "Rakibul", 56 | "last_name": "Islam" 57 | } 58 | } 59 | ] 60 | }, 61 | ... 62 | { 63 | "img": "http://localhost:3000/uploads\\blogs\\1620377835812.jpg", 64 | "posted": "07 May 2021", 65 | "_id": "609500eb8c2e3c15b4d3cbac", 66 | "title": "RUET ECE16 series landed to their final year of graduation", 67 | "category": { 68 | "_id": "6094fb8f8c2e3c15b4d3cba1", 69 | "name": "motivational" 70 | }, 71 | "writter": { 72 | "joined": "07 May 2021", 73 | "_id": "6094fafc8c2e3c15b4d3cb9d", 74 | "first_name": "Tazbinur", 75 | "last_name": "Rahaman" 76 | }, 77 | "comments": [] 78 | } 79 | ], 80 | "totalBlogs": 7, 81 | "totalPages": 2, 82 | "currentPage": 1 83 | } 84 | ``` 85 | 86 | ## Error response 87 | **Condition**: If bloggerId, CategoryId or both are invalied 88 | 89 | **Code**: `400 Bad Request` 90 | 91 | **Content**: 92 | ```bash 93 | "error": { 94 | "status": 400, 95 | "message": "Invalied Id provided" 96 | } 97 | ``` -------------------------------------------------------------------------------- /docs/blog/reactToBlog.md: -------------------------------------------------------------------------------- 1 | ## React to blog 2 | To react, change react, remove react to/ from a particular blog 3 | 4 | **URL**: `localhost:3000/blog/react` 5 | 6 | **Method**: `PUT` 7 | 8 | **Authentication**: Required 9 | 10 | ## Request body 11 | **Required fields:** `blogId`, `reactName` 12 | 13 | **Optional fields:** 14 | 15 | **Data**: 16 | ```bash 17 | { 18 | "blogId": "606f2b0e17586e3480477137", 19 | "reactName": "like" 20 | } 21 | ``` 22 | 23 | ## Success response 24 | **Code**: `200 OK` 25 | 26 | **Content**: 27 | ```bash 28 | { 29 | "reacts": { 30 | "like": [ 31 | "609655be3bfe280015a45d04", 32 | "609656cc3bfe280015a45d07", 33 | "6094fafc8c2e3c15b4d3cb9d" 34 | ], 35 | "love": [], 36 | "funny": [], 37 | "sad": [], 38 | "informative": [] 39 | }, 40 | "img": "uploads\\blogs\\1620377310963.png", 41 | "posted": "07 May 2021", 42 | "_id": "6094fede8c2e3c15b4d3cba7", 43 | "title": "Designing a cover photo with MS PowerPoint", 44 | "category": { 45 | "_id": "6094fb9a8c2e3c15b4d3cba2", 46 | "name": "skill development" 47 | }, 48 | "body": "Lorem Ipsum is simply dummy ...", 49 | "writter": { 50 | "joined": "07 May 2021", 51 | "_id": "6094fafc8c2e3c15b4d3cb9d", 52 | "first_name": "Tazbinur", 53 | "last_name": "Rahaman" 54 | }, 55 | "comments": [ 56 | { 57 | "time": "08 May 2021", 58 | "_id": "6096562d3bfe280015a45d05", 59 | "people": { 60 | "img": "uploads/1620654288692.jpg", 61 | "_id": "609655be3bfe280015a45d04", 62 | "first_name": "Imtiaz", 63 | "last_name": "Ahmed" 64 | }, 65 | "body": "Wow! I didn't know MS powerpoint can do that much things!" 66 | }, 67 | { 68 | "time": "08 May 2021", 69 | "_id": "609657a43bfe280015a45d08", 70 | "people": { 71 | "img": "uploads/1620654239923.jpg", 72 | "_id": "609656cc3bfe280015a45d07", 73 | "first_name": "Rakibul", 74 | "last_name": "Islam" 75 | }, 76 | "body": "Thanks for sharing, very informative..." 77 | } 78 | ], 79 | "__v": 9 80 | } 81 | ``` 82 | 83 | ## Error response 84 | **Condition**: If any of required fields is absent, invalid `blogId` or `accessToken` is absent 85 | 86 | **Code**: `400 Bad Request` 87 | 88 | **Content**: 89 | ```bash 90 | { 91 | "error": { 92 | "status": 400, 93 | "message": "Invalied blogId" 94 | } 95 | } 96 | ``` -------------------------------------------------------------------------------- /docs/category/createCategory.md: -------------------------------------------------------------------------------- 1 | ## Create a new category 2 | To create a new category 3 | 4 | **URL**: `localhost:3000/category` 5 | 6 | **Method**: `POST` 7 | 8 | **Authentication**: Required 9 | 10 | ## Request body 11 | **Required fields:** `name` 12 | 13 | **Optional fields:** 14 | 15 | **Data**: 16 | ```bash 17 | { 18 | "name": "Education" 19 | } 20 | ``` 21 | 22 | ## Success response 23 | **Code**: `200 OK` 24 | 25 | **Content**: 26 | ```bash 27 | { 28 | "_id": "6089b67fdfe5685294ab631c", 29 | "name": "education", 30 | "__v": 0 31 | } 32 | ``` 33 | 34 | ## Error response 35 | **Condition**: If any of the required params is absent or the `accessToken` is absent. 36 | 37 | **Code**: `500 Internal Server Error` 38 | 39 | **Content**: 40 | ```bash 41 | { 42 | "error": { 43 | "status": 500, 44 | "message": "Category validation failed: name: Path `name` is required." 45 | } 46 | } 47 | ``` -------------------------------------------------------------------------------- /docs/category/getListOfCategories.md: -------------------------------------------------------------------------------- 1 | ## Get list of all categories 2 | To get a list of all categories 3 | 4 | **URL**: `localhost:3000/category` 5 | 6 | **Method**: `GET` 7 | 8 | **Authentication**: Not required 9 | 10 | ## Request body 11 | **Required fields:** 12 | 13 | **Optional fields:** 14 | 15 | **Data**: 16 | ```bash 17 | 18 | ``` 19 | 20 | ## Success response 21 | **Code**: `200 OK` 22 | 23 | **Content**: 24 | ```bash 25 | [ 26 | { 27 | "_id": "6069c9807f65a454540d1386", 28 | "name": "tech" 29 | }, 30 | { 31 | "_id": "6069c9857f65a454540d1387", 32 | "name": "medical" 33 | }, 34 | { 35 | "_id": "6069c98a7f65a454540d1388", 36 | "name": "travel" 37 | }, 38 | { 39 | "_id": "6089b67fdfe5685294ab631c", 40 | "name": "education" 41 | } 42 | ] 43 | ``` 44 | 45 | ## Error response 46 | **Condition**: 47 | 48 | **Code**: 49 | 50 | **Content**: 51 | ```bash 52 | 53 | ``` -------------------------------------------------------------------------------- /docs/category/getListOfCategoriezedBlogs.md: -------------------------------------------------------------------------------- 1 | ## Get list of all categorized blog counts of user 2 | To get a list of all categories & number of blogs of each category of a user 3 | 4 | **URL**: `GET localhost:3000/category/categorizedBlogs/:bloggerId` 5 | 6 | **Method**: `GET` 7 | 8 | **Authentication**: Not required 9 | 10 | ## Request body 11 | **Required fields:** `bloggerId` (query params) 12 | 13 | **Optional fields:** 14 | 15 | **Data**: 16 | ```bash 17 | 18 | ``` 19 | 20 | ## Success response 21 | **Code**: `200 OK` 22 | 23 | **Content**: 24 | ```bash 25 | [ 26 | { 27 | "_id": "6069c9807f65a454540d1386", 28 | "name": "tech", 29 | "count": 6 30 | }, 31 | { 32 | "_id": "6069c9857f65a454540d1387", 33 | "name": "medical", 34 | "count": 5 35 | }, 36 | { 37 | "_id": "6069c98a7f65a454540d1388", 38 | "name": "travel", 39 | "count": 11 40 | }, 41 | { 42 | "_id": "6089b67fdfe5685294ab631c", 43 | "name": "education", 44 | "count": 0 45 | } 46 | ] 47 | ``` 48 | 49 | ## Error response 50 | **Condition**: 51 | 52 | **Code**: 53 | 54 | **Content**: 55 | ```bash 56 | 57 | ``` -------------------------------------------------------------------------------- /docs/user/editUserProfile.md: -------------------------------------------------------------------------------- 1 | ## Edit user profile 2 | To update/edit a registered user's information 3 | 4 | **URL**: `localhost:3000/user/editProfile` 5 | 6 | **Method**: `PUT` 7 | 8 | **Authentication**: Required 9 | 10 | ## Request body 11 | 12 | **Required fields:** `email`, `first_name`, `last_name` 13 | 14 | **Optional fields:** `job`, `address`, `about` 15 | 16 | **Data**: 17 | ```bash 18 | { 19 | "email": "t@gmail.com", 20 | "first_name": "Tazbinur", 21 | "last_name": "Rahaman", 22 | "job": "Software engineer", 23 | "address": "Jashore", 24 | "about": "I am a node js developer" 25 | } 26 | ``` 27 | 28 | ## Success response 29 | **Code**: `200 OK` 30 | 31 | **Content**: 32 | ```bash 33 | { 34 | "role": "blogger", 35 | "_id": "606efbba17e43a04cce0286d", 36 | "first_name": "Tazbinur" 37 | } 38 | ``` 39 | 40 | ## Error response 41 | **Condition**: If any of the required fields is absent, `accessToken` is absent. 42 | 43 | **Code**: `500 Internal Server Error` 44 | 45 | **Content**: 46 | ```bash 47 | { 48 | "error": { 49 | "status": 500, 50 | "message": "\"first_name\" is required" 51 | } 52 | } 53 | ``` -------------------------------------------------------------------------------- /docs/user/getBloggersInfo.md: -------------------------------------------------------------------------------- 1 | ## Get bloggers info 2 | To get registered blogger's detail information 3 | 4 | **URL**: `localhost:3000/user/bloggerProfile/:bloggerId` 5 | 6 | **Method**: `GET` 7 | 8 | **Authentication**: Not required 9 | 10 | ## Request body 11 | 12 | **Required fields:** `bloggerId` (query params) 13 | 14 | **Optional fields:** 15 | 16 | **Data**: 17 | ```bash 18 | 19 | ``` 20 | 21 | ## Success response 22 | **Code**: `200 OK` 23 | 24 | **Content**: 25 | ```bash 26 | { 27 | "role": "blogger", 28 | "joined": "08 Apr 2021", 29 | "_id": "606efbba17e43a04cce0286d", 30 | "first_name": "Tazbinur", 31 | "last_name": "Rahaman", 32 | "email": "t@gmail.com", 33 | "about": "I am a node js developer", 34 | "address": "Jashore", 35 | "job": "Software engineer", 36 | "img": "data:image/jpeg;base64,/9j/4AAQSk...n8ln48v/0lf//Z" 37 | } 38 | ``` 39 | 40 | ## Error response 41 | **Condition**: If `bloggerId` is invalid 42 | 43 | **Code**: `400 Bad Request` 44 | 45 | **Content**: 46 | ```bash 47 | { 48 | "error": { 49 | "status": 400, 50 | "message": "Invalid bloggerId" 51 | } 52 | } 53 | ``` -------------------------------------------------------------------------------- /docs/user/getLoggedInUserInfo.md: -------------------------------------------------------------------------------- 1 | ## Get logged In User Info 2 | To get the information of loggedin user 3 | 4 | **URL**: `localhost:3000/user/me` 5 | 6 | **Method**: `GET 7 | 8 | **Authentication**: Required 9 | 10 | ## Request body 11 | **Required fields:** 12 | 13 | **Optional fields:** 14 | 15 | **Data**: 16 | ```bash 17 | 18 | ``` 19 | 20 | ## Success response 21 | **Code**: `200 OK` 22 | 23 | **Content**: 24 | ```bash 25 | { 26 | "role": "blogger", 27 | "joined": "08 Apr 2021", 28 | "_id": "606efbba17e43a04cce0286d", 29 | "first_name": "Tazbinur", 30 | "last_name": "Rahaman", 31 | "email": "t@gmail.com", 32 | "about": "I am a node js developer", 33 | "address": "Jashore", 34 | "job": "Software engineer" 35 | } 36 | ``` 37 | 38 | ## Error response 39 | **Condition**: If `accessToken` in absent. 40 | 41 | **Code**: `401 Unauthorized` 42 | 43 | **Content**: 44 | ```bash 45 | { 46 | "error": { 47 | "status": 401, 48 | "message": "JsonWebTokenError" 49 | } 50 | } 51 | ``` -------------------------------------------------------------------------------- /docs/user/login.md: -------------------------------------------------------------------------------- 1 | ## Login 2 | To login a user, collect loggedin user data & tokens 3 | 4 | **URL**: `localhost:5000/user/login` 5 | 6 | **Method**: `POST` 7 | 8 | **Authentication**: Not required 9 | 10 | ## Request body 11 | **Required fields:** `email`, `password` 12 | 13 | **Optional fields:** 14 | 15 | **Data**: 16 | ```bash 17 | { 18 | "email": "t@gmail.com", 19 | "password": "123" 20 | } 21 | ``` 22 | 23 | ## Success response 24 | **Code**: `200 OK` 25 | 26 | **Content**: 27 | ```bash 28 | { 29 | "user": { 30 | "_id": "606efbba17e43a04cce0286d", 31 | "first_name": "Tazbinur", 32 | "role": "blogger" 33 | }, 34 | "accessToken": "eyJhbGciOiJIUzI1NiIsInR...TL54pO2vJkQ21J6kzQ", 35 | "refreshToken": "eyJhbGciOiJIUzI1NiIsInR...SlZ1R_Kd3lxph4N8IFbg" 36 | } 37 | ``` 38 | 39 | ## Error response 40 | **Condition**: If any of the required fields is absent, `email` or `password` is wrong. 41 | 42 | **Code**: `401 Unauthorized` 43 | 44 | **Content**: 45 | ```bash 46 | { 47 | "error": { 48 | "status": 401, 49 | "message": "Incorrect email or password" 50 | } 51 | } 52 | ``` -------------------------------------------------------------------------------- /docs/user/refreshTokens.md: -------------------------------------------------------------------------------- 1 | ## Refresh tokens 2 | To refresh tokens & generate new pair of accessToken, refreshToken 3 | 4 | **URL**: `localhost:3000/user/me/refresToken` 5 | 6 | **Method**: `POST` 7 | 8 | **Authentication**: Required 9 | 10 | ## Request body 11 | 12 | **Required fields:** `refreshToken` 13 | 14 | **Optional fields:** 15 | 16 | **Data**: 17 | ```bash 18 | { 19 | "refreshToken": "eyJhbGciOiJIUzI1Ni...ldMTk6_9B8K9QgR-jGQpgg" 20 | } 21 | ``` 22 | 23 | ## Success response 24 | **Code**: `200 OK` 25 | 26 | **Content**: 27 | ```bash 28 | { 29 | "accessToken": "eyJhbGciOiJIUzI1NiIsIn...bb_PqBFpLvtSF3JKRiMQ3-9nOu5M", 30 | "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5c...F218Sa4DgdTXA7muiLL-cofteo" 31 | } 32 | ``` 33 | 34 | ## Error response 35 | **Condition**: If the refreshToken is absent, invalid or expired. 36 | 37 | **Code**: `403 Forbidden` 38 | 39 | **Content**: 40 | ```bash 41 | { 42 | "error": { 43 | "status": 403, 44 | "message": "JsonWebTokenError" 45 | } 46 | } 47 | ``` -------------------------------------------------------------------------------- /docs/user/register.md: -------------------------------------------------------------------------------- 1 | ## Register 2 | To register a new user, collect registered user data & tokens 3 | 4 | **URL**: `localhost:3000/user/register` 5 | 6 | **Method**: `POST` 7 | 8 | **Authentication**: Not required 9 | 10 | ## Request body 11 | **Required fields:** `email`, `first_name`, `last_name`, `password` 12 | 13 | **Optional fields:** 14 | 15 | **Data**: 16 | ```bash 17 | { 18 | "email": "t@gmail.com", 19 | "first_name": "Tazbinur", 20 | "last_name": "Rahaman", 21 | "password": "123" 22 | } 23 | ``` 24 | 25 | ## Success response 26 | **Code**: `200 OK` 27 | 28 | **Content**: 29 | ```bash 30 | { 31 | "user": { 32 | "_id": "606efbba17e43a04cce0286d", 33 | "first_name": "Tazbinur", 34 | "role": "blogger" 35 | }, 36 | "accessToken": "eyJhbGciOiJIUzI1NiIsInR...TL54pO2vJkQ21J6kzQ", 37 | "refreshToken": "eyJhbGciOiJIUzI1NiIsInR...SlZ1R_Kd3lxph4N8IFbg" 38 | } 39 | ``` 40 | 41 | ## Error response 42 | **Condition**: If any of the required params is absent or the `email` is already registered. 43 | 44 | **Code**: `409 Conflict` 45 | 46 | **Content**: 47 | ```bash 48 | { 49 | "error": { 50 | "status": 409, 51 | "message": "t@gmail.com already exists" 52 | } 53 | } 54 | ``` -------------------------------------------------------------------------------- /helpers/cloudinary.helper.js: -------------------------------------------------------------------------------- 1 | const cloudinary = require('cloudinary').v2; 2 | require('dotenv').config(); 3 | 4 | cloudinary.config({ 5 | cloud_name: process.env.CLOUD_NAME, 6 | api_key: process.env.CLOUDINARY_API_KEY, 7 | api_secret: process.env.API_SECRET 8 | }); 9 | 10 | module.exports = cloudinary; -------------------------------------------------------------------------------- /helpers/jwt.helper.js: -------------------------------------------------------------------------------- 1 | // import 2 | const jwt = require('jsonwebtoken'); 3 | const createErrors = require('http-errors'); 4 | 5 | const signAccessToken = async(userId) => { 6 | try { 7 | 8 | userId = JSON.stringify(userId); 9 | 10 | const payload = {}; 11 | const privateKey = process.env.accessTokenKey; 12 | const options = { 13 | expiresIn: "1d", 14 | issuer: 'tazbinur.info', 15 | audience: userId 16 | }; 17 | 18 | const accessToken = await jwt.sign(payload, privateKey, options); 19 | return Promise.resolve(accessToken); 20 | 21 | } catch (error) { 22 | return Promise.reject(error); 23 | } 24 | } 25 | 26 | const verifyAccessToken = async(req, res, next) => { 27 | try { 28 | 29 | if (!req.headers['authorization']){ 30 | throw createErrors.Unauthorized('No accessToken'); 31 | } 32 | 33 | const accessToken = req.headers['authorization'].split(' ')[1]; 34 | if( !accessToken ) { 35 | throw createErrors.Unauthorized('No accessToken'); 36 | } 37 | 38 | const decoded = await jwt.verify(accessToken, process.env.accessTokenKey); 39 | 40 | if( !decoded ) { 41 | throw createErrors.Unauthorized('No accessToken'); 42 | } 43 | 44 | req.body.userId = JSON.parse(decoded.aud); 45 | next(); 46 | 47 | } catch (error) { 48 | if( error.name == 'TokenExpiredError' || error.name == 'JsonWebTokenError' ) { 49 | error = createErrors.Unauthorized(error.name); 50 | } 51 | next(error); 52 | } 53 | } 54 | 55 | const signRefreshToken = async(userId) => { 56 | try { 57 | 58 | userId = JSON.stringify(userId); 59 | 60 | const payload = {}; 61 | const privateKey = process.env.refreshTokenKey; 62 | const options = { 63 | expiresIn: "1d", 64 | issuer: 'tazbinur.info', 65 | audience: userId 66 | }; 67 | 68 | const refreshToken = await jwt.sign(payload, privateKey, options); 69 | return Promise.resolve(refreshToken); 70 | 71 | } catch (error) { 72 | return Promise.reject(error); 73 | } 74 | } 75 | 76 | const verifyRefreshToken = async(refreshToken) => { 77 | try { 78 | 79 | const decoded = await jwt.verify(refreshToken, process.env.refreshTokenKey); 80 | const userId = JSON.parse(decoded.aud); 81 | 82 | return Promise.resolve(userId); 83 | 84 | } catch (error) { 85 | if( error.name == 'TokenExpiredError' || error.name == 'JsonWebTokenError' ) { 86 | error = createErrors.Forbidden(error.name); 87 | } 88 | return Promise.reject(error); 89 | } 90 | } 91 | 92 | // exports 93 | module.exports = { 94 | signAccessToken, 95 | verifyAccessToken, 96 | signRefreshToken, 97 | verifyRefreshToken 98 | } -------------------------------------------------------------------------------- /helpers/mongodb.helper.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | mongoose.connect(process.env.MONGODB_URL, { 4 | useNewUrlParser: true, 5 | useUnifiedTopology: true, 6 | useCreateIndex: true 7 | }) 8 | .then(() => { 9 | console.log('mongodb connected...') 10 | }) 11 | .catch((err) => { 12 | console.log('ERROR: ' + err.message) 13 | }); 14 | 15 | 16 | mongoose.connection.on('connected', () => { 17 | console.log('mongodb successfully connected...') 18 | }); 19 | 20 | // handling errors after initial connection 21 | mongoose.connection.on('error', (err) => { 22 | console.log('ERROR!! mongodb after initial connection error!') 23 | }); 24 | 25 | mongoose.connection.on('disconnected', () => { 26 | console.log('mongodb disconnected...') 27 | }); 28 | 29 | process.on('SIGINT', async() => { 30 | await mongoose.connection.close() 31 | process.exit(0) 32 | }); -------------------------------------------------------------------------------- /middlewares/user.middleware.js: -------------------------------------------------------------------------------- 1 | // imports 2 | const Joi = require('joi'); 3 | const { 4 | userSchema, 5 | userRegisterSchema, 6 | userLoginSchema } = require('../validators/user.validator'); 7 | 8 | // middlewares 9 | const validateUserEditReq = async(req, res, next) => { 10 | try { 11 | await userSchema.validateAsync(req.body); 12 | next(); 13 | 14 | } catch (error) { 15 | next(error); 16 | } 17 | } 18 | 19 | const validateRegisterReq = async(req, res, next) => { 20 | try { 21 | 22 | await userRegisterSchema.validateAsync(req.body); 23 | next(); 24 | 25 | } catch (error) { 26 | next(error); 27 | } 28 | } 29 | 30 | const validateLoginReq = async(req, res, next) => { 31 | try { 32 | 33 | await userLoginSchema.validateAsync(req.body); 34 | next(); 35 | 36 | } catch (error) { 37 | next(error); 38 | } 39 | } 40 | 41 | // exports 42 | module.exports = { 43 | validateUserEditReq, 44 | validateRegisterReq, 45 | validateLoginReq 46 | } -------------------------------------------------------------------------------- /models/blog.model.js: -------------------------------------------------------------------------------- 1 | // imports 2 | const mongoose = require('mongoose'); 3 | const utils = require('../util'); 4 | const { Schema } = mongoose; 5 | 6 | // shcema definition 7 | const BlogSchema = new Schema({ 8 | writter: { 9 | type: Schema.Types.ObjectId, 10 | ref: 'User', 11 | required: true 12 | }, 13 | title:{ 14 | type: String, 15 | required: true 16 | }, 17 | img: { 18 | type: String, 19 | default: 'https://res.cloudinary.com/s4whf65/image/upload/v1661202691/blogs/eoiptbfzwf38m17yffft.jpg' 20 | }, 21 | posted: { 22 | type: String, 23 | default: utils.getCurretDate() 24 | }, 25 | category: { 26 | type: Schema.Types.ObjectId, 27 | ref: 'Category', 28 | required: true 29 | }, 30 | body: { 31 | type: String, 32 | required: true 33 | }, 34 | reacts: { 35 | like: [{ 36 | type: Schema.Types.ObjectId, 37 | ref: 'User' 38 | }], 39 | love: [{ 40 | type: Schema.Types.ObjectId, 41 | ref: 'User' 42 | }], 43 | funny: [{ 44 | type: Schema.Types.ObjectId, 45 | ref: 'User' 46 | }], 47 | sad: [{ 48 | type: Schema.Types.ObjectId, 49 | ref: 'User' 50 | }], 51 | informative: [{ 52 | type: Schema.Types.ObjectId, 53 | ref: 'User' 54 | }] 55 | }, 56 | comments: [{ 57 | people: { 58 | type: Schema.Types.ObjectId, 59 | ref: 'User', 60 | required: true 61 | }, 62 | time: { 63 | type: String, 64 | default: utils.getCurretDate() 65 | }, 66 | body: { 67 | type: String, 68 | required: true 69 | } 70 | }] 71 | }); 72 | 73 | const Blog = mongoose.model('Blog', BlogSchema); 74 | 75 | // exports 76 | module.exports = { 77 | Blog 78 | }; -------------------------------------------------------------------------------- /models/category.model.js: -------------------------------------------------------------------------------- 1 | // imports 2 | const mongoose = require('mongoose'); 3 | const utils = require('../util'); 4 | const { Schema } = mongoose; 5 | 6 | // shcema definition 7 | const categorySchema = new Schema({ 8 | name: { 9 | type: String, 10 | required: true, 11 | lowercase: true, 12 | unique: true 13 | } 14 | }); 15 | 16 | const Category = mongoose.model('Category', categorySchema); 17 | 18 | // exports 19 | module.exports = { 20 | Category 21 | }; -------------------------------------------------------------------------------- /models/user.model.js: -------------------------------------------------------------------------------- 1 | // imports 2 | const mongoose = require('mongoose'); 3 | const utils = require('../util'); 4 | const { Schema } = mongoose; 5 | 6 | // shcema definition 7 | const userSchema = new Schema({ 8 | img: { 9 | type: String, 10 | default: 'https://res.cloudinary.com/s4whf65/image/upload/v1661179042/avatars/vgj2dxxqucypsx7tkpfv.jpg' 11 | }, 12 | email:{ 13 | type: String, 14 | required: true, 15 | lowercase: true, 16 | unique: true 17 | }, 18 | first_name: { 19 | type: String, 20 | required: true 21 | }, 22 | last_name: { 23 | type: String, 24 | required: true 25 | }, 26 | role: { 27 | type: String, 28 | default: 'blogger' 29 | }, 30 | job: { 31 | type: String, 32 | require: false, 33 | default: '' 34 | }, 35 | joined: { 36 | type: String, 37 | default: utils.getCurretDate() 38 | }, 39 | address: { 40 | type: String, 41 | require: false, 42 | default: '' 43 | }, 44 | about: { 45 | type: String, 46 | require: false, 47 | default: '' 48 | }, 49 | password: { 50 | type: String, 51 | required: true 52 | } 53 | }); 54 | 55 | const User = mongoose.model('User', userSchema); 56 | 57 | // exports 58 | module.exports = { 59 | User 60 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blog-app-rest_api", 3 | "version": "1.0.0", 4 | "description": "This is the backend of a blog app", 5 | "main": "app.js", 6 | "scripts": { 7 | "start": "node server", 8 | "dev": "nodemon -L server", 9 | "test": "jest" 10 | }, 11 | "author": "Tazbinur", 12 | "license": "ISC", 13 | "dependencies": { 14 | "bcrypt": "^5.0.1", 15 | "cloudinary": "^1.30.1", 16 | "cors": "^2.8.5", 17 | "dotenv": "^8.2.0", 18 | "express": "^4.17.1", 19 | "http-errors": "^1.8.0", 20 | "jest": "^29.1.2", 21 | "joi": "^17.4.0", 22 | "jsonwebtoken": "^8.5.1", 23 | "mongoose": "^5.12.2", 24 | "multer": "^1.4.2", 25 | "nodemon": "^2.0.20" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /routes/blog.route.js: -------------------------------------------------------------------------------- 1 | // imports 2 | const express = require('express'); 3 | const createErrors = require('http-errors'); 4 | const blogCtrl = require('../controllers/blog.controller'); 5 | 6 | const { 7 | verifyAccessToken 8 | } = require('../helpers/jwt.helper'); 9 | 10 | var multer = require('multer'); 11 | 12 | var storage = multer.diskStorage({ 13 | destination: function (req, file, cb) { 14 | cb(null, './uploads/blogs') 15 | }, 16 | filename: function (req, file, cb) { 17 | cb(null, Date.now() + '.' + file.originalname.split('.').reverse()[0]) 18 | } 19 | }); 20 | 21 | const fileFilter = (req, file, cb) => { 22 | 23 | if( file.mimetype != 'image/jpeg' && file.mimetype != 'image/png' ) { 24 | cb(null, false); 25 | cb(createErrors.BadRequest("File type must be of jpg, jpeg or png!")); 26 | } else if( !req.body.title ) { 27 | cb(null, false); 28 | cb(createErrors.BadRequest("Title must not be empty!")); 29 | } else if( !req.body.category ) { 30 | cb(null, false); 31 | cb(createErrors.BadRequest("Category must not be empty!")); 32 | } else if( !req.body.body ) { 33 | cb(null, false); 34 | cb(createErrors.BadRequest("Body must not be empty!")); 35 | } else { 36 | cb(null, true); 37 | } 38 | }; 39 | 40 | var upload = multer({ 41 | storage: storage, 42 | limits: { 43 | fileSize: 1024*1024*3 44 | }, 45 | fileFilter: fileFilter 46 | }); 47 | 48 | // constants 49 | const router = express.Router(); 50 | 51 | // route: blog/ 52 | 53 | router.post('/', verifyAccessToken, upload.single('img'), verifyAccessToken, blogCtrl.createBlog); 54 | router.get('/details/:blogId', blogCtrl.getSingleBlog); 55 | router.get('/:bloggerId?/:categoryId?', blogCtrl.getBlogList); 56 | router.put('/react', verifyAccessToken, blogCtrl.reactToBlog); 57 | router.post('/comment', verifyAccessToken, blogCtrl.commentToBlog); 58 | router.delete('/comment', verifyAccessToken, blogCtrl.deleteComment); 59 | 60 | 61 | // exports 62 | module.exports = router; -------------------------------------------------------------------------------- /routes/category.route.js: -------------------------------------------------------------------------------- 1 | // imports 2 | const express = require('express'); 3 | const createErrors = require('http-errors'); 4 | const categoryCtrl = require('../controllers/category.controller'); 5 | 6 | // constants 7 | const router = express.Router(); 8 | 9 | // route: category/ 10 | 11 | router.post('/', categoryCtrl.createCategory); 12 | router.get('/', categoryCtrl.getCategories); 13 | router.get('/categorizedBlogs/:bloggerId?', categoryCtrl.getCategorizedBlogCount); 14 | 15 | 16 | // exports 17 | module.exports = router; -------------------------------------------------------------------------------- /routes/user.route.js: -------------------------------------------------------------------------------- 1 | // imports 2 | const express = require('express'); 3 | const createErrors = require('http-errors'); 4 | const userCtrl = require('../controllers/user.controller'); 5 | 6 | const { 7 | validateUserEditReq, 8 | validateRegisterReq, 9 | validateLoginReq 10 | } = require('../middlewares/user.middleware'); 11 | const { 12 | verifyAccessToken 13 | } = require('../helpers/jwt.helper'); 14 | 15 | var multer = require('multer'); 16 | 17 | var storage = multer.diskStorage({ 18 | destination: function (req, file, cb) { 19 | cb(null, './uploads') 20 | }, 21 | filename: function (req, file, cb) { 22 | cb(null, Date.now() + '.' + file.originalname.split('.').reverse()[0]) 23 | } 24 | }); 25 | 26 | const fileFilter = (req, file, cb) => { 27 | if( file.mimetype != 'image/jpeg' && file.mimetype != 'image/png' ) { 28 | cb(null, false); 29 | cb(createErrors.BadRequest("File type must be of jpg, jpeg or png!")); 30 | } else if( !req.body.email ) { 31 | cb(null, false); 32 | cb(createErrors.BadRequest("Email must not be empty!")); 33 | } else if( !req.body.first_name ) { 34 | cb(null, false); 35 | cb(createErrors.BadRequest("First name must not be empty!")); 36 | } else if( !req.body.last_name ) { 37 | cb(null, false); 38 | cb(createErrors.BadRequest("Last name must not be empty!")); 39 | } else { 40 | cb(null, true); 41 | } 42 | }; 43 | 44 | var upload = multer({ 45 | storage: storage, 46 | limits: { 47 | fileSize: 1024*1024*1 48 | }, 49 | fileFilter: fileFilter 50 | }); 51 | 52 | // constants 53 | const router = express.Router(); 54 | 55 | // route: user/ 56 | 57 | router.post('/register', validateRegisterReq, userCtrl.registerUser); 58 | router.post('/login', validateLoginReq, userCtrl.loginUser); 59 | 60 | router.put('/editProfile', 61 | verifyAccessToken, 62 | // validateUserEditReq, 63 | upload.single('img'), 64 | verifyAccessToken, 65 | userCtrl.editUser); 66 | 67 | router.post('/me/refresToken', userCtrl.refreshToken); 68 | router.get('/me', verifyAccessToken, userCtrl.getMyData); 69 | router.get('/bloggerProfile/:bloggerId', userCtrl.getBloggerProfile); 70 | router.delete('/logout', userCtrl.logout); 71 | 72 | 73 | // exports 74 | module.exports = router; -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | // imports 2 | const app = require('./app'); 3 | 4 | require('dotenv').config(); 5 | require('./helpers/mongodb.helper'); 6 | 7 | // constants 8 | const PORT = process.env.PORT || 5000; 9 | 10 | // start the server 11 | app.listen(PORT, () => { 12 | console.log(`server running at port ${PORT}...`); 13 | }); -------------------------------------------------------------------------------- /services/blog.service.js: -------------------------------------------------------------------------------- 1 | // imports 2 | const express = require('express'); 3 | const createErrors = require('http-errors'); 4 | const { Blog } = require('../models/blog.model'); 5 | 6 | // CRUD 7 | 8 | const createBlog = async(blogBody) => { 9 | try { 10 | 11 | const newBlog = new Blog(blogBody); 12 | let savedBlog = await newBlog.save(); 13 | 14 | savedBlog = await savedBlog 15 | .populate('writter', 'first_name last_name joined') 16 | .populate('category', 'name') 17 | .populate('comments.people', 'first_name last_name') 18 | .execPopulate(); 19 | 20 | return Promise.resolve(savedBlog); 21 | 22 | } catch (error) { 23 | return Promise.reject(error); 24 | } 25 | } 26 | 27 | const readBlogs = async( 28 | searchParams = {}, 29 | selectFields = '', 30 | perPage = 99999999, 31 | page = 0) => { 32 | try { 33 | 34 | const blogs = await Blog 35 | .find(searchParams) 36 | .limit(perPage) 37 | .skip(perPage * page) 38 | .populate('writter', 'first_name last_name joined') 39 | .populate('category', 'name') 40 | .populate('comments.people', 'first_name last_name img') 41 | .select(selectFields); 42 | return Promise.resolve(blogs); 43 | 44 | } catch (error) { 45 | if( error.name == 'CastError' ) { 46 | error = createErrors.BadRequest('Invalied blogId'); 47 | } 48 | return Promise.reject(error); 49 | } 50 | } 51 | 52 | const countBlogs = async(countParams) => { 53 | try { 54 | 55 | const numBlogs = await Blog 56 | .where(countParams) 57 | .countDocuments(); 58 | return Promise.resolve(numBlogs); 59 | 60 | } catch (error) { 61 | if( error.name == 'CastError' ) { 62 | error = createErrors.BadRequest('Invalied Id provided'); 63 | } 64 | return Promise.reject(error); 65 | } 66 | } 67 | 68 | const reactBlog = async(blog, reactBody) => { 69 | 70 | try { 71 | 72 | const allReacts = ['like', 'love', 'funny', 'sad', 'informative']; 73 | let oldReactName = ''; 74 | 75 | // remove all reacts of this user 76 | allReacts.forEach(react => { 77 | blog.reacts[react] = blog.reacts[react].filter(r => { 78 | if( reactBody.userId == r ) { 79 | oldReactName = react; 80 | } else { 81 | return r; 82 | } 83 | }); 84 | }); 85 | 86 | // set new react 87 | if( oldReactName != reactBody.reactName ) { 88 | blog.reacts[reactBody.reactName].push(reactBody.userId); 89 | } 90 | 91 | let updatedBlog = await blog.save(); 92 | 93 | return Promise.resolve(updatedBlog); 94 | 95 | } catch (error) { 96 | return Promise.reject(error); 97 | } 98 | } 99 | 100 | const postComment = async(blog, commentBody) => { 101 | try { 102 | 103 | blog.comments.push({ 104 | people: commentBody.userId, 105 | body: commentBody.body 106 | }); 107 | 108 | let updatedBlog = await blog.save(); 109 | updatedBlog = await updatedBlog 110 | .populate('comments.people', 'first_name last_name img') 111 | .execPopulate(); 112 | 113 | return Promise.resolve(updatedBlog); 114 | 115 | } catch (error) { 116 | return Promise.reject(error); 117 | } 118 | } 119 | 120 | const deleteComment = async(blog, commentBody) => { 121 | try { 122 | 123 | blog.comments = blog.comments.filter(c => { 124 | if( c._id == commentBody.id && c.people._id == commentBody.userId ) { 125 | return false; 126 | } else { 127 | return true; 128 | } 129 | }); 130 | 131 | const updatedBlog = await blog.save(); 132 | return Promise.resolve(updatedBlog); 133 | 134 | } catch (error) { 135 | return Promise.reject(error); 136 | } 137 | } 138 | 139 | // exports 140 | module.exports = { 141 | createBlog, 142 | readBlogs, 143 | countBlogs, 144 | reactBlog, 145 | postComment, 146 | deleteComment 147 | } -------------------------------------------------------------------------------- /services/category.service.js: -------------------------------------------------------------------------------- 1 | // imports 2 | const express = require('express'); 3 | const createErrors = require('http-errors'); 4 | const { Category } = require('../models/category.model'); 5 | 6 | // CRUD 7 | 8 | const createCategory = async(categoryBody) => { 9 | try { 10 | 11 | const newCategory = new Category(categoryBody); 12 | const savedCategory = await newCategory.save(); 13 | return Promise.resolve(savedCategory); 14 | 15 | } catch (error) { 16 | if( error.code && error.code == 11000 ) { 17 | error = createErrors.Forbidden(`${categoryBody.name} already exists`); 18 | return Promise.reject(error); 19 | } 20 | return Promise.reject(error); 21 | } 22 | } 23 | 24 | const readCategory = async() => { 25 | try { 26 | 27 | const categories = await Category 28 | .find() 29 | .select('name'); 30 | return Promise.resolve(categories); 31 | 32 | } catch (error) { 33 | return Promise.reject(error); 34 | } 35 | } 36 | 37 | // exports 38 | module.exports = { 39 | createCategory, 40 | readCategory 41 | } -------------------------------------------------------------------------------- /services/user.service.js: -------------------------------------------------------------------------------- 1 | // imports 2 | const express = require('express'); 3 | const createErrors = require('http-errors'); 4 | const { User } = require('../models/user.model'); 5 | const utils = require('../util'); 6 | 7 | // CRUD 8 | 9 | const createUser = async(userbody) => { 10 | try { 11 | 12 | const newUser = new User(userbody); 13 | const savedUser = await newUser.save(); 14 | return Promise.resolve(savedUser); 15 | 16 | } catch (error) { 17 | if( error.code && error.code == 11000 ) { 18 | error = createErrors.Conflict(`${userbody.email} already exists`); 19 | return Promise.reject(error); 20 | } 21 | return Promise.reject(error); 22 | } 23 | } 24 | 25 | const findUniqueUser = async(searchParams, selectFields = '') => { 26 | try { 27 | 28 | const userResult = await User 29 | .findOne(searchParams) 30 | .select(selectFields); 31 | if( !userResult ) { 32 | throw createErrors.NotFound('Incorrect information'); 33 | } 34 | 35 | return Promise.resolve(userResult); 36 | 37 | } catch (error) { 38 | if( error.name == 'CastError' ) { 39 | error = createErrors.BadRequest('Invalid bloggerId') 40 | } 41 | return Promise.reject(error); 42 | } 43 | } 44 | 45 | const updateUser = async(userBody) => { 46 | try { 47 | 48 | const userId = userBody.userId; 49 | const updateBody = utils.makeObjectExcept(userBody, ['userId']); 50 | const updatedUser = await User.updateOne({ _id: userId }, updateBody); 51 | 52 | return Promise.resolve(updatedUser); 53 | 54 | } catch (error) { 55 | return Promise.reject(error); 56 | } 57 | } 58 | 59 | // exports 60 | module.exports = { 61 | createUser, 62 | findUniqueUser, 63 | updateUser 64 | } -------------------------------------------------------------------------------- /uploads/blogs/default.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tazbin/blog-website-backend-nodejs-REST-API/c743cf8a4a8391db1f87ac81ddfb790beeccaa1a/uploads/blogs/default.jpg -------------------------------------------------------------------------------- /util/index.js: -------------------------------------------------------------------------------- 1 | getCurretDate = () => { 2 | let d = new Date(); 3 | let ye = new Intl.DateTimeFormat('en', { year: 'numeric' }).format(d); 4 | let mo = new Intl.DateTimeFormat('en', { month: 'short' }).format(d); 5 | let da = new Intl.DateTimeFormat('en', { day: '2-digit' }).format(d); 6 | 7 | const todayDate = `${da} ${mo} ${ye}`; 8 | return todayDate; 9 | } 10 | 11 | makeObjectSelected = (obj, props) => { 12 | let newObj = {}; 13 | 14 | props.forEach(p => { 15 | newObj[p] = obj[p]; 16 | }); 17 | return newObj; 18 | } 19 | 20 | makeObjectExcept = (obj, props) => { 21 | let newObj = {}; 22 | let hasPorp = false; 23 | 24 | for (const property in obj) { 25 | hasPorp = false; 26 | 27 | props.forEach(p => { 28 | if( property == p ) { 29 | hasPorp = true; 30 | } 31 | }); 32 | 33 | if( !hasPorp ) { 34 | newObj[property] = obj[property]; 35 | } 36 | } 37 | 38 | return newObj; 39 | } 40 | 41 | combineArrayObjectAndArray = (obj, objFields, array, arrayFieldName) => { 42 | let result = []; 43 | let sampleObj = {}; 44 | const length = obj.length; 45 | 46 | for(let i=0; i { 49 | sampleObj[a] = obj[i][a]; 50 | }); 51 | 52 | sampleObj[arrayFieldName] = array[i]; 53 | result.push(sampleObj); 54 | sampleObj = {}; 55 | 56 | } 57 | 58 | return result; 59 | } 60 | 61 | // exports 62 | module.exports = { 63 | getCurretDate, 64 | makeObjectSelected, 65 | makeObjectExcept, 66 | combineArrayObjectAndArray 67 | } -------------------------------------------------------------------------------- /validators/user.validator.js: -------------------------------------------------------------------------------- 1 | // imports 2 | const Joi = require('joi'); 3 | 4 | // defining user validation schemas 5 | 6 | const userSchema = Joi.object({ 7 | 8 | userId: Joi.string() 9 | .required(), 10 | 11 | img: Joi.string(), 12 | 13 | email: Joi.string() 14 | .email({ minDomainSegments: 2 }) 15 | .required(), 16 | 17 | first_name: Joi.string() 18 | .alphanum() 19 | .min(3) 20 | .max(30) 21 | .required(), 22 | 23 | last_name: Joi.string() 24 | .alphanum() 25 | .min(3) 26 | .max(30) 27 | .required(), 28 | 29 | job: Joi.string() 30 | .min(3) 31 | .max(30), 32 | 33 | address: Joi.string() 34 | .min(3) 35 | .max(30), 36 | 37 | about: Joi.string() 38 | .min(3) 39 | .max(150) 40 | 41 | }); 42 | 43 | const userRegisterSchema = Joi.object({ 44 | 45 | email: Joi.string() 46 | .email({ minDomainSegments: 2 }) 47 | .required(), 48 | 49 | first_name: Joi.string() 50 | .alphanum() 51 | .min(3) 52 | .max(30) 53 | .required(), 54 | 55 | last_name: Joi.string() 56 | .alphanum() 57 | .min(3) 58 | .max(30) 59 | .required(), 60 | 61 | password: Joi.string() 62 | .min(3) 63 | .max(30) 64 | .required() 65 | 66 | }); 67 | 68 | const userLoginSchema = Joi.object({ 69 | 70 | email: Joi.string() 71 | .email({ minDomainSegments: 2 }) 72 | .required(), 73 | 74 | password: Joi.string() 75 | .required() 76 | 77 | }); 78 | 79 | // exports 80 | module.exports = { 81 | userSchema, 82 | userRegisterSchema, 83 | userLoginSchema 84 | }; --------------------------------------------------------------------------------