Skip to content

Commit 096fb55

Browse files
authored
Feat/5 add post features (#14)
Add the ability to upload, view, and delete posts
1 parent e8cd2a2 commit 096fb55

File tree

66 files changed

+2673
-14719
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+2673
-14719
lines changed

backend/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
"test": "cross-env NODE_ENV=test jest --verbose --detectOpenHandles",
1010
"start": "cross-env NODE_ENV=production node src/index.ts",
1111
"dev": "cross-env NODE_ENV=development nodemon --files src/index.ts",
12-
"start:test": "cross-env NODE_ENV=test node src/index.ts",
12+
"start:test": "cross-env NODE_ENV=test ts-node --files src/index.ts",
1313
"start:dev": "cross-env NODE_ENV=development ts-node --files src/index.ts",
14-
"docker:mongo": "docker-compose -f docker-compose.dev.yml up",
14+
"docker:mongo": "docker-compose -f docker-compose.dev.yml up -d",
1515
"lint": "eslint .",
1616
"build:ui": "rm -rf build && cd ../frontend && npm run build && mv build ../backend"
1717
},

backend/requests/post-requests.rest

+47-3
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,57 @@ Content-Type: application/json
2020
}
2121

2222
###
23-
#creating post
24-
@token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImJvYmJ5ZG9iIiwiaWQiOiI2MjYxZTE1OTcxYmFhMzFkOWU3YjA5NjMiLCJpYXQiOjE2NTA1ODIwMjMsImV4cCI6MTY1MDU4NTYyM30.Y2FKPx7u1b_nKiDrN8IJjXkfRWQEMWX6UjHahM6Lgrc
23+
#creating post with caption and image
24+
# replace {{token}} with the token from the login response
25+
@token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImJvYmJ5ZG9iIiwiaWQiOiI2NWRhODA5YmQ5ZmVkMGNiYjI1YWY5YTYiLCJpYXQiOjE3MDkzNTI3MjUsImV4cCI6MTcwOTM1NjMyNX0.NkY1Lx7Mlb72qTtE4Y9eu4rNmObxVkhsvqiKr1NO-Ew
2526
POST http://localhost:3001/api/posts
2627
Content-Type: application/json
2728
Authorization: bearer {{token}}
2829

2930
{
3031
"caption": "a blue square",
31-
"image": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIj48cmVjdCBmaWxsPSIjMDBCMUZGIiB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIvPjwvc3ZnPg=="
32+
"imageDataUrl": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIj48cmVjdCBmaWxsPSIjMDBCMUZGIiB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIvPjwvc3ZnPg=="
3233
}
34+
35+
###
36+
#creating post with image and no caption (should pass)
37+
# replace {{token}} with the token from the login response
38+
@token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImJvYmJ5ZG9iIiwiaWQiOiI2NWRhODA5YmQ5ZmVkMGNiYjI1YWY5YTYiLCJpYXQiOjE3MDkzNTI3MjUsImV4cCI6MTcwOTM1NjMyNX0.NkY1Lx7Mlb72qTtE4Y9eu4rNmObxVkhsvqiKr1NO-Ew
39+
POST http://localhost:3001/api/posts
40+
Content-Type: application/json
41+
Authorization: bearer {{token}}
42+
43+
{
44+
"imageDataUrl": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIj48cmVjdCBmaWxsPSIjMDBCMUZGIiB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIvPjwvc3ZnPg=="
45+
}
46+
47+
###
48+
#creating post with no image (should fail)
49+
# replace {{token}} with the token from the login response
50+
@token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImJvYmJ5ZG9iIiwiaWQiOiI2NWRhODA5YmQ5ZmVkMGNiYjI1YWY5YTYiLCJpYXQiOjE3MDkzNTI3MjUsImV4cCI6MTcwOTM1NjMyNX0.NkY1Lx7Mlb72qTtE4Y9eu4rNmObxVkhsvqiKr1NO-Ew
51+
POST http://localhost:3001/api/posts
52+
Content-Type: application/json
53+
Authorization: bearer {{token}}
54+
55+
{
56+
"caption": "a blue square"
57+
}
58+
59+
###
60+
# deleting a post (use the id from the one of the responses above)
61+
# replace {{token}} with the token from the login response
62+
# should return 204
63+
@token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImJvYmJ5ZG9iIiwiaWQiOiI2NWRhODA5YmQ5ZmVkMGNiYjI1YWY5YTYiLCJpYXQiOjE3MDkzNTI3MjUsImV4cCI6MTcwOTM1NjMyNX0.NkY1Lx7Mlb72qTtE4Y9eu4rNmObxVkhsvqiKr1NO-Ew
64+
DELETE http://localhost:3001/api/posts/65e2a7dc58bb1d3eaacc4f89
65+
Content-Type: application/json
66+
Authorization: bearer {{token}}
67+
68+
69+
### deleting non-existing post
70+
# replace {{token}} with the token from the login response
71+
# delete an existing post use its id in the url
72+
# should return 404
73+
@token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImJvYmJ5ZG9iIiwiaWQiOiI2NWRhODA5YmQ5ZmVkMGNiYjI1YWY5YTYiLCJpYXQiOjE3MDkzNTI3MjUsImV4cCI6MTcwOTM1NjMyNX0.NkY1Lx7Mlb72qTtE4Y9eu4rNmObxVkhsvqiKr1NO-Ew
74+
DELETE http://localhost:3001/api/posts/65e2a7dc58bb1d3eaacc4f89
75+
Content-Type: application/json
76+
Authorization: bearer {{token}}

