diff --git a/README.md b/README.md index 0f9f073..ef495ef 100644 --- a/README.md +++ b/README.md @@ -9,3 +9,19 @@ Install dependencies with `npm install`, then start the server by running `npm r ## View it live Every project should be deployed somewhere. Be sure to include the link to the deployed project so that the viewer can click around and see what it's all about. + +# Project has been developed in three steps + +## First stage + +- API endpoints for creating, listing and liking thoughts to work with the existing Happy Thoughts App +- work with static data + +## Second stage +- API uses Mongo DB for storing thoughts +- Data is now persistent + +## Third stage +- Support for user authentication +- Post creation, edition and deletion now require authentication +- deployed on onRender: https://js-project-api-vj6h.onrender.com diff --git a/data.json b/data.json deleted file mode 100644 index a2c844f..0000000 --- a/data.json +++ /dev/null @@ -1,121 +0,0 @@ -[ - { - "_id": "682bab8c12155b00101732ce", - "message": "Berlin baby", - "hearts": 37, - "createdAt": "2025-05-19T22:07:08.999Z", - "__v": 0 - }, - { - "_id": "682e53cc4fddf50010bbe739", - "message": "My family!", - "hearts": 0, - "createdAt": "2025-05-22T22:29:32.232Z", - "__v": 0 - }, - { - "_id": "682e4f844fddf50010bbe738", - "message": "The smell of coffee in the morning....", - "hearts": 23, - "createdAt": "2025-05-22T22:11:16.075Z", - "__v": 0 - }, - { - "_id": "682e48bf4fddf50010bbe737", - "message": "Newly washed bedlinen, kids that sleeps through the night.. FINGERS CROSSED 🤞🏼\n", - "hearts": 6, - "createdAt": "2025-05-21T21:42:23.862Z", - "__v": 0 - }, - { - "_id": "682e45804fddf50010bbe736", - "message": "I am happy that I feel healthy and have energy again", - "hearts": 13, - "createdAt": "2025-05-21T21:28:32.196Z", - "__v": 0 - }, - { - "_id": "682e23fecf615800105107aa", - "message": "cold beer", - "hearts": 2, - "createdAt": "2025-05-21T19:05:34.113Z", - "__v": 0 - }, - { - "_id": "682e22aecf615800105107a9", - "message": "My friend is visiting this weekend! <3", - "hearts": 6, - "createdAt": "2025-05-21T18:59:58.121Z", - "__v": 0 - }, - { - "_id": "682cec1b17487d0010a298b6", - "message": "A god joke: \nWhy did the scarecrow win an award?\nBecause he was outstanding in his field!", - "hearts": 12, - "createdAt": "2025-05-20T20:54:51.082Z", - "__v": 0 - }, - { - "_id": "682cebbe17487d0010a298b5", - "message": "Tacos and tequila🌮🍹", - "hearts": 2, - "createdAt": "2025-05-19T20:53:18.899Z", - "__v": 0 - }, - { - "_id": "682ceb5617487d0010a298b4", - "message": "Netflix and late night ice-cream🍦", - "hearts": 1, - "createdAt": "2025-05-18T20:51:34.494Z", - "__v": 0 - }, - { - "_id": "682c99ba3bff2d0010f5d44e", - "message": "Summer is coming...", - "hearts": 2, - "createdAt": "2025-05-20T15:03:22.379Z", - "__v": 0 - }, - { - "_id": "682c706c951f7a0017130024", - "message": "Exercise? I thought you said extra fries! 🍟😂", - "hearts": 14, - "createdAt": "2025-05-20T12:07:08.185Z", - "__v": 0 - }, - { - "_id": "682c6fe1951f7a0017130023", - "message": "I’m on a seafood diet. I see food, and I eat it.", - "hearts": 4, - "createdAt": "2025-05-20T12:04:49.978Z", - "__v": 0 - }, - { - "_id": "682c6f0e951f7a0017130022", - "message": "Cute monkeys🐒", - "hearts": 2, - "createdAt": "2025-05-20T12:01:18.308Z", - "__v": 0 - }, - { - "_id": "682c6e65951f7a0017130021", - "message": "The weather is nice!", - "hearts": 0, - "createdAt": "2025-05-20T11:58:29.662Z", - "__v": 0 - }, - { - "_id": "682bfdb4270ca300105af221", - "message": "good vibes and good things", - "hearts": 3, - "createdAt": "2025-05-20T03:57:40.322Z", - "__v": 0 - }, - { - "_id": "682bab8c12155b00101732ce", - "message": "Berlin baby", - "hearts": 37, - "createdAt": "2025-05-19T22:07:08.999Z", - "__v": 0 - } -] \ No newline at end of file diff --git a/package.json b/package.json index bf25bb6..36a0648 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,12 @@ "@babel/core": "^7.17.9", "@babel/node": "^7.16.8", "@babel/preset-env": "^7.16.11", + "bcryptjs": "^3.0.2", "cors": "^2.8.5", + "dotenv": "^16.5.0", "express": "^4.17.3", + "moment": "^2.30.1", + "mongoose": "^8.15.2", "nodemon": "^3.0.1" } } diff --git a/pull_request_template.md b/pull_request_template.md index fb9fdc3..c6ca86d 100644 --- a/pull_request_template.md +++ b/pull_request_template.md @@ -1 +1,4 @@ -Please include your Render link here. \ No newline at end of file +Please include your Render link here. + +Here you go: +https://js-project-api-vj6h.onrender.com \ No newline at end of file diff --git a/server.js b/server.js index f47771b..798bed7 100644 --- a/server.js +++ b/server.js @@ -1,22 +1,237 @@ -import cors from "cors" -import express from "express" +import cors from "cors"; +import express from "express"; +import crypto from "crypto"; +import bcrypt from "bcryptjs"; +import dotenv from "dotenv"; +import mongoose from "mongoose"; + +// loading environment from env file +dotenv.config(); + +//connecting to MondoDB +const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/thoughts"; +mongoose.connect(mongoUrl); + +//data model for Thoughts +const thoughtSchema = new mongoose.Schema({ + _id: { + type: String, + unique: true, + default: () => { + return crypto.randomBytes(12).toString("hex"); + }, + }, + message: { + type: String, + require: true, + }, + userName: { + type: String, + require: true, + }, + hearts: { + type: Number, + default: 0, + }, + createdAt: { + type: Date, + default: Date.now, + }, + __v: { + type: Number, + default: 0, + }, +}); + +const Thought = mongoose.model("Thought", thoughtSchema); + +//user schema +const User = mongoose.model("User", { + name: { + type: String, + unique: true, + }, + password: { + type: String, + required: true, + }, + accessToken: { + type: String, + default: () => crypto.randomBytes(128).toString("hex"), + }, +}); + +//example of a user +//const user = new User({name: "Mary", password: bcrypt.hashSync("Maryspassword")}); +//user.save(); // Defines the port the app will run on. Defaults to 8080, but can be overridden // when starting the server. Example command to overwrite PORT env variable value: // PORT=9000 npm start -const port = process.env.PORT || 8080 -const app = express() +const port = process.env.PORT || 8080; +const app = express(); // Add middlewares to enable cors and json body parsing -app.use(cors()) -app.use(express.json()) +app.use(cors()); +app.use(express.json()); + +//endpoint for creating a user +app.post("/users", async (req, res) => { + const userName = req.body.user; + const password = req.body.password; + if (!userName || !password) { + res + .status(400) + .send({ error: "Could not create user. User or password missing" }); + return; + } + try { + const user = new User({ + name: userName, + password: bcrypt.hashSync(password), + }); + await user.save(); + res.status(201).send(user); + } catch (error) { + res.status(400).send({ error: error }); + } +}); + +//endpoint for logging +app.post("/users/:userName", async (req, res) => { + const userName = req.params.userName; + const passEncrypted = req.body.password; + if (!passEncrypted) { + res.json({ error: "password missing in the body of request" }); + } + const user = await User.findOne({ name: userName }); + + if (user && bcrypt.compareSync(passEncrypted, user.password)) { + //success + res.json({ userName: user.name, accessToken: user.accessToken }); + } else { + //failure + res.status(401).json({ notFound: true }); + } +}); + +///authenticating middleware +const authenticateUser = async (req, res, next) => { + const user = await User.findOne({ accessToken: req.header("Authorization") }); + if (user) { + req.user = user; + next(); + } else { + res.status(401).json({ error: "User logged out" }); + } +}; // Start defining your routes here app.get("/", (req, res) => { - res.send("Hello Technigo!") + res.send("Hello Technigo!"); +}); + +//listing all the thoughts +app.get("/thoughts", async (req, res) => { + const thoughts = await Thought.find().sort({createdAt: -1}).limit(20); + res.json(thoughts); +}); + +//sending a single thought +app.get("/thoughts/:thoughtId", async (req, res) => { + const id = req.params.thoughtId; + const post = await Thought.find({ + _id: id, + }); + if (post.length === 0) { + res.status(404).json({ error: "Thought not found" }); + } + res.json(post[0]); +}); + +//removing a single thought +app.delete("/thoughts/:thoughtId", authenticateUser); +app.delete("/thoughts/:thoughtId", async (req, res) => { + const id = req.params.thoughtId; + const result = await Thought.deleteOne({ + _id: id, + }); + if (result.deletedCount === 0) { + res.status(404).json({ error: "Thought not found" }); + } + res.json(result); +}); + +// editing a thought +app.patch('/thoughts/:thoughtId', authenticateUser); +app.patch('/thoughts/:thoughtId', async (req, res) => { + try { + const edited = await Thought.findOneAndUpdate( + { _id: req.params.thoughtId, userName: req.user.name }, + { message: req.body.message }, + { new: true, runValidators: true } + ) + if (!edited) { + res.status(404).json({ + error: "Thought not found, or you are not the owner of this thought", + }); + } + else { + res.json(edited) + } + } catch (error) { + res.status(404).json({ + error: "Thought not found, or you are not the owner of this thought", + }); + } }) -// Start the server + +// accepting/ adding a new thought + +app.post("/thoughts", authenticateUser); +app.post("/thoughts", async (req, res) => { + const note = req.body.message; + + if (!note) { + res.status(400).send({ error: "Could not save thought. Message missing" }); + return; + } + + if (note.length < 5) { + res + .status(400) + .send({ error: "Text is shorter than minimum allowed lenght of 5" }); + return; + } + + const thought = new Thought({ + message: note, + userName: req.user.name + }); + await thought.save(); + res.status(201).send(thought); +}); + +//liking a thought with a given ID +app.post("/thoughts/:thoughtId/like", async (req, res) => { + const id = req.params.thoughtId; + try { + const likedThought = await Thought.findOneAndUpdate( + { _id: id }, + { $inc: { hearts: 1 } }, + { new: true } + ); + + res.status(201).json(likedThought); + } catch (error) { + res.status(404).json({ + error: "Thought not found", + }); + } +}); + +// Starting the server app.listen(port, () => { - console.log(`Server running on http://localhost:${port}`) -}) + console.log(`Server running on http://localhost:${port}`); +});