Skip to content

Commit

Permalink
Merge pull request #155 from dali-lab/dev
Browse files Browse the repository at this point in the history
Add blog endpoints
  • Loading branch information
wu-ciesielska authored Feb 5, 2024
2 parents e79ba05 + c00a842 commit e853962
Show file tree
Hide file tree
Showing 19 changed files with 1,449 additions and 62 deletions.
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)
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@
"cors": "^2.8.5",
"dotenv": "^8.2.0",
"express": "^4.16.3",
"firebase-admin": "^12.0.0",
"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"
}
}
169 changes: 169 additions & 0 deletions src/controllers/blog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import mongoose from 'mongoose';
import { RESPONSE_CODES } from '../constants';
import { Blog } from '../models';
import { uploadFileToFirebase } from '../utils';

/**
* @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;
if (uploadedFile) {
const filePath = await uploadFileToFirebase(
uploadedFile.path,
`uploads/${uploadedFile.filename}`,
);
post.image = filePath;
}
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);

if (uploadedFile) {
const filePath = await uploadFileToFirebase(
uploadedFile.path,
`uploads/${uploadedFile.filename}`,
);

await Blog.updateOne({ _id: postId }, { ...fields, image: filePath });
} else {
await Blog.updateOne({ _id: postId }, fields);
}

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

0 comments on commit e853962

Please sign in to comment.