backend/src/routes/posts.ts

+22-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { NewPostFields, Image } from '../types';
1010
const router = express.Router();
1111

1212
// TODO: remove authenticator from this. instagram allows this.
13-
router.get('/:id', authenticator(), async (req, res) => {
13+
router.get('/:id', async (req, res) => {
1414
const post = await postService.getPost(req.params.id);
1515
if (!post) return res.status(404).end();
1616
return res.send(post);
@@ -20,7 +20,7 @@ router.post('/', authenticator(), async (req, res, next) => {
2020
let newPostFields: NewPostFields;
2121
let image: Image | undefined;
2222
// TODO: check if the user exists before proceeding?
23-
// or should that happen in the autheticator middleware?
23+
// or should that happen in the authenticator middleware?
2424
const user = await User.findById(req.userToken!.id);
2525

2626
try {
@@ -80,6 +80,26 @@ router.put('/:id', authenticator(), async (req, res, next) => {
8080
}
8181
});
8282

83+
router.delete('/:id', authenticator(), async (req, res, next) => {
84+
try {
85+
await postService.deletePostById(req.params.id, req.userToken!.id);
86+
return res.status(204).end();
87+
} catch (error) {
88+
const errorMessage = logger.getErrorMessage(error);
89+
logger.error(errorMessage);
90+
91+
if (/unauthorized/i.test(errorMessage)) {
92+
return res.status(401).send({ error: errorMessage });
93+
}
94+
95+
if (/post not found/i.test(errorMessage)) {
96+
return res.status(404).send({ error: errorMessage });
97+
}
98+
99+
return next(error);
100+
}
101+
});
102+
83103
// TODO: write a delete route
84104

85105
export default router;

backend/src/services/post-service.ts

+18
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { Post } from '../mongo';
22
import { NewPost, ProofedUpdatedPost } from '../types';
3+
import cloudinary from '../utils/cloudinary';
4+
35
// TODO: figure out what exactly needs to be populated.
46
const getPost = async (id: string) => {
57
const post = await Post.findById(id)
@@ -49,8 +51,24 @@ const updatePostById = async (
4951
return updatedPost;
5052
};
5153

54+
const deletePostById = async (id: string, requester: string) => {
55+
const deletedPost = await Post.findById(id);
56+
57+
if (!deletedPost) {
58+
throw new Error('Post not found');
59+
}
60+
61+
if (requester !== deletedPost.creator.toString()) {
62+
throw new Error('Unauthorized');
63+
}
64+
65+
await cloudinary.destroy(deletedPost.image.publicId);
66+
await deletedPost.remove();
67+
};
68+
5269
export default {
5370
getPost,
5471
addPost,
5572
updatePostById,
73+
deletePostById,
5674
};

backend/src/types.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@ export interface Comment {
1616

1717
export interface Post {
1818
id: string,
19-
creator: string, // ref -> User
19+
creator: User, // ref -> User
2020
caption?: string,
2121
image: Image,
2222
comments?: string[], // ref
2323
likes?: string[], // ref -> User
24+
createdAt: string, // Date
25+
updatedAt: string, // Date
2426
}
2527

2628
export interface User {
@@ -44,13 +46,13 @@ export interface NewUser {
4446
}
4547

4648
export interface NewPostFields {
47-
caption: string,
49+
caption?: string,
4850
imageDataUrl: string,
4951
}
5052

5153
export interface NewPost {
52-
caption: string,
53-
image?: Image,
54+
caption?: string,
55+
image: Image,
5456
}
5557

5658
export interface ProofedUpdatedUserFields {

backend/src/utils/cloudinary.ts

+11
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,18 @@ const destroy = async (publicId: string) => {
2121
await cloudinary.uploader.destroy(publicId);
2222
};
2323

24+
const checkIfImageExists = async (publicId: string) => {
25+
try {
26+
await cloudinary.api.resource(publicId);
27+
return true;
28+
} catch (error) {
29+
console.error(error);
30+
return false;
31+
}
32+
};
33+
2434
export default {
2535
upload,
2636
destroy,
37+
checkIfImageExists,
2738
};

backend/src/utils/field-parsers.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export const parseStringField = (param: unknown, fieldKey: string) => {
1313

1414
const parseDataURIField = (param: unknown) => {
1515
if (!param || !isString(param) || !validDataUrl(param)) {
16-
throw new Error('Malformatted or missing image');
16+
throw new Error('Malformed or missing image');
1717
}
1818

1919
return param;
@@ -84,8 +84,10 @@ interface UnknownPostFields {
8484
}
8585

8686
const proofPostFields = ({ caption, imageDataUrl }: UnknownPostFields) => {
87+
const optionalCaption = caption ? parseStringField(caption, 'caption') : '';
88+
8789
const postFields = {
88-
caption: parseStringField(caption, 'caption'),
90+
caption: optionalCaption,
8991
imageDataUrl: parseDataURIField(imageDataUrl),
9092
};
9193

0 commit comments

Comments
 (0)