├── .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 | };
--------------------------------------------------------------------------------