Skip to content

Commit 1fce1d0

Browse files
committed
part 4d
1 parent 9a26266 commit 1fce1d0

11 files changed

Lines changed: 1900 additions & 51 deletions

File tree

part4/blog/app.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
const express = require("express");
2+
require("express-async-errors");
23
const app = express();
3-
require("express-async-error")
44
const cors = require("cors");
55
const mongoose = require("mongoose");
66
const config = require("./utils/config");
77
const blogRouter = require("./controllers/blog");
8-
const morgan = require("morgan")
9-
const middleware = require("./utils/middleware")
8+
const userRouter = require("./controllers/users");
9+
const morgan = require("morgan");
10+
const middleware = require("./utils/middleware");
11+
const loginRouter = require("./controllers/login");
1012

1113
morgan.token("body", function (req, res) {
1214
return JSON.stringify(req.body);
@@ -25,7 +27,9 @@ app.use(
2527
morgan(":method :url :status :res[content-length] - :response-time ms :body")
2628
);
2729
app.use("/api/blogs", blogRouter);
28-
app.use(middleware.unknownEndpoint)
29-
app.use(middleware.errorHandler)
30+
app.use("/api/users", userRouter);
31+
app.use("/api/login", loginRouter);
32+
app.use(middleware.unknownEndpoint);
33+
app.use(middleware.errorHandler);
3034

3135
module.exports = app;

part4/blog/controllers/blog.js

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,57 @@
1+
require("dotenv").config();
12
const blogRouter = require("express").Router();
23
const Blog = require("../models/blog");
4+
const User = require("../models/user");
5+
const jwt = require("jsonwebtoken");
6+
const { tokenExtractor } = require("../utils/middleware");
37

48
blogRouter
59
.route("/")
610
.get(async (req, res) => {
7-
const blogs = await Blog.find({});
11+
const blogs = await Blog.find({}).populate("user");
812
res.json(blogs);
913
})
10-
.post(async (req, res) => {
14+
.post(tokenExtractor, async (req, res) => {
1115
const { author, title, url, likes } = req.body;
1216

17+
const user = await User.findById(req.user);
18+
1319
if (!title || !url) res.status(400).end();
1420

1521
const blog = new Blog({
1622
author: author || "unknown",
1723
title,
1824
url,
1925
likes: likes || 0,
26+
user: user._id,
2027
});
2128

22-
const result = await blog.save();
23-
res.status(201).json(result);
29+
const savedBlog = await blog.save();
30+
user.blogs = [...user.blogs, savedBlog._id];
31+
await user.save();
32+
res.status(201).json(savedBlog);
2433
});
2534

2635
blogRouter
2736
.route("/:id")
28-
.delete(async (req, res) => {
29-
await Blog.findByIdAndRemove(req.params.id);
30-
res.status(204).end();
37+
.get(async (req, res) => {
38+
const blog = await Blog.findById(req.params.id);
39+
if (blog) {
40+
res.json(blog.toJSON());
41+
} else {
42+
res.status(404).end();
43+
}
44+
})
45+
.delete(tokenExtractor, async (req, res) => {
46+
const { user } = req;
47+
const blog = await Blog.findById(req.params.id);
48+
49+
if (blog === null) return res.status(400).end();
50+
51+
if (blog.user.toString() === user) {
52+
await Blog.findByIdAndRemove(req.params.id);
53+
res.status(204).end();
54+
}
3155
})
3256
.put(async (req, res) => {
3357
const updatedBlog = await Blog.findByIdAndUpdate(req.params.id, req.body, {

part4/blog/controllers/login.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
require("dotenv").config();
2+
const router = require("express").Router();
3+
const jwt = require("jsonwebtoken");
4+
const bcrypt = require("bcrypt");
5+
const User = require("../models/user");
6+
7+
router.route("/").post(async (req, res) => {
8+
const { username, password } = req.body;
9+
10+
const user = await User.findOne({ username });
11+
const verifyPassword =
12+
user !== null && (await bcrypt.compare(password, user.passwordHash));
13+
14+
if (!(user && verifyPassword)) {
15+
return res.status(401).json({
16+
error: "invalid username or password",
17+
});
18+
}
19+
20+
const token = jwt.sign(
21+
{ username: user.username, id: user._id },
22+
process.env.SECRET,
23+
{ expiresIn: "1hr" }
24+
);
25+
26+
res.status(200).send({ token, username: user.username, name: user.name });
27+
});
28+
29+
module.exports = router;

part4/blog/controllers/users.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
const router = require("express").Router();
2+
const User = require("../models/user");
3+
const bcrypt = require("bcrypt");
4+
5+
router
6+
.route("/")
7+
.get(async (req, res) => {
8+
const users = await User.find({}).populate("blogs");
9+
res.json(users);
10+
})
11+
.post(async (req, res) => {
12+
const salt = await bcrypt.genSalt(10);
13+
const passwordHash = await bcrypt.hash(req.body.password, salt);
14+
15+
const newUser = new User({
16+
...req.body,
17+
passwordHash,
18+
});
19+
const result = await newUser.save();
20+
21+
res.status(201).json(result);
22+
});
23+
24+
module.exports = router;

part4/blog/models/blog.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ const blogSchema = new mongoose.Schema({
66
author: { type: String },
77
url: { type: String },
88
likes: { type: Number },
9+
user: {
10+
type: mongoose.Schema.Types.ObjectId,
11+
ref: "User",
12+
},
913
});
1014

1115
blogSchema.plugin(uniqueValidator);

part4/blog/models/user.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
const mongoose = require("mongoose");
2+
const uniqueValidator = require("mongoose-unique-validator");
3+
4+
const userSchema = new mongoose.Schema({
5+
username: { type: String, unique: true },
6+
passwordHash: String,
7+
name: { type: String, minLength: 4 },
8+
blogs: [
9+
{
10+
type: mongoose.Schema.Types.ObjectId,
11+
ref: "Blog",
12+
},
13+
],
14+
});
15+
16+
userSchema.plugin(uniqueValidator, {
17+
message: "Error, {PATH} already exist.",
18+
});
19+
userSchema.set("toJSON", {
20+
transform: (document, returnedObject) => {
21+
returnedObject.id = returnedObject._id.toString();
22+
delete returnedObject._id;
23+
delete returnedObject.__v;
24+
// the passwordHash should not be revealed
25+
delete returnedObject.passwordHash;
26+
},
27+
});
28+
29+
module.exports = mongoose.model("User", userSchema);

part4/blog/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@
1010
"author": "Joseph Odunsi",
1111
"license": "MIT",
1212
"dependencies": {
13+
"bcrypt": "^5.0.1",
1314
"cors": "^2.8.5",
1415
"cross-env": "^7.0.3",
1516
"dotenv": "^8.2.0",
1617
"express": "^4.17.1",
17-
"express-async-error": "^0.0.2",
18+
"express-async-errors": "^3.1.1",
19+
"jsonwebtoken": "^8.5.1",
1820
"mongoose": "^5.12.5",
1921
"morgan": "^1.10.0",
2022
"supertest": "^6.1.3"

part4/blog/test/api.test.js

Lines changed: 73 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,20 @@ const Blog = require("../models/blog");
55
const { initialBlogs, blogsInDb } = require("./test_helper");
66

77
const api = supertest(app);
8+
let token;
89

9-
beforeEach(async() => {
10+
beforeAll(async () => {
11+
await api
12+
.post("/api/users")
13+
.send({ username: "test", name: "test", password: "test" });
14+
15+
const response = await api
16+
.post("/api/login")
17+
.send({ username: "test", password: "test" });
18+
token = response.body.token;
19+
});
20+
21+
beforeEach(async () => {
1022
await Blog.deleteMany({});
1123
await Blog.insertMany(initialBlogs);
1224
});
@@ -17,7 +29,21 @@ test("get blogs", async () => {
1729
.expect(200)
1830
.expect("Content-Type", /application\/json/);
1931

20-
expect(response.body).toHaveLength(0);
32+
expect(response.body).toHaveLength(initialBlogs.length);
33+
});
34+
35+
test("should require authentication to save a blog", async () => {
36+
const newBlog = {
37+
title: "Type wars",
38+
author: "Unknown",
39+
url: "http://blog.cleancoder.com/uncle-bob/2016/05/01/TypeWars.html",
40+
likes: 2,
41+
};
42+
43+
await api.post("/api/blogs").send(newBlog).expect(401).expect("Unauthorized");
44+
45+
const res = await api.get("/api/blogs");
46+
expect(res.body).toHaveLength(initialBlogs.length);
2147
});
2248

2349
test("save a blog", async () => {
@@ -26,10 +52,12 @@ test("save a blog", async () => {
2652
author: "Unknown",
2753
url: "http://blog.cleancoder.com/uncle-bob/2016/05/01/TypeWars.html",
2854
likes: 2,
55+
user: token.id,
2956
};
3057

3158
await api
3259
.post("/api/blogs")
60+
.set("Authorization", `Bearer ${token}`)
3361
.send(newBlog)
3462
.expect(201)
3563
.expect("Content-Type", /application\/json/);
@@ -51,10 +79,12 @@ test("if the likes property is missing, default value to 0", async () => {
5179
title: "Type wars",
5280
author: "Unknown",
5381
url: "http://blog.cleancoder.com/uncle-bob/2016/05/01/TypeWars.html",
82+
user: token.id,
5483
};
5584

5685
const { body: savedBlog } = await api
5786
.post("/api/blogs")
87+
.set("Authorization", `Bearer ${token}`)
5888
.send(newBlog)
5989
.expect(201)
6090
.expect("Content-Type", /application\/json/);
@@ -67,37 +97,58 @@ test("return bad request if title and url is missing", async () => {
6797
author: "John Doe",
6898
};
6999

70-
const response = await api.post("/api/blogs").send(newBlog);
100+
const response = await api
101+
.post("/api/blogs")
102+
.set("Authorization", `Bearer ${token}`)
103+
.send(newBlog);
71104
expect(response.statusCode).toBe(400);
72105

73-
const {body:blogs} = await api.get("/api/blogs")
74-
expect(blogs).toHaveLength(initialBlogs.length)
106+
const { body: blogs } = await api.get("/api/blogs");
107+
expect(blogs).toHaveLength(initialBlogs.length);
75108
});
76109

77-
test("confirm deletion of a blog", async()=>{
78-
const blogsAtStart = await blogsInDb()
79-
const blogToDelete = blogsAtStart[0]
110+
test("confirm deletion of a blog", async () => {
111+
const blogsAtStart = await blogsInDb();
112+
const newBlog = {
113+
title: "Type wars",
114+
author: "Unknown",
115+
url: "http://blog.cleancoder.com/uncle-bob/2016/05/01/TypeWars.html",
116+
user: token.id,
117+
};
80118

81-
const response = await api.delete(`/api/blogs/${blogToDelete.id}`)
82-
expect(response.status).toBe(204)
83-
84-
const {body:blogsAtEnd} = await api.get("/api/blogs")
85-
expect(blogsAtEnd).toHaveLength(initialBlogs.length - 1)
86-
const contents = blogsAtEnd.map(blog => blog.title)
119+
const { body: savedBlog } = await api
120+
.post("/api/blogs")
121+
.set("Authorization", `Bearer ${token}`)
122+
.send(newBlog)
123+
.expect(201)
124+
.expect("Content-Type", /application\/json/);
125+
126+
const response = await api
127+
.delete(`/api/blogs/${savedBlog.id}`)
128+
.set("Authorization", `Bearer ${token}`);
129+
expect(response.status).toBe(204);
130+
131+
const { body: blogsAtEnd } = await api.get("/api/blogs");
132+
expect(blogsAtEnd).toHaveLength(blogsAtStart.length);
133+
const contents = blogsAtEnd.map((blog) => blog.title);
87134

88-
expect(contents).not.toContain(blogToDelete.title)
89-
})
135+
expect(contents).not.toContain(savedBlog.title);
136+
});
90137

91-
test("update a blog", async()=>{
138+
test("update a blog", async () => {
92139
const blogsAtStart = await blogsInDb();
93-
const {title, author, url, id} = blogsAtStart[0]
140+
const { title, author, url, id } = blogsAtStart[0];
94141

95-
const blog = {title, url, author, likes: 30}
142+
const blog = { title, url, author, likes: 30 };
96143

97-
const {body:updatedBlog} = await api.put(`/api/blogs/${id}`).send(blog).expect(200)
144+
const { body: updatedBlog } = await api
145+
.put(`/api/blogs/${id}`)
146+
.set("Authorization", `Bearer ${token}`)
147+
.send(blog)
148+
.expect(200);
98149

99-
expect(updatedBlog.likes).toBe(30)
100-
})
150+
expect(updatedBlog.likes).toBe(30);
151+
});
101152

102153
afterAll(async (done) => {
103154
await mongoose.connection.close();

0 commit comments

Comments
 (0)