diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..d1f4512 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,22 @@ +{ + "workbench.colorCustomizations": { + "activityBar.activeBackground": "#2f7c47", + "activityBar.background": "#2f7c47", + "activityBar.foreground": "#e7e7e7", + "activityBar.inactiveForeground": "#e7e7e799", + "activityBarBadge.background": "#422c74", + "activityBarBadge.foreground": "#e7e7e7", + "commandCenter.border": "#e7e7e799", + "sash.hoverBorder": "#2f7c47", + "statusBar.background": "#215732", + "statusBar.foreground": "#e7e7e7", + "statusBarItem.hoverBackground": "#2f7c47", + "statusBarItem.remoteBackground": "#215732", + "statusBarItem.remoteForeground": "#e7e7e7", + "titleBar.activeBackground": "#215732", + "titleBar.activeForeground": "#e7e7e7", + "titleBar.inactiveBackground": "#21573299", + "titleBar.inactiveForeground": "#e7e7e799" + }, + "peacock.color": "#215732" +} diff --git a/README.md b/README.md index 0f9f073..59fa868 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,7 @@ -# Project API - -This project includes the packages and babel setup for an express server, and is just meant to make things a little simpler to get up and running with. +Backend: https://get-thoughts-out-api.onrender.com -## Getting started +Full demo on Happy Thoughts: https://happy-thoughts-byjd.netlify.app/ -Install dependencies with `npm install`, then start the server by running `npm run dev` - -## View it live +# Project API -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. +This project includes the packages and babel setup for an express server, and is just meant to make things a little simpler to get up and running with. diff --git a/data.json b/data.json index a2c844f..5d075d2 100644 --- a/data.json +++ b/data.json @@ -1,13 +1,13 @@ [ - { - "_id": "682bab8c12155b00101732ce", + { + "_id": "682bab8c12155b00101732cb", "message": "Berlin baby", "hearts": 37, "createdAt": "2025-05-19T22:07:08.999Z", "__v": 0 }, { - "_id": "682e53cc4fddf50010bbe739", + "_id": "682e53cc4fddf50010bbe739", "message": "My family!", "hearts": 0, "createdAt": "2025-05-22T22:29:32.232Z", @@ -17,7 +17,7 @@ "_id": "682e4f844fddf50010bbe738", "message": "The smell of coffee in the morning....", "hearts": 23, - "createdAt": "2025-05-22T22:11:16.075Z", + "createdAt": "2024-05-22T22:11:16.075Z", "__v": 0 }, { @@ -25,7 +25,7 @@ "message": "Newly washed bedlinen, kids that sleeps through the night.. FINGERS CROSSED 🤞🏼\n", "hearts": 6, "createdAt": "2025-05-21T21:42:23.862Z", - "__v": 0 + "__v": 0 }, { "_id": "682e45804fddf50010bbe736", @@ -53,7 +53,7 @@ "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 + "__v": 0 }, { "_id": "682cebbe17487d0010a298b5", @@ -74,7 +74,7 @@ "message": "Summer is coming...", "hearts": 2, "createdAt": "2025-05-20T15:03:22.379Z", - "__v": 0 + "__v": 0 }, { "_id": "682c706c951f7a0017130024", @@ -113,9 +113,9 @@ }, { "_id": "682bab8c12155b00101732ce", - "message": "Berlin baby", + "message": "London 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..a0e42e8 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "description": "Project API", "scripts": { "start": "babel-node server.js", - "dev": "nodemon server.js --exec babel-node" + "dev": "nodemon server.js --exec babel-node", + "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", @@ -12,8 +13,16 @@ "@babel/core": "^7.17.9", "@babel/node": "^7.16.8", "@babel/preset-env": "^7.16.11", + "bcrypt": "^6.0.0", + "bcrypt-nodejs": "^0.0.3", "cors": "^2.8.5", "express": "^4.17.3", + "express-list-endpoints": "^7.1.1", + "mongodb": "^7.0.0", + "mongoose": "^9.1.5", "nodemon": "^3.0.1" + }, + "devDependencies": { + "@types/bcrypt-nodejs": "^0.0.31" } } diff --git a/server.js b/server.js index f47771b..5abe2a3 100644 --- a/server.js +++ b/server.js @@ -1,22 +1,386 @@ -import cors from "cors" -import express from "express" +import cors from "cors"; +import express from "express"; +import data from "./data.json"; +import listEndpoints from "express-list-endpoints"; +import mongoose, { Model, set } from "mongoose"; +import crypto from "crypto"; +import bcrypt from "bcrypt-nodejs"; -// 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() +/* console.log("Tweets here: ", data.length) */ + +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()); + +//Connect database: +const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/thoughts"; +mongoose.connect(mongoUrl); +mongoose.Promise = Promise; + +// ---- Models ---- + +//Full thoughts' information +const Thought = mongoose.model("Thought", { + message: { + type: String, + required: true, + minLength: 5, + maxLength: 140, + }, + hearts: { + type: Number, + default: 0, + }, + createdAt: { + type: Date, + default: () => new Date(), + }, +}); + +const User = mongoose.model("User", { + name: { + type: String, + unique: true, + required: true, + minLength: 4, + maxLength: 32, + }, + + email: { + type: String, + required: true, + unique: true, + }, + + password: { + type: String, + required: true, + }, + + accessToken: { + type: String, + default: () => crypto.randomBytes(128).toString("hex"), + }, + + registerDate: { + type: Date, + default: () => new Date(), + }, +}); +// ---- / Models ---- -// Start defining your routes here +// ---- Authentication function: only authorised users can add or like thoughts + +const authenticateUser = async (req, res, next) => { + const user = await User.findOne; + const token = req.header("Authorization"); + if (!token) { + return res.status(401).json({ message: "User is logged out" }); + } + + if (user) { + req.user = user; + next(); + } else { + //user is matched and not authorized to do smth + res.status(401).json({ loggedOut: true }); + } +}; +// ---- // Authentication function + +// ---- List all endpoints ---- app.get("/", (req, res) => { - res.send("Hello Technigo!") -}) + const endpoints = listEndpoints(app); + + res.json({ endpoints: endpoints }); +}); + +// ---- ALL USER AUTHORISATION ROUTES + +// ----Creating new user, route: /register---- + +app.post("/register", async (req, res) => { + try { + const { name, email, password } = req.body; + + if (!name || !email || !password) { + return res.status(400).json({ + message: "All fields are required to register a user", + }); + } + + if (password.length < 8) { + return res + .status(400) + .json({ message: "Password must be at least 8 characters long" }); + } + + const salt = bcrypt.genSaltSync(); + const hashedPass = bcrypt.hashSync(password, salt); + + const user = new User({ + name, + email, + password: hashedPass, + }); + + await user.save(); + + res.status(201).json({ + id: user._id, + accessToken: user.accessToken, + }); //encrypting passwords + } catch (err) { + //User with this name already exists + if (err.code === 11000) { + return res + .status(400) + .json({ message: "User with this name or email alredy exists" }); + } + //Bad request + res + .status(400) + .json({ message: "Couldn't create a user", error: err.errors }); + } +}); + +// ---- Access with existing user, route: /login ---- +app.post("/login", async (req, res) => { + try { + const { email, password } = req.body; + + if (!email || !password) { + return res.status(400).json({ + message: "All fields are required to login", + }); + } + + const user = await User.findOne({ email }); //retrieving username from database, lookig for match by email that should be unique (1 email=1 user) + + if (!user || !bcrypt.compareSync(password, user.password)) { + return res.status(401).json({ message: "Invalid email or password" }); + } + res.json({ userId: user._id, accessToken: user.accessToken }); + } catch (err) { + //Bad request: + // 1. Email is incorrect + // 2. password doesn't match + res.status(500).json({ + message: "Something went wrong, please try agan", + error: err.errors, + }); + } +}); + +// ---- Endpoints POST ---- +// ---- Post a thought: +app.post("/thoughts", authenticateUser, async (req, res) => { + //This will only work if next() function is called from the authenticateUser + const { message } = req.body; + + try { + const thought = await new Thought({ message }).save(); + res.status(201).json(thought); + } catch (err) { + //Bad request + res.status(400).json({ + message: "Couldn't save a thought to the database", + error: err.errors, //check the errors + }); + } +}); + +// ---- Like a thought +app.post("/thoughts/:id/like", async (req, res) => { + const { id } = req.params; + + //Storing operator in variable and options in variables + const update = { $inc: { hearts: 1 } }; + const options = { new: true, runValidators: true }; + + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(404).json({ error: `Oops, this id ${id} is invalid` }); + } + + try { + const addLike = await Thought.findByIdAndUpdate(id, update, options); + + if (!addLike) { + return res.status(404).json({ + error: `Oops, can't like thought with id ${id} because it doesn't exist or was deleted`, + }); + } + + res.status(200).json(addLike); + } catch (err) { + res.status(500).json({ + message: "Couldn't save like to the database", + error: err.errors, + }); + } +}); + +// ---- / Endpoints POST ---- + +// ---- Endpoints GET ---- +// ---- All messages, and filter: query param: filter by hearts N url ex.: http://localhost:8080/thoughts?hearts=23 +app.get("/thoughts", async (req, res) => { + /* console.log("this is the one with params"); */ + const { hearts } = req.query; + + const heartsNumber = Number(hearts); + + const query = Thought.find().sort({ createdAt: "desc" }); //building query for filtering by amount of hearts + try { + if (hearts) { + //Filter by amount of hearts + query.find({ + hearts: { $eq: heartsNumber }, + }); + } + + const allThoughts = await query.exec(); + + if (!allThoughts.length) { + return res + .status(404) + .json({ error: `Message with ${heartsNumber} hearts doesn't exist` }); + } + + res.json(allThoughts); + } catch (err) { + res.status(400).json({ + message: "On no, something went wrong, couldn't retrieve any thoughts", + }); + } +}); + +// ---- Find one thought by id +app.get("/thoughts/:id", async (req, res) => { + const { id } = req.params; + + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ error: `Oops, this id ${id} is invalid` }); + } + + try { + const singleThought = await Thought.findById(id).exec(); + + if (!singleThought) { + return res.status(404).json({ + error: `Oh no, seem like a thougth with ${id} doesn't exist or was deleted!`, + }); + } + + res.status(201).json(singleThought); + } catch (err) { + res.status(404).json({ + message: `Oops, thought with ${id} doesn't seem to exist yet`, + error: err.errors, + }); + } +}); + +// --- All messages with filter: query param, that have N or more hearts, url ex.: http://localhost:8080/thoughts/hearts?hearts=N&sort=desc + +// ---- / Endpoints GET ---- + +// ---- Endpoints PUT ---- + +//Edit a thought +app.put("/thoughts/:id", authenticateUser, async (req, res) => { + const { id } = req.params; + const editedThought = req.body.message; + + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ error: `Oops, this id ${id} is invalid` }); + } + + if (!editedThought) { + return res.status(400).json({ + error: "Message is required to update a thought", + }); + } + + const foundThought = await Thought.findById(id); + try { + if (!foundThought) { + return res.status(404).json({ + error: `Oops, thought with ${id} doesn't seem to exist yet`, + }); + } + foundThought.message = editedThought; + await foundThought.save(); + res.status(200).json({ + message: "Message was successfully updated", + thought: foundThought, + }); + } catch (err) { + res.status(500).json({ + message: "Something went wrong at the server", + error: err.errors, + }); + } +}); + +// ---- / Endpoints PUT ---- + +// ---- Endpoints DELETE ---- +// Delete a thought +app.delete("/thoughts/:id", authenticateUser, async (req, res) => { + const { id } = req.params; + + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ error: "Oops, looks like id is invalid" }); + } + try { + const foundThought = await Thought.findByIdAndDelete(id).exec(); + + if (!foundThought) { + return res.status(404).json({ + error: `Oh no, you can't delete a thougth with ${id} because it doesn't exist!`, + }); + } + + res.status(200).json({ message: "Thought was deleted" }); + } catch (err) { + res.status(500).json({ + message: "Something went wrong at the server", + error: err.errors, + }); + } +}); + +// ---- / Endpoints DELETE ---- + +// TODO ---- LATER ----- + +// ---- Sorting by date ---- + +//Endpoint to sort all messages by date from old to new, url ex.: OR with /sort-by/?date=old:-new +app.get("/sort-oldest/", async (req, res) => { + const sortedByDate = await new Thought.sort( + // TODO still data + (a, b) => new Date(a.createdAt) - new Date(b.createdAt), + ); + + res.json(sortedByDate); +}); + +//Endpoint to sort all messages by date new to old, url ex.: /messages/sort-by/?date=old:-new +app.get("/thoughts/sort-newest/", (req, res) => { + const sortedByDate = data.sort( + // TODO still data + (a, b) => new Date(b.createdAt) - new Date(a.createdAt), + ); + + res.json(sortedByDate); +}); // Start the server app.listen(port, () => { - console.log(`Server running on http://localhost:${port}`) -}) + console.log(`Server running on http://localhost:${port}`); +});