Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add blog endpoint #150

Merged
merged 5 commits into from
Nov 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 23 additions & 22 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -1,27 +1,28 @@
{
extends: "airbnb-base",
parser: "@babel/eslint-parser",
env: {
browser: false,
node: true,
es6: true
"extends": "airbnb-base",
"parser": "@babel/eslint-parser",
"env": {
"browser": false,
"node": true,
"es6": true
},
rules: {
strict: 0,
quotes: [2, "single"],
no-else-return: 0,
new-cap: ["error", {"capIsNewExceptions": ["Router"]}],
no-console: 0,
import/no-unresolved: [2, { commonjs: true, caseSensitive: false}],
no-unused-vars: ["error", { "vars": "all", "args": "none" }],
no-underscore-dangle: 0,
arrow-body-style: ["error", "always"],
no-shadow: ["error", { "allow": ["done", "res", "cb", "err", "resolve", "reject"] }],
no-use-before-define: ["error", { "functions": false }],
max-len: 0,
no-param-reassign: 0
"rules": {
"strict": 0,
"quotes": [2, "single"],
"no-else-return": 0,
"new-cap": ["error", {"capIsNewExceptions": ["Router"]}],
"no-console": 0,
"import/no-unresolved": [2, { "commonjs": true, "caseSensitive": false}],
"import/no-extraneous-dependencies": ["error", {"devDependencies": true}],
"no-unused-vars": ["error", { "vars": "all", "args": "none" }],
"no-underscore-dangle": 0,
"arrow-body-style": ["error", "always"],
"no-shadow": ["error", { "allow": ["done", "res", "cb", "err", "resolve", "reject"] }],
"no-use-before-define": ["error", { "functions": false }],
"max-len": 0,
"no-param-reassign": 0
},
plugins: [
'import'
"plugins": [
"import"
]
}
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ yarn-error.log
.DS_Store
.env
npm-debug.log
build
build
public/uploads
.vscode
28 changes: 28 additions & 0 deletions docs/BLOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Blog Operations

## `GET /blog`

Returns all blog post objects in the database.

## `GET /blog/:id`

Returns blog post object of specified `id` in the database.

## `PUT /blog/:id`

Expects authorization header with Bearer token.
Updates blog post object of specified `id` in the database with the fields supplied in a JSON body.

## `DELETE /blog/:id`

Expects authorization header with Bearer token.
Deletes blog post object of specified `id` in the database

## `POST /blog/create`

Expects authorization header with Bearer token.
Creates blog post with fields provided in multipart/form-data body. Requires `title` and `body`, has optional `image` field. `author` is fetched from the session info.

## `GET /blog/user/:id`

Returns blog post object of specified author `id` in the database.
1 change: 1 addition & 0 deletions docs/ROUTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
- [Summarized Ranger District Data](./SUMMARIZED-RD.md)
- [Unsummarized Trapping Data](./UNSUMMARIZED.md)
- [Users](./USERS.md)
- [Blog](./BLOG.md)
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"jwt-simple": "^0.5.6",
"mongoose": "^6.10.4",
"morgan": "^1.9.0",
"multer": "^1.4.5-lts.1",
"nodemailer": "^6.6.1",
"passport": "^0.6.0",
"passport-jwt": "^4.0.0"
Expand Down
15 changes: 8 additions & 7 deletions src/constants/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const COLLECTION_NAMES = {
summarizedRangerDistrict: 'summarizedrangerdistricts',
unsummarized: 'unsummarizedtrappings',
users: 'users',
blogPost: 'blogs',
};

/**
Expand All @@ -16,7 +17,9 @@ const COLLECTION_NAMES = {
*/
export const extractCredentialsFromAuthorization = (authorization) => {
// adapted from: https://gist.github.com/charlesdaniel/1686663
const auth = Buffer.from(authorization.split(' ')[1], 'base64').toString().split(':');
const auth = Buffer.from(authorization.split(' ')[1], 'base64')
.toString()
.split(':');

return {
email: auth[0],
Expand All @@ -37,12 +40,10 @@ export const generateResponse = (responseType, payload) => {
return {
status,
type,
...(status === RESPONSE_CODES.SUCCESS.status ? { data: payload } : { error: payload }),
...(status === RESPONSE_CODES.SUCCESS.status
? { data: payload }
: { error: payload }),
};
};

export {
COLLECTION_NAMES,
RESPONSE_CODES,
RESPONSE_TYPES,
};
export { COLLECTION_NAMES, RESPONSE_CODES, RESPONSE_TYPES };
4 changes: 4 additions & 0 deletions src/constants/response-codes.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,9 @@
"NO_CONTENT": {
"status": 200,
"type": "NOTHING TO UPDATE"
},
"VALIDATION_ERROR": {
"status": 403,
"type": "VALIDATION_ERROR"
}
}
157 changes: 157 additions & 0 deletions src/controllers/blog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import mongoose from 'mongoose';
import { RESPONSE_CODES } from '../constants';
import { Blog } from '../models';

/**
* @description retrieves blog post object
* @param {String} id blog post id
* @returns {Promise<Blog>} promise that resolves to blog post object or error
*/
export const getBlogPostById = async (id) => {
try {
const blogPost = await Blog.findById(id);

if (blogPost) {
return {
...RESPONSE_CODES.SUCCESS,
blogPost,
};
}
return RESPONSE_CODES.NOT_FOUND;
} catch (error) {
console.log(error);
return RESPONSE_CODES.NOT_FOUND;
}
};

/**
* @description retrieves blog all blog posts by given author
* @param {String} id author id
* @returns {Promise<Blog[]>} promise that resolves to blog post objects array or error
*/
export const getBlogPostsByAuthorId = async (id) => {
try {
const blogPosts = await Blog.find({ authorId: id });

if (blogPosts) {
return {
...RESPONSE_CODES.SUCCESS,
blogPosts,
};
}
return RESPONSE_CODES.NOT_FOUND;
} catch (error) {
console.error(error);
return RESPONSE_CODES.NOT_FOUND;
}
};

/**
* @description creates blog post object in database
* @param {Object} fields blog post fields (title, body)
* @param {File} uploadedFile the image that was uploaded by the user
* @param {Object} user user who created the blog post
* @returns {Promise<Blog>} promise that resolves to blog object or error
*/
export const createBlogPost = async (fields, uploadedFile, user) => {
const { title, body } = fields;

const post = new Blog();

const { first_name: firstName, last_name: lastName, _id: id } = user;

post.title = title;
post.body = body;
post.author = `${firstName} ${lastName}`;
post.authorId = id;
post.image = uploadedFile?.path || null;

try {
const savedPost = await post.save();
return savedPost.toJSON();
} catch (error) {
if (error.name === 'ValidationError') {
if (error.errors.title) {
const errorToThrow = new Error(error.errors.title.properties.message);
errorToThrow.code = RESPONSE_CODES.VALIDATION_ERROR;
throw errorToThrow;
} else if (error.errors.body) {
const errorToThrow = new Error(error.errors.body.properties.message);
errorToThrow.code = RESPONSE_CODES.VALIDATION_ERROR;
throw errorToThrow;
}
}
throw new Error({
code: RESPONSE_CODES.INTERNAL_ERROR,
error,
});
}
};

/**
* @description update blog post object
* @param {Object} fields blog post fields (title, body, image)
* @param {String} id blog post id
* @returns {Promise<Blog>} promise that resolves to blog object or error
*/
export const updateBlogPost = async (id, fields, uploadedFile) => {
const { id: providedId, _id: providedUnderId } = fields;

// reject update of id
if (providedId || providedUnderId) {
throw new Error({
code: RESPONSE_CODES.BAD_REQUEST,
error: { message: 'Cannot update blog post id' },
});
}

try {
const postId = new mongoose.Types.ObjectId(id);

await Blog.updateOne(
{ _id: postId },
{ ...fields, image: uploadedFile?.path || null },
);

const blogPost = await Blog.findById(postId);

await blogPost.save();

return {
...RESPONSE_CODES.SUCCESS,
blogPost: blogPost.toJSON(),
};
} catch (error) {
if (error.name === 'ValidationError') {
if (error.errors.title) {
const errorToThrow = new Error(error.errors.title.properties.message);
errorToThrow.code = RESPONSE_CODES.VALIDATION_ERROR;
throw errorToThrow;
} else if (error.errors.body) {
const errorToThrow = new Error(error.errors.body.properties.message);
errorToThrow.code = RESPONSE_CODES.VALIDATION_ERROR;
throw errorToThrow;
}
}
throw new Error({
code: RESPONSE_CODES.INTERNAL_ERROR,
error,
});
}
};

/**
* @description removes blog post with given id
* @param {String} id blog post id
* @param {String} userId id of a user who requests deletion
* @returns {Promise<Blog>} promise that resolves to success object or error
*/
export const deleteBlogPost = async (id, userId) => {
try {
await Blog.deleteOne({ authorId: userId, _id: id });
return RESPONSE_CODES.SUCCESS;
} catch (error) {
console.log(error);
return error;
}
};
5 changes: 2 additions & 3 deletions src/controllers/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/* eslint-disable import/prefer-default-export */
import * as User from './user';
import * as Blog from './blog';

export {
User,
};
export { User, Blog };
8 changes: 8 additions & 0 deletions src/controllers/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,14 @@ export const tokenForUser = (email) => {
return jwt.encode({ sub: email, iat: timestamp }, process.env.AUTH_SECRET);
};

export const getUserByJWT = async (authorization) => {
const JWTtoken = authorization.split(' ')[1];
const decoded = jwt.decode(JWTtoken, process.env.AUTH_SECRET);
const user = await getUserByEmail(decoded.sub);

return user;
};

export const userWithEmailExists = async (email) => {
try {
const user = await getUserByEmail(email);
Expand Down
35 changes: 35 additions & 0 deletions src/models/blog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import mongoose, { Schema } from 'mongoose';

const BlogPostSchema = new Schema(
{
title: {
type: String,
required: [true, 'A blog post must have a title'],
},
body: {
type: String,
required: [true, 'A blog post must have a body'],
},
author: {
type: String,
required: true,
},
authorId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
},
image: {
type: String,
},
},
{
toJSON: {
virtuals: true,
},
timestamps: { createdAt: 'date_created', updatedAt: 'date_edited' },
},
);

const BlogModel = mongoose.model('Blog', BlogPostSchema);

export default BlogModel;
2 changes: 2 additions & 0 deletions src/models/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/* eslint-disable import/prefer-default-export */
import User from './user';
import Blog from './blog';

export {
User,
Blog,
};
Loading