From 879db5f123bff67bb0f0c000dba634c9d39c7f6e Mon Sep 17 00:00:00 2001 From: Jonny Hicks Date: Mon, 26 May 2025 18:10:44 +0200 Subject: [PATCH 01/24] Update package.json with project details and structure --- package.json | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index bf25bb6..5cdc3c7 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,23 @@ { - "name": "project-api", + "name": "js-project-api", "version": "1.0.0", - "description": "Project API", + "description": " JS Project API", + "homepage": "https://github.com/KidFromCalifornia/js-project-api#readme", + "bugs": { + "url": "https://github.com/KidFromCalifornia/js-project-api/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/KidFromCalifornia/js-project-api.git" + }, + "license": "ISC", + "author": "", + "type": "commonjs", + "main": "server.js", "scripts": { "start": "babel-node server.js", "dev": "nodemon server.js --exec babel-node" }, - "author": "", - "license": "ISC", "dependencies": { "@babel/core": "^7.17.9", "@babel/node": "^7.16.8", @@ -15,5 +25,6 @@ "cors": "^2.8.5", "express": "^4.17.3", "nodemon": "^3.0.1" - } + }, + "devDependencies": {} } From dcbf71bfea065ad9e6c9526b136b7363ef89c29b Mon Sep 17 00:00:00 2001 From: Jonny Hicks Date: Wed, 28 May 2025 15:59:46 +0200 Subject: [PATCH 02/24] .get added and workiing with data.json --- data.json | 14 +++++++------- server.js | 37 +++++++++++++++++++++++++++---------- 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/data.json b/data.json index a2c844f..44f324e 100644 --- a/data.json +++ b/data.json @@ -1,5 +1,5 @@ [ - { + { "_id": "682bab8c12155b00101732ce", "message": "Berlin baby", "hearts": 37, @@ -7,7 +7,7 @@ "__v": 0 }, { - "_id": "682e53cc4fddf50010bbe739", + "_id": "682e53cc4fddf50010bbe739", "message": "My family!", "hearts": 0, "createdAt": "2025-05-22T22:29:32.232Z", @@ -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", @@ -100,7 +100,7 @@ { "_id": "682c6e65951f7a0017130021", "message": "The weather is nice!", - "hearts": 0, + "hearts": 2, "createdAt": "2025-05-20T11:58:29.662Z", "__v": 0 }, @@ -118,4 +118,4 @@ "createdAt": "2025-05-19T22:07:08.999Z", "__v": 0 } -] \ No newline at end of file +] diff --git a/server.js b/server.js index f47771b..c027e7e 100644 --- a/server.js +++ b/server.js @@ -1,22 +1,39 @@ -import cors from "cors" -import express from "express" +import cors from "cors"; +import express from "express"; +import thoughts from "./data.json"; // 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 || 9000; +0; +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()); // Start defining your routes here app.get("/", (req, res) => { - res.send("Hello Technigo!") -}) + res.send("I did it!"); +}); + +app.get("/thoughts", (req, res) => { + res.json(thoughts); +}); + +app.get("/thoughts/:id", (req, res) => { + const { id } = req.params; + const thought = thoughts.find((t) => t._id === id); + if (thought) { + res.json(thought); + } else { + res.status(404).json({ error: "Thought not found" }); + } +}); // Start the server app.listen(port, () => { - console.log(`Server running on http://localhost:${port}`) -}) + console.log(`Server running on http://localhost:${port}`); +}); From 87e38767c74ca43e91eab6536c21b45b5fff141c Mon Sep 17 00:00:00 2001 From: Jonny Hicks Date: Mon, 2 Jun 2025 20:17:29 +0200 Subject: [PATCH 03/24] Add new endpoints for filtering, searching, pagination, and posting thoughts --- data.json | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ server.js | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+) diff --git a/data.json b/data.json index 44f324e..20e18df 100644 --- a/data.json +++ b/data.json @@ -117,5 +117,75 @@ "hearts": 37, "createdAt": "2025-05-19T22:07:08.999Z", "__v": 0 + }, + { + "_id": "650b32c9e4bce17db221d8a7", + "message": "Long bike rides with no destination πŸš΄β€β™€οΈ", + "hearts": 19, + "createdAt": "2025-03-03T10:43:19.553Z", + "__v": 0 + }, + { + "_id": "64fcde98be72c80afc8d33ee", + "message": "When everything just clicks at work πŸ’‘", + "hearts": 25, + "createdAt": "2025-01-17T14:11:07.734Z", + "__v": 0 + }, + { + "_id": "63d1ecfc22cb0f173a6a8894", + "message": "A surprise sunny day in February β˜€οΈ", + "hearts": 30, + "createdAt": "2025-02-20T12:50:45.286Z", + "__v": 0 + }, + { + "_id": "64bb9b72a7a6b87ffce30d44", + "message": "Waking up before the alarm feeling refreshed 😴", + "hearts": 13, + "createdAt": "2025-04-10T06:39:58.914Z", + "__v": 0 + }, + { + "_id": "649a1dc0cced95dfad9a7e33", + "message": "Watching birds from the balcony 🐦", + "hearts": 6, + "createdAt": "2025-03-28T08:23:30.201Z", + "__v": 0 + }, + { + "_id": "652ee0db90de8445dfc29a2e", + "message": "Late-night deep talks with a friend πŸŒ™", + "hearts": 31, + "createdAt": "2025-01-04T23:57:12.483Z", + "__v": 0 + }, + { + "_id": "63cf410d27b5f2cf6a97c028", + "message": "A clean desk and a clear mind 🧼🧠", + "hearts": 10, + "createdAt": "2025-05-06T11:45:01.006Z", + "__v": 0 + }, + { + "_id": "64aaf9ee8d8e2953df0cd987", + "message": "Hearing your favorite song on the radio πŸ“»", + "hearts": 20, + "createdAt": "2025-04-25T15:20:38.527Z", + "__v": 0 + }, + { + "_id": "64b4b702df25e8e93f8a3e49", + "message": "The smell of fresh rain 🌧️", + "hearts": 12, + "createdAt": "2025-03-10T18:11:26.749Z", + "__v": 0 + }, + { + "_id": "6504d2b53a7bc742b7fbe114", + "message": "Seeing someone smile because of you 😊", + "hearts": 28, + "createdAt": "2025-02-12T20:02:14.318Z", + "__v": 0 } ] diff --git a/server.js b/server.js index c027e7e..9621bf3 100644 --- a/server.js +++ b/server.js @@ -33,6 +33,70 @@ app.get("/thoughts/:id", (req, res) => { } }); +app.get("/thoughts/hearts/:min", (req, res) => { + const min = Number(req.params.min); + if (isNaN(min)) { + return res + .status(400) + .json({ error: "Invalid 'min' parameter. Must be a number." }); + } + const filteredThoughts = thoughts.filter( + (t) => typeof t.hearts === "number" && t.hearts >= min + ); + res.json(filteredThoughts); +}); + +app.get("/thoughts/search/:word", (req, res) => { + const word = req.params.word.toLowerCase(); + const filtered = thoughts.filter( + (t) => + typeof t.message === "string" && t.message.toLowerCase().includes(word) + ); + res.json(filtered); +}); + +app.get("/thoughts/page/:page", (req, res) => { + const page = Number(req.params.page); + if (!Number.isInteger(page) || page <= 0) { + return res.status(400).json({ error: "Page must be a positive integer." }); + } + const pageSize = 5; // Number of thoughts per page + const start = (page - 1) * pageSize; + const end = start + pageSize; + const pagedThoughts = thoughts.slice(start, end); + res.json(pagedThoughts); +}); + +app.post("/thoughts", (req, res) => { + const { message, hearts } = req.body; + if (!message) { + return res.status(400).json({ error: "Message is required" }); + } + if (hearts !== undefined && (typeof hearts !== "number" || isNaN(hearts))) { + return res.status(400).json({ error: "Hearts must be a valid number" }); + } + const newThought = { + _id: Date.now().toString(), + message, + hearts: hearts || 0, + createdAt: new Date().toISOString(), + __v: 0, + }; + thoughts.push(newThought); + res.status(201).json(newThought); +}); + +app.delete("/thoughts/:id", (req, res) => { + const { id } = req.params; + const index = thoughts.findIndex((t) => t._id === id); + if (index !== -1) { + const deleted = thoughts.splice(index, 1); + res.json({ success: true, deleted: deleted[0] }); + } else { + res.status(404).json({ error: "Thought not found" }); + } +}); + // Start the server app.listen(port, () => { console.log(`Server running on http://localhost:${port}`); From a772d40078a7b3cec16ed6fb0bb3c80d2f1454f0 Mon Sep 17 00:00:00 2001 From: Jonny Hicks Date: Tue, 3 Jun 2025 15:53:04 +0200 Subject: [PATCH 04/24] Refactor server to use mongoose for database operations and update thoughts endpoints for async handling --- package.json | 4 +- server.js | 122 ++++++++++++++++++++++++++++++++++----------------- 2 files changed, 84 insertions(+), 42 deletions(-) diff --git a/package.json b/package.json index 5cdc3c7..436aa66 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "@babel/preset-env": "^7.16.11", "cors": "^2.8.5", "express": "^4.17.3", + "mongoose": "^8.15.1", "nodemon": "^3.0.1" - }, - "devDependencies": {} + } } diff --git a/server.js b/server.js index 9621bf3..298181d 100644 --- a/server.js +++ b/server.js @@ -1,6 +1,11 @@ import cors from "cors"; import express from "express"; -import thoughts from "./data.json"; + +import mongoose from "mongoose"; + +const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/thoughts"; +mongoose.connect(mongoUrl, { useNewUrlParser: true, useUnifiedTopology: true }); +mongoose.Promise = Promise; // 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: @@ -19,17 +24,48 @@ app.get("/", (req, res) => { res.send("I did it!"); }); -app.get("/thoughts", (req, res) => { - res.json(thoughts); +const Thought = mongoose.model("Thought", { + message: { + type: String, + required: true, + minlength: 5, + maxlength: 140, + }, + hearts: { + type: Number, + default: 0, + }, + createdAt: { + type: Date, + default: Date.now, + }, }); -app.get("/thoughts/:id", (req, res) => { - const { id } = req.params; - const thought = thoughts.find((t) => t._id === id); - if (thought) { - res.json(thought); - } else { - res.status(404).json({ error: "Thought not found" }); +new Thought({ + message: "This is a test thought", + hearts: 0, +}).save(); + +app.get("/thoughts", async (req, res) => { + try { + const thoughts = await Thought.find().sort({ createdAt: -1 }).limit(20); + res.json(thoughts); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Internal server error" }); + } +}); + +app.get("/thoughts/:id", async (req, res) => { + try { + const thought = await Thought.findById(req.params.id); + if (thought) { + res.json(thought); + } else { + res.status(404).json({ error: "Thought not found" }); + } + } catch (err) { + res.status(400).json({ error: "Invalid ID" }); } }); @@ -55,45 +91,51 @@ app.get("/thoughts/search/:word", (req, res) => { res.json(filtered); }); -app.get("/thoughts/page/:page", (req, res) => { +app.get("/thoughts/page/:page", async (req, res) => { const page = Number(req.params.page); + const pageSize = 5; // Number of thoughts per page + if (!Number.isInteger(page) || page <= 0) { - return res.status(400).json({ error: "Page must be a positive integer." }); + return res.status(400).json({ error: "Page must 1 or more " }); + } + + try { + const thoughts = await Thought.find() + .sort({ createdAt: -1 }) + .skip((page - 1) * pageSize) + .limit(pageSize); + + res.json(thoughts); + } catch (err) { + res.status(500).json({ error: "Internal server error" }); } - const pageSize = 5; // Number of thoughts per page - const start = (page - 1) * pageSize; - const end = start + pageSize; - const pagedThoughts = thoughts.slice(start, end); - res.json(pagedThoughts); }); -app.post("/thoughts", (req, res) => { +app.post("/thoughts", async (req, res) => { const { message, hearts } = req.body; - if (!message) { - return res.status(400).json({ error: "Message is required" }); - } - if (hearts !== undefined && (typeof hearts !== "number" || isNaN(hearts))) { - return res.status(400).json({ error: "Hearts must be a valid number" }); + try { + const newThought = await Thought.create({ + message, + hearts: hearts || 0, + }); + res.status(201).json(newThought); + } catch (err) { + res + .status(400) + .json({ error: "Could not create thought", details: err.message }); } - const newThought = { - _id: Date.now().toString(), - message, - hearts: hearts || 0, - createdAt: new Date().toISOString(), - __v: 0, - }; - thoughts.push(newThought); - res.status(201).json(newThought); }); -app.delete("/thoughts/:id", (req, res) => { - const { id } = req.params; - const index = thoughts.findIndex((t) => t._id === id); - if (index !== -1) { - const deleted = thoughts.splice(index, 1); - res.json({ success: true, deleted: deleted[0] }); - } else { - res.status(404).json({ error: "Thought not found" }); +app.delete("/thoughts/:id", async (req, res) => { + try { + const deleted = await Thought.findByIdAndDelete(req.params.id); + if (deleted) { + res.json({ success: true, deleted }); + } else { + res.status(404).json({ error: "Thought not found" }); + } + } catch (err) { + res.status(400).json({ error: "Invalid ID" }); } }); From bc18d2a0ff84533c88bad9e3472a631789b82bc0 Mon Sep 17 00:00:00 2001 From: Jonny Hicks Date: Tue, 3 Jun 2025 16:09:24 +0200 Subject: [PATCH 05/24] d add like functionality to increment hearts --- server.js | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/server.js b/server.js index 298181d..75017be 100644 --- a/server.js +++ b/server.js @@ -41,10 +41,20 @@ const Thought = mongoose.model("Thought", { }, }); -new Thought({ - message: "This is a test thought", - hearts: 0, -}).save(); +Thought.deleteMany().then(() => { + new Thought({ + message: "This is a test one", + hearts: 0, + }).save(); + new Thought({ + message: "This is a test two", + hearts: 7, + }).save(); + new Thought({ + message: "This is a test 3", + hearts: 5, + }).save(); +}); // Clear the collection before seeding app.get("/thoughts", async (req, res) => { try { @@ -125,6 +135,22 @@ app.post("/thoughts", async (req, res) => { .json({ error: "Could not create thought", details: err.message }); } }); +app.post("/thoughts/:id/like", async (req, res) => { + try { + const addHearts = await Thought.findByIdAndUpdate( + req.params.id, + { $inc: { hearts: 1 } }, // Increment hearts by 1 + { new: true } + ); + if (addHearts) { + res.json(addHearts); + } else { + res.status(404).json({ error: "Thought not found" }); + } + } catch (err) { + res.status(400).json({ error: "Invalid ID" }); + } +}); app.delete("/thoughts/:id", async (req, res) => { try { From 7f552ec7d20b884f0bb376626960616bb9b5854f Mon Sep 17 00:00:00 2001 From: Jonny Hicks Date: Wed, 4 Jun 2025 12:07:24 +0200 Subject: [PATCH 06/24] Refactor server routes to return thoughts and improve error handling; update Thought model definition and seed logic --- package.json | 3 ++- server.js | 45 +++++++++++++++++++++++++++++---------------- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 436aa66..8a583b0 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "main": "server.js", "scripts": { "start": "babel-node server.js", - "dev": "nodemon server.js --exec babel-node" + "dev": "nodemon server.js --exec babel-node", + "build": "babel server.js -d dist" }, "dependencies": { "@babel/core": "^7.17.9", diff --git a/server.js b/server.js index 75017be..11d2b8d 100644 --- a/server.js +++ b/server.js @@ -21,12 +21,21 @@ app.use(express.json()); // Start defining your routes here app.get("/", (req, res) => { - res.send("I did it!"); + Thought.find() + .then((thoughts) => { + res.json(thoughts); + }) + .catch((err) => { + console.error(err); + res.status(500).json({ error: "Internal server error" }); + }); }); const Thought = mongoose.model("Thought", { message: { - type: String, + type: mongoose.Schema.Types.String, + trim: true, + ref: "Thought", required: true, minlength: 5, maxlength: 140, @@ -41,20 +50,24 @@ const Thought = mongoose.model("Thought", { }, }); -Thought.deleteMany().then(() => { - new Thought({ - message: "This is a test one", - hearts: 0, - }).save(); - new Thought({ - message: "This is a test two", - hearts: 7, - }).save(); - new Thought({ - message: "This is a test 3", - hearts: 5, - }).save(); -}); // Clear the collection before seeding +if (process.env.RESET_DATABASE) { + console.log("Resetting database"); + + const seedThoughts = async () => { + new Thought({ + message: "This is a test one", + hearts: 0, + }).save(); + new Thought({ + message: "This is a test two", + hearts: 7, + }).save(); + new Thought({ + message: "This is a test 3", + hearts: 5, + }).save(); + }; +} app.get("/thoughts", async (req, res) => { try { From 16080438595a0415fc0db72ea2d7f86470cfb4ae Mon Sep 17 00:00:00 2001 From: Jonny Hicks Date: Wed, 4 Jun 2025 12:18:33 +0200 Subject: [PATCH 07/24] Add @babel/cli dependency to package.json --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 8a583b0..a6a3da2 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "build": "babel server.js -d dist" }, "dependencies": { + "@babel/cli": "^7.27.2", "@babel/core": "^7.17.9", "@babel/node": "^7.16.8", "@babel/preset-env": "^7.16.11", From 725bcec39c14450cabc8f0a5db68790ac1e5db19 Mon Sep 17 00:00:00 2001 From: Jonny Hicks Date: Wed, 4 Jun 2025 12:25:55 +0200 Subject: [PATCH 08/24] Fix start script in package.json to use npx for babel-node --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a6a3da2..af2494c 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "type": "commonjs", "main": "server.js", "scripts": { - "start": "babel-node server.js", + "start": " npx babel-node server.js", "dev": "nodemon server.js --exec babel-node", "build": "babel server.js -d dist" }, From 07afc4226c358ec97f0a3112fa6025f530e8b837 Mon Sep 17 00:00:00 2001 From: Jonny Hicks Date: Wed, 4 Jun 2025 12:48:20 +0200 Subject: [PATCH 09/24] Fix start script in package.json to remove npx for babel-node --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index af2494c..8b62174 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "type": "commonjs", "main": "server.js", "scripts": { - "start": " npx babel-node server.js", + "start": " babel-node server.js", "dev": "nodemon server.js --exec babel-node", "build": "babel server.js -d dist" }, From 74a52affb34a59efd69e3a9408073dcc1e4f1516 Mon Sep 17 00:00:00 2001 From: Jonny Hicks Date: Wed, 4 Jun 2025 12:54:23 +0200 Subject: [PATCH 10/24] Remove unused dependencies and clean up package.json scripts --- package.json | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 8b62174..5cdc3c7 100644 --- a/package.json +++ b/package.json @@ -15,18 +15,16 @@ "type": "commonjs", "main": "server.js", "scripts": { - "start": " babel-node server.js", - "dev": "nodemon server.js --exec babel-node", - "build": "babel server.js -d dist" + "start": "babel-node server.js", + "dev": "nodemon server.js --exec babel-node" }, "dependencies": { - "@babel/cli": "^7.27.2", "@babel/core": "^7.17.9", "@babel/node": "^7.16.8", "@babel/preset-env": "^7.16.11", "cors": "^2.8.5", "express": "^4.17.3", - "mongoose": "^8.15.1", "nodemon": "^3.0.1" - } + }, + "devDependencies": {} } From 63c74e41fd2ea9bd1878393080e4e816c8fe4f01 Mon Sep 17 00:00:00 2001 From: Jonny Hicks Date: Wed, 4 Jun 2025 16:59:59 +0200 Subject: [PATCH 11/24] Remove empty devDependencies section from package.json --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 5cdc3c7..436aa66 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "@babel/preset-env": "^7.16.11", "cors": "^2.8.5", "express": "^4.17.3", + "mongoose": "^8.15.1", "nodemon": "^3.0.1" - }, - "devDependencies": {} + } } From 9088efac3618194e0ff141624c09f14af769032a Mon Sep 17 00:00:00 2001 From: Jonny Hicks Date: Thu, 12 Jun 2025 10:56:25 +0200 Subject: [PATCH 12/24] Implement user authentication and enhance thoughts management features --- server.js | 133 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 95 insertions(+), 38 deletions(-) diff --git a/server.js b/server.js index 11d2b8d..73a2956 100644 --- a/server.js +++ b/server.js @@ -1,26 +1,63 @@ import cors from "cors"; import express from "express"; - import mongoose from "mongoose"; +import crypto from "crypto"; +import bcrypt from "bcrypt-nodejs"; const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/thoughts"; -mongoose.connect(mongoUrl, { useNewUrlParser: true, useUnifiedTopology: true }); +mongoose.connect(mongoUrl); mongoose.Promise = Promise; -// 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 || 9000; -0; +const port = process.env.PORT || 9001; const app = express(); +const authenticationUser = async (req, res, next) => { + const username = await users.findOne({ + accessToken: req.headers("accesstoken"), + }); + if (username) { + req.username = username; + next(); + } else { + return res.status(401).json({ loggedout: true, error: "Unauthorized" }); + } +}; + +const users = mongoose.model("username", { + username: { + type: String, + required: true, + unique: true, + }, + password: { + type: String, + required: true, + }, + acessToken: { + type: String, + default: () => crypto.randomBytes(128).toString("hex"), + }, +}); + +app.post("/login", async (req, res) => { + const user = await users.findOne({ + username: req.body.username, + }); + if (user && bcrypt.compareSync(req.body.password, user.password)) { + // If user exists and password matches + res.json({ userid: user._id, acessToken: user.acessToken }); + } else { + // If user does not exist or password does not match + res.json({ error: "Invalid username or password" }); + } +}); + // Add middlewares to enable cors and json body parsing app.use(cors()); app.use(express.json()); // Start defining your routes here -app.get("/", (req, res) => { +app.get("/", (_, res) => { Thought.find() .then((thoughts) => { res.json(thoughts); @@ -30,12 +67,10 @@ app.get("/", (req, res) => { res.status(500).json({ error: "Internal server error" }); }); }); - -const Thought = mongoose.model("Thought", { +const thoughtSchema = new mongoose.Schema({ message: { - type: mongoose.Schema.Types.String, + type: String, trim: true, - ref: "Thought", required: true, minlength: 5, maxlength: 140, @@ -50,26 +85,20 @@ const Thought = mongoose.model("Thought", { }, }); +const Thought = mongoose.model("Thought", thoughtSchema); + if (process.env.RESET_DATABASE) { console.log("Resetting database"); - const seedThoughts = async () => { - new Thought({ - message: "This is a test one", - hearts: 0, - }).save(); - new Thought({ - message: "This is a test two", - hearts: 7, - }).save(); - new Thought({ - message: "This is a test 3", - hearts: 5, - }).save(); + await Thought.deleteMany({}); + await new Thought({ message: "This is a test one", hearts: 0 }).save(); + await new Thought({ message: "This is a test two", hearts: 7 }).save(); + await new Thought({ message: "This is a test 3", hearts: 5 }).save(); }; + seedThoughts(); } -app.get("/thoughts", async (req, res) => { +app.get("/thoughts", async (_, res) => { try { const thoughts = await Thought.find().sort({ createdAt: -1 }).limit(20); res.json(thoughts); @@ -92,26 +121,31 @@ app.get("/thoughts/:id", async (req, res) => { } }); -app.get("/thoughts/hearts/:min", (req, res) => { +app.get("/thoughts/hearts/:min", async (req, res) => { const min = Number(req.params.min); if (isNaN(min)) { return res .status(400) .json({ error: "Invalid 'min' parameter. Must be a number." }); } - const filteredThoughts = thoughts.filter( - (t) => typeof t.hearts === "number" && t.hearts >= min - ); - res.json(filteredThoughts); + try { + const filteredThoughts = await Thought.find({ hearts: { $gte: min } }); + res.json(filteredThoughts); + } catch (err) { + res.status(500).json({ error: "Internal server error" }); + } }); -app.get("/thoughts/search/:word", (req, res) => { +app.get("/thoughts/search/:word", async (req, res) => { const word = req.params.word.toLowerCase(); - const filtered = thoughts.filter( - (t) => - typeof t.message === "string" && t.message.toLowerCase().includes(word) - ); - res.json(filtered); + try { + const filtered = await Thought.find({ + message: { $regex: word, $options: "i" }, + }); + res.json(filtered); + } catch (err) { + res.status(500).json({ error: "Internal server error" }); + } }); app.get("/thoughts/page/:page", async (req, res) => { @@ -134,6 +168,7 @@ app.get("/thoughts/page/:page", async (req, res) => { } }); +app.post("/thoughts", authenticationUser); app.post("/thoughts", async (req, res) => { const { message, hearts } = req.body; try { @@ -148,6 +183,8 @@ app.post("/thoughts", async (req, res) => { .json({ error: "Could not create thought", details: err.message }); } }); + +app.post("/thoughts/:id/like", authenticationUser); app.post("/thoughts/:id/like", async (req, res) => { try { const addHearts = await Thought.findByIdAndUpdate( @@ -165,6 +202,7 @@ app.post("/thoughts/:id/like", async (req, res) => { } }); +app.delete("/thoughts/:id", authenticationUser); app.delete("/thoughts/:id", async (req, res) => { try { const deleted = await Thought.findByIdAndDelete(req.params.id); @@ -177,6 +215,25 @@ app.delete("/thoughts/:id", async (req, res) => { res.status(400).json({ error: "Invalid ID" }); } }); +// PATCH endpoint to edit a thought +app.patch("/thoughts/:id", authenticationUser, async (req, res) => { + try { + const updatedThought = await Thought.findByIdAndUpdate( + req.params.id, + req.body, // Only updates the fields sent in the request + { new: true, runValidators: true } + ); + if (updatedThought) { + res.json(updatedThought); + } else { + res.status(404).json({ error: "Thought not found" }); + } + } catch (err) { + res + .status(400) + .json({ error: "Could not update thought", details: err.message }); + } +}); // Start the server app.listen(port, () => { From 3b58a191e5eb7fe2626a6e2a0b05ee8066d04d07 Mon Sep 17 00:00:00 2001 From: Jonny Hicks Date: Thu, 12 Jun 2025 10:56:37 +0200 Subject: [PATCH 13/24] Add user authentication and thoughts management routes, models, and middleware --- middlewares/authenticationUser.js | 17 +++ models/Thought.js | 23 +++ models/User.js | 27 ++++ package.json | 8 +- routes/thoughts.js | 163 +++++++++++++++++++++ routes/users.js | 55 +++++++ server.js | 235 ++---------------------------- 7 files changed, 302 insertions(+), 226 deletions(-) create mode 100644 middlewares/authenticationUser.js create mode 100644 models/Thought.js create mode 100644 models/User.js create mode 100644 routes/thoughts.js create mode 100644 routes/users.js diff --git a/middlewares/authenticationUser.js b/middlewares/authenticationUser.js new file mode 100644 index 0000000..57d1775 --- /dev/null +++ b/middlewares/authenticationUser.js @@ -0,0 +1,17 @@ +import User from "../models/User.js"; + +const authenticationUser = async (req, res, next) => { + const accessToken = req.headers["accesstoken"]; + if (!accessToken) { + return res.status(401).json({ error: "Access token required" }); + } + const user = await User.findOne({ accessToken }); + if (user) { + req.user = user; + next(); + } else { + return res.status(401).json({ loggedout: true, error: "Unauthorized" }); + } +}; + +export default authenticationUser; diff --git a/models/Thought.js b/models/Thought.js new file mode 100644 index 0000000..1ad0643 --- /dev/null +++ b/models/Thought.js @@ -0,0 +1,23 @@ +import mongoose from "mongoose"; + +const thoughtSchema = new mongoose.Schema({ + message: { + type: String, + trim: true, + required: true, + minlength: 5, + maxlength: 140, + }, + hearts: { + type: Number, + default: 0, + }, + createdAt: { + type: Date, + default: Date.now, + }, +}); + +const Thought = mongoose.model("Thought", thoughtSchema); + +export default Thought; diff --git a/models/User.js b/models/User.js new file mode 100644 index 0000000..81ed710 --- /dev/null +++ b/models/User.js @@ -0,0 +1,27 @@ +import mongoose from "mongoose"; +import crypto from "crypto"; + +const User = mongoose.model("User", { + username: { + type: String, + required: true, + unique: true, + index: true, // Add index for optimization + }, + email: { + type: String, + required: true, + unique: true, + index: true, // Add index for optimization + }, + password: { + type: String, + required: true, + }, + accessToken: { + type: String, + default: () => crypto.randomBytes(128).toString("hex"), + }, +}); + +export default User; diff --git a/package.json b/package.json index 436aa66..ddadf79 100644 --- a/package.json +++ b/package.json @@ -22,9 +22,13 @@ "@babel/core": "^7.17.9", "@babel/node": "^7.16.8", "@babel/preset-env": "^7.16.11", + "bcrypt-nodejs": "^0.0.3", + "bcryptjs": "^3.0.2", "cors": "^2.8.5", - "express": "^4.17.3", + "dotenv": "^16.5.0", + "express": "^4.21.2", "mongoose": "^8.15.1", - "nodemon": "^3.0.1" + "nodemon": "^3.0.1", + "validator": "^13.15.15" } } diff --git a/routes/thoughts.js b/routes/thoughts.js new file mode 100644 index 0000000..2a2e61e --- /dev/null +++ b/routes/thoughts.js @@ -0,0 +1,163 @@ +import express from "express"; +import Thought from "../models/Thought.js"; +import authenticationUser from "../middlewares/authenticationUser.js"; + +const router = express.Router(); + +router.get("/", (_, res) => { + Thought.find() + .then((thoughts) => { + res.json(thoughts); + }) + .catch((err) => { + console.error(err); + res.status(500).json({ error: "Internal server error" }); + }); +}); + +router.get("/thoughts", async (_, res) => { + try { + const thoughts = await Thought.find().sort({ createdAt: -1 }).limit(20); + res.json(thoughts); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Internal server error" }); + } +}); + +router.get("/thoughts/:id", async (req, res) => { + try { + const thought = await Thought.findById(req.params.id); + if (thought) { + res.json(thought); + } else { + res.status(404).json({ error: "Thought not found" }); + } + } catch (err) { + res.status(400).json({ error: "Invalid ID" }); + } +}); + +router.get("/thoughts/hearts/:min", async (req, res) => { + const min = Number(req.params.min); + if (isNaN(min)) { + return res + .status(400) + .json({ error: "Invalid 'min' parameter. Must be a number." }); + } + try { + const filteredThoughts = await Thought.find({ hearts: { $gte: min } }); + res.json(filteredThoughts); + } catch (err) { + res.status(500).json({ error: "Internal server error" }); + } +}); + +router.get("/thoughts/search/:word", async (req, res) => { + const word = req.params.word.toLowerCase(); + try { + const filtered = await Thought.find({ + message: { $regex: word, $options: "i" }, + }); + res.json(filtered); + } catch (err) { + console.error("Search error:", err); // Add this line + res.status(500).json({ error: "Internal server error" }); + } +}); + +router.get("/thoughts/page/:page", async (req, res) => { + const page = Number(req.params.page); + const pageSize = 5; // Number of thoughts per page + + if (!Number.isInteger(page) || page <= 0) { + return res.status(400).json({ error: "Page must 1 or more " }); + } + + try { + const thoughts = await Thought.find() + .sort({ createdAt: -1 }) + .skip((page - 1) * pageSize) + .limit(pageSize); + + res.json(thoughts); + } catch (err) { + res.status(500).json({ error: "Internal server error" }); + } +}); +router.post("/thoughts", authenticationUser, async (req, res) => { + const { message, hearts } = req.body; + try { + const newThought = await Thought.create({ + message, + hearts: hearts || 0, + }); + res.status(201).json(newThought); + } catch (err) { + res + .status(400) + .json({ error: "Could not create thought", details: err.message }); + } +}); + +router.post("/thoughts/:id/like", authenticationUser, async (req, res) => { + try { + const addHearts = await Thought.findByIdAndUpdate( + req.params.id, + { $inc: { hearts: 1 } }, // Increment hearts by 1 + { new: true } + ); + if (addHearts) { + res.json(addHearts); + } else { + res.status(404).json({ error: "Thought not found" }); + } + } catch (err) { + res.status(400).json({ error: "Invalid ID" }); + } +}); + +router.delete("/thoughts/:id", authenticationUser, async (req, res) => { + try { + const deleted = await Thought.findByIdAndDelete(req.params.id); + if (deleted) { + res.json({ success: true, deleted }); + } else { + res.status(404).json({ error: "Thought not found" }); + } + } catch (err) { + res.status(400).json({ error: "Invalid ID" }); + } +}); + +// PATCH endpoint to edit a thought +router.patch("/thoughts/:id", authenticationUser, async (req, res) => { + try { + const updatedThought = await Thought.findByIdAndUpdate( + req.params.id, + req.body, + { new: true, runValidators: true } + ); + if (updatedThought) { + res.json(updatedThought); + } else { + res.status(404).json({ error: "Thought not found" }); + } + } catch (err) { + res + .status(400) + .json({ error: "Could not update thought", details: err.message }); + } +}); + +if (process.env.RESET_DATABASE) { + console.log("Resetting database"); + const seedThoughts = async () => { + await Thought.deleteMany({}); + await new Thought({ message: "This is a test one", hearts: 0 }).save(); + await new Thought({ message: "This is a test two", hearts: 7 }).save(); + await new Thought({ message: "This is a test 3", hearts: 5 }).save(); + }; + seedThoughts(); +} +export default router; diff --git a/routes/users.js b/routes/users.js new file mode 100644 index 0000000..90bdaaa --- /dev/null +++ b/routes/users.js @@ -0,0 +1,55 @@ +import express from "express"; +import bcrypt from "bcryptjs"; +import validator from "validator"; +import User from "../models/User.js"; + +const router = express.Router(); + +router.post("/login", async (req, res) => { + const { username, password } = req.body; + const user = await User.findOne({ username }); + if (user && bcrypt.compareSync(password, user.password)) { + res.json({ userId: user._id, accessToken: user.accessToken }); + } else { + res.status(401).json({ error: "Invalid username or password" }); + } +}); + +router.post("/register", async (req, res) => { + const { username, email, password } = req.body; + + if (!username || !email || !password) { + return res.status(400).json({ error: "All fields are required" }); + } + if (!validator.isEmail(email)) { + return res.status(400).json({ error: "Invalid email format" }); + } + if (password.length < 6) { + return res + .status(400) + .json({ error: "Password must be at least 6 characters long" }); + } + const existingUser = await User.findOne({ + $or: [{ username }, { email }], + }); + if (existingUser) { + return res.status(400).json({ error: "Username or email already exists" }); + } + const salt = bcrypt.genSaltSync(); + const user = new User({ + username, + email, + password: bcrypt.hashSync(password, salt), + }); + + await user.save(); + + res.status(201).json({ + success: true, + message: "User created successfully", + userId: user._id, + accessToken: user.accessToken, + user, + }); +}); +export default router; diff --git a/server.js b/server.js index 73a2956..1ad6427 100644 --- a/server.js +++ b/server.js @@ -1,241 +1,28 @@ import cors from "cors"; import express from "express"; import mongoose from "mongoose"; -import crypto from "crypto"; -import bcrypt from "bcrypt-nodejs"; +import dotenv from "dotenv"; +import thoughtsRoutes from "./routes/thoughts.js"; +import usersRoutes from "./routes/users.js"; -const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/thoughts"; +dotenv.config(); + +const mongoUrl = process.env.MONGO_URL; mongoose.connect(mongoUrl); mongoose.Promise = Promise; -const port = process.env.PORT || 9001; +const port = process.env.PORT || 8088; const app = express(); -const authenticationUser = async (req, res, next) => { - const username = await users.findOne({ - accessToken: req.headers("accesstoken"), - }); - if (username) { - req.username = username; - next(); - } else { - return res.status(401).json({ loggedout: true, error: "Unauthorized" }); - } -}; - -const users = mongoose.model("username", { - username: { - type: String, - required: true, - unique: true, - }, - password: { - type: String, - required: true, - }, - acessToken: { - type: String, - default: () => crypto.randomBytes(128).toString("hex"), - }, -}); - -app.post("/login", async (req, res) => { - const user = await users.findOne({ - username: req.body.username, - }); - if (user && bcrypt.compareSync(req.body.password, user.password)) { - // If user exists and password matches - res.json({ userid: user._id, acessToken: user.acessToken }); - } else { - // If user does not exist or password does not match - res.json({ error: "Invalid username or password" }); - } -}); - -// Add middlewares to enable cors and json body parsing app.use(cors()); app.use(express.json()); -// Start defining your routes here -app.get("/", (_, res) => { - Thought.find() - .then((thoughts) => { - res.json(thoughts); - }) - .catch((err) => { - console.error(err); - res.status(500).json({ error: "Internal server error" }); - }); -}); -const thoughtSchema = new mongoose.Schema({ - message: { - type: String, - trim: true, - required: true, - minlength: 5, - maxlength: 140, - }, - hearts: { - type: Number, - default: 0, - }, - createdAt: { - type: Date, - default: Date.now, - }, -}); +// Use routes +app.use("/thoughts", thoughtsRoutes); +app.use("/", usersRoutes); -const Thought = mongoose.model("Thought", thoughtSchema); - -if (process.env.RESET_DATABASE) { - console.log("Resetting database"); - const seedThoughts = async () => { - await Thought.deleteMany({}); - await new Thought({ message: "This is a test one", hearts: 0 }).save(); - await new Thought({ message: "This is a test two", hearts: 7 }).save(); - await new Thought({ message: "This is a test 3", hearts: 5 }).save(); - }; - seedThoughts(); -} - -app.get("/thoughts", async (_, res) => { - try { - const thoughts = await Thought.find().sort({ createdAt: -1 }).limit(20); - res.json(thoughts); - } catch (err) { - console.error(err); - res.status(500).json({ error: "Internal server error" }); - } -}); - -app.get("/thoughts/:id", async (req, res) => { - try { - const thought = await Thought.findById(req.params.id); - if (thought) { - res.json(thought); - } else { - res.status(404).json({ error: "Thought not found" }); - } - } catch (err) { - res.status(400).json({ error: "Invalid ID" }); - } -}); - -app.get("/thoughts/hearts/:min", async (req, res) => { - const min = Number(req.params.min); - if (isNaN(min)) { - return res - .status(400) - .json({ error: "Invalid 'min' parameter. Must be a number." }); - } - try { - const filteredThoughts = await Thought.find({ hearts: { $gte: min } }); - res.json(filteredThoughts); - } catch (err) { - res.status(500).json({ error: "Internal server error" }); - } -}); - -app.get("/thoughts/search/:word", async (req, res) => { - const word = req.params.word.toLowerCase(); - try { - const filtered = await Thought.find({ - message: { $regex: word, $options: "i" }, - }); - res.json(filtered); - } catch (err) { - res.status(500).json({ error: "Internal server error" }); - } -}); - -app.get("/thoughts/page/:page", async (req, res) => { - const page = Number(req.params.page); - const pageSize = 5; // Number of thoughts per page - - if (!Number.isInteger(page) || page <= 0) { - return res.status(400).json({ error: "Page must 1 or more " }); - } - - try { - const thoughts = await Thought.find() - .sort({ createdAt: -1 }) - .skip((page - 1) * pageSize) - .limit(pageSize); - - res.json(thoughts); - } catch (err) { - res.status(500).json({ error: "Internal server error" }); - } -}); - -app.post("/thoughts", authenticationUser); -app.post("/thoughts", async (req, res) => { - const { message, hearts } = req.body; - try { - const newThought = await Thought.create({ - message, - hearts: hearts || 0, - }); - res.status(201).json(newThought); - } catch (err) { - res - .status(400) - .json({ error: "Could not create thought", details: err.message }); - } -}); - -app.post("/thoughts/:id/like", authenticationUser); -app.post("/thoughts/:id/like", async (req, res) => { - try { - const addHearts = await Thought.findByIdAndUpdate( - req.params.id, - { $inc: { hearts: 1 } }, // Increment hearts by 1 - { new: true } - ); - if (addHearts) { - res.json(addHearts); - } else { - res.status(404).json({ error: "Thought not found" }); - } - } catch (err) { - res.status(400).json({ error: "Invalid ID" }); - } -}); - -app.delete("/thoughts/:id", authenticationUser); -app.delete("/thoughts/:id", async (req, res) => { - try { - const deleted = await Thought.findByIdAndDelete(req.params.id); - if (deleted) { - res.json({ success: true, deleted }); - } else { - res.status(404).json({ error: "Thought not found" }); - } - } catch (err) { - res.status(400).json({ error: "Invalid ID" }); - } -}); -// PATCH endpoint to edit a thought -app.patch("/thoughts/:id", authenticationUser, async (req, res) => { - try { - const updatedThought = await Thought.findByIdAndUpdate( - req.params.id, - req.body, // Only updates the fields sent in the request - { new: true, runValidators: true } - ); - if (updatedThought) { - res.json(updatedThought); - } else { - res.status(404).json({ error: "Thought not found" }); - } - } catch (err) { - res - .status(400) - .json({ error: "Could not update thought", details: err.message }); - } -}); +// ...any other setup... -// Start the server app.listen(port, () => { console.log(`Server running on http://localhost:${port}`); }); From cfe032714410602f16144f23a3a77512e0a706aa Mon Sep 17 00:00:00 2001 From: Jonny Hicks Date: Thu, 12 Jun 2025 17:11:27 +0200 Subject: [PATCH 14/24] Refactor thoughts and users routes for improved structure and clarity --- routes/thoughts.js | 77 +++++++++++++++++++--------------------------- routes/users.js | 4 ++- server.js | 2 +- 3 files changed, 35 insertions(+), 48 deletions(-) diff --git a/routes/thoughts.js b/routes/thoughts.js index 2a2e61e..aeebd60 100644 --- a/routes/thoughts.js +++ b/routes/thoughts.js @@ -4,41 +4,28 @@ import authenticationUser from "../middlewares/authenticationUser.js"; const router = express.Router(); -router.get("/", (_, res) => { - Thought.find() - .then((thoughts) => { - res.json(thoughts); - }) - .catch((err) => { - console.error(err); - res.status(500).json({ error: "Internal server error" }); - }); -}); - -router.get("/thoughts", async (_, res) => { +router.get("/", async (_, res) => { try { const thoughts = await Thought.find().sort({ createdAt: -1 }).limit(20); res.json(thoughts); } catch (err) { - console.error(err); res.status(500).json({ error: "Internal server error" }); } }); -router.get("/thoughts/:id", async (req, res) => { +router.get("/search/:word", async (req, res) => { + const word = req.params.word.toLowerCase(); try { - const thought = await Thought.findById(req.params.id); - if (thought) { - res.json(thought); - } else { - res.status(404).json({ error: "Thought not found" }); - } + const filtered = await Thought.find({ + message: { $regex: word, $options: "i" }, + }); + res.json(filtered); } catch (err) { - res.status(400).json({ error: "Invalid ID" }); + res.status(500).json({ error: "Internal server error" }); } }); -router.get("/thoughts/hearts/:min", async (req, res) => { +router.get("/hearts/:min", async (req, res) => { const min = Number(req.params.min); if (isNaN(min)) { return res @@ -53,39 +40,37 @@ router.get("/thoughts/hearts/:min", async (req, res) => { } }); -router.get("/thoughts/search/:word", async (req, res) => { - const word = req.params.word.toLowerCase(); - try { - const filtered = await Thought.find({ - message: { $regex: word, $options: "i" }, - }); - res.json(filtered); - } catch (err) { - console.error("Search error:", err); // Add this line - res.status(500).json({ error: "Internal server error" }); - } -}); - -router.get("/thoughts/page/:page", async (req, res) => { +router.get("/page/:page", async (req, res) => { const page = Number(req.params.page); - const pageSize = 5; // Number of thoughts per page - + const pageSize = 5; if (!Number.isInteger(page) || page <= 0) { return res.status(400).json({ error: "Page must 1 or more " }); } - try { const thoughts = await Thought.find() .sort({ createdAt: -1 }) .skip((page - 1) * pageSize) .limit(pageSize); - res.json(thoughts); } catch (err) { res.status(500).json({ error: "Internal server error" }); } }); -router.post("/thoughts", authenticationUser, async (req, res) => { + +router.get("/:id", async (req, res) => { + try { + const thought = await Thought.findById(req.params.id); + if (thought) { + res.json(thought); + } else { + res.status(404).json({ error: "Thought not found" }); + } + } catch (err) { + res.status(400).json({ error: "Invalid ID" }); + } +}); + +router.post("/", authenticationUser, async (req, res) => { const { message, hearts } = req.body; try { const newThought = await Thought.create({ @@ -100,11 +85,11 @@ router.post("/thoughts", authenticationUser, async (req, res) => { } }); -router.post("/thoughts/:id/like", authenticationUser, async (req, res) => { +router.post("/:id/like", authenticationUser, async (req, res) => { try { const addHearts = await Thought.findByIdAndUpdate( req.params.id, - { $inc: { hearts: 1 } }, // Increment hearts by 1 + { $inc: { hearts: 1 } }, { new: true } ); if (addHearts) { @@ -117,7 +102,7 @@ router.post("/thoughts/:id/like", authenticationUser, async (req, res) => { } }); -router.delete("/thoughts/:id", authenticationUser, async (req, res) => { +router.delete("/:id", authenticationUser, async (req, res) => { try { const deleted = await Thought.findByIdAndDelete(req.params.id); if (deleted) { @@ -130,8 +115,7 @@ router.delete("/thoughts/:id", authenticationUser, async (req, res) => { } }); -// PATCH endpoint to edit a thought -router.patch("/thoughts/:id", authenticationUser, async (req, res) => { +router.patch("/:id", authenticationUser, async (req, res) => { try { const updatedThought = await Thought.findByIdAndUpdate( req.params.id, @@ -160,4 +144,5 @@ if (process.env.RESET_DATABASE) { }; seedThoughts(); } + export default router; diff --git a/routes/users.js b/routes/users.js index 90bdaaa..6942227 100644 --- a/routes/users.js +++ b/routes/users.js @@ -18,6 +18,7 @@ router.post("/login", async (req, res) => { router.post("/register", async (req, res) => { const { username, email, password } = req.body; + // 1. Validate input first! if (!username || !email || !password) { return res.status(400).json({ error: "All fields are required" }); } @@ -35,13 +36,14 @@ router.post("/register", async (req, res) => { if (existingUser) { return res.status(400).json({ error: "Username or email already exists" }); } + + // 2. Only hash and save after validation const salt = bcrypt.genSaltSync(); const user = new User({ username, email, password: bcrypt.hashSync(password, salt), }); - await user.save(); res.status(201).json({ diff --git a/server.js b/server.js index 1ad6427..cd45439 100644 --- a/server.js +++ b/server.js @@ -17,7 +17,7 @@ const app = express(); app.use(cors()); app.use(express.json()); -// Use routes +// Use routesrs app.use("/thoughts", thoughtsRoutes); app.use("/", usersRoutes); From c60b78c489b182480549582e984357182febf48a Mon Sep 17 00:00:00 2001 From: Jonny Hicks Date: Fri, 13 Jun 2025 07:38:18 +0200 Subject: [PATCH 15/24] Refactor authentication middleware and update thoughts route for consistency --- middlewares/authenticationUser.js | 28 ++++++++++++++++++---------- models/User.js | 6 ++++-- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/middlewares/authenticationUser.js b/middlewares/authenticationUser.js index 57d1775..1fb4ffd 100644 --- a/middlewares/authenticationUser.js +++ b/middlewares/authenticationUser.js @@ -1,16 +1,24 @@ import User from "../models/User.js"; const authenticationUser = async (req, res, next) => { - const accessToken = req.headers["accesstoken"]; - if (!accessToken) { - return res.status(401).json({ error: "Access token required" }); - } - const user = await User.findOne({ accessToken }); - if (user) { - req.user = user; - next(); - } else { - return res.status(401).json({ loggedout: true, error: "Unauthorized" }); + try { + const authHeader = req.headers && req.headers.authorization; + const accessToken = + authHeader && authHeader.startsWith("Bearer ") + ? authHeader.split(" ")[1] + : null; + if (!accessToken) { + return res.status(401).json({ error: "Access token required" }); + } + const user = await User.findOne({ accessToken }); + if (user) { + req.user = user; + return next(); + } else { + return res.status(401).json({ loggedout: true, error: "Unauthorized" }); + } + } catch (error) { + return res.status(500).json({ error: "Internal server error" }); } }; diff --git a/models/User.js b/models/User.js index 81ed710..bf2e77d 100644 --- a/models/User.js +++ b/models/User.js @@ -1,7 +1,7 @@ import mongoose from "mongoose"; import crypto from "crypto"; -const User = mongoose.model("User", { +const userSchema = new mongoose.Schema({ username: { type: String, required: true, @@ -20,8 +20,10 @@ const User = mongoose.model("User", { }, accessToken: { type: String, - default: () => crypto.randomBytes(128).toString("hex"), + default: () => crypto.randomBytes(37).toString("hex"), }, }); +const User = mongoose.models.User || mongoose.model("User", userSchema); + export default User; From 140ad0fdfa2341ed684c278f5e0d3877d4f5f075 Mon Sep 17 00:00:00 2001 From: Jonny Hicks Date: Fri, 13 Jun 2025 09:10:20 +0200 Subject: [PATCH 16/24] Add user reference to Thought model and enforce ownership in delete route --- models/Thought.js | 5 +++++ routes/thoughts.js | 16 +++++++++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/models/Thought.js b/models/Thought.js index 1ad0643..33900d1 100644 --- a/models/Thought.js +++ b/models/Thought.js @@ -16,6 +16,11 @@ const thoughtSchema = new mongoose.Schema({ type: Date, default: Date.now, }, + user: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + required: true, + }, }); const Thought = mongoose.model("Thought", thoughtSchema); diff --git a/routes/thoughts.js b/routes/thoughts.js index aeebd60..926e6d3 100644 --- a/routes/thoughts.js +++ b/routes/thoughts.js @@ -76,6 +76,7 @@ router.post("/", authenticationUser, async (req, res) => { const newThought = await Thought.create({ message, hearts: hearts || 0, + user: req.user._id, }); res.status(201).json(newThought); } catch (err) { @@ -104,12 +105,17 @@ router.post("/:id/like", authenticationUser, async (req, res) => { router.delete("/:id", authenticationUser, async (req, res) => { try { - const deleted = await Thought.findByIdAndDelete(req.params.id); - if (deleted) { - res.json({ success: true, deleted }); - } else { - res.status(404).json({ error: "Thought not found" }); + const thought = await Thought.findById(req.params.id); + if (!thought) { + return res.status(404).json({ error: "Thought not found" }); + } + if (!thought.user.equals(req.user._id)) { + return res + .status(403) + .json({ error: "You can only delete your own thoughts" }); } + await thought.deleteOne(); + res.json({ success: true, deleted: thought }); } catch (err) { res.status(400).json({ error: "Invalid ID" }); } From c3d3204b8575273ae9de5b65dfea7700b4841b25 Mon Sep 17 00:00:00 2001 From: Jonny Hicks Date: Sat, 14 Jun 2025 10:15:48 +0200 Subject: [PATCH 17/24] Refactor post and delete routes in thoughts.js for improved organization and fix user reference in post route --- models/Thought.js | 6 +++--- routes/thoughts.js | 32 +++++++++++++++++++++----------- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/models/Thought.js b/models/Thought.js index 33900d1..89f72d7 100644 --- a/models/Thought.js +++ b/models/Thought.js @@ -16,10 +16,10 @@ const thoughtSchema = new mongoose.Schema({ type: Date, default: Date.now, }, - user: { - type: mongoose.Schema.Types.ObjectId, - ref: "User", + username: { + type: String, required: true, + trim: true, }, }); diff --git a/routes/thoughts.js b/routes/thoughts.js index 926e6d3..45b2e19 100644 --- a/routes/thoughts.js +++ b/routes/thoughts.js @@ -69,6 +69,9 @@ router.get("/:id", async (req, res) => { res.status(400).json({ error: "Invalid ID" }); } }); +///////////////// +// post routes // +///////////////// router.post("/", authenticationUser, async (req, res) => { const { message, hearts } = req.body; @@ -76,7 +79,7 @@ router.post("/", authenticationUser, async (req, res) => { const newThought = await Thought.create({ message, hearts: hearts || 0, - user: req.user._id, + user: req.user.usernamne, }); res.status(201).json(newThought); } catch (err) { @@ -103,6 +106,10 @@ router.post("/:id/like", authenticationUser, async (req, res) => { } }); +//////////////////// +// Delete routes /// +//////////////////// + router.delete("/:id", authenticationUser, async (req, res) => { try { const thought = await Thought.findById(req.params.id); @@ -120,6 +127,9 @@ router.delete("/:id", authenticationUser, async (req, res) => { res.status(400).json({ error: "Invalid ID" }); } }); +////////////////// +// Patch routes // +////////////////// router.patch("/:id", authenticationUser, async (req, res) => { try { @@ -140,15 +150,15 @@ router.patch("/:id", authenticationUser, async (req, res) => { } }); -if (process.env.RESET_DATABASE) { - console.log("Resetting database"); - const seedThoughts = async () => { - await Thought.deleteMany({}); - await new Thought({ message: "This is a test one", hearts: 0 }).save(); - await new Thought({ message: "This is a test two", hearts: 7 }).save(); - await new Thought({ message: "This is a test 3", hearts: 5 }).save(); - }; - seedThoughts(); -} +//if (process.env.RESET_DATABASE) { +// console.log("Resetting database"); +//const seedThoughts = async () => { +//await Thought.deleteMany({}); +//await new Thought({ message: "This is a test one", hearts: 0 }).save(); +//await new Thought({ message: "This is a test two", hearts: 7 }).save(); +//await new Thought({ message: "This is a test 3", hearts: 5 }).save(); +//}; +//seedThoughts(); +//} export default router; From 98c5af09c849a2cd0a73fbe91c320e6d7a1b52e2 Mon Sep 17 00:00:00 2001 From: Jonny Hicks Date: Sat, 14 Jun 2025 10:33:50 +0200 Subject: [PATCH 18/24] Fix typo in user reference when creating a new thought --- routes/thoughts.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/thoughts.js b/routes/thoughts.js index 45b2e19..1cdce8e 100644 --- a/routes/thoughts.js +++ b/routes/thoughts.js @@ -79,7 +79,7 @@ router.post("/", authenticationUser, async (req, res) => { const newThought = await Thought.create({ message, hearts: hearts || 0, - user: req.user.usernamne, + user: req.user.usernamn, }); res.status(201).json(newThought); } catch (err) { From 14322551825fda4bbbb3b242c40c450a351c75c5 Mon Sep 17 00:00:00 2001 From: Jonny Hicks Date: Sat, 14 Jun 2025 10:46:55 +0200 Subject: [PATCH 19/24] Fix typo in user property assignment in post route --- routes/thoughts.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/thoughts.js b/routes/thoughts.js index 1cdce8e..16c22b8 100644 --- a/routes/thoughts.js +++ b/routes/thoughts.js @@ -79,7 +79,7 @@ router.post("/", authenticationUser, async (req, res) => { const newThought = await Thought.create({ message, hearts: hearts || 0, - user: req.user.usernamn, + user: req.user.username, }); res.status(201).json(newThought); } catch (err) { From 85cb5d3e47c4c703fcba4369b21805c207595cc2 Mon Sep 17 00:00:00 2001 From: Jonny Hicks Date: Sat, 14 Jun 2025 10:56:11 +0200 Subject: [PATCH 20/24] Fix typo in user property assignment in post route --- routes/thoughts.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/thoughts.js b/routes/thoughts.js index 16c22b8..2863d39 100644 --- a/routes/thoughts.js +++ b/routes/thoughts.js @@ -79,7 +79,7 @@ router.post("/", authenticationUser, async (req, res) => { const newThought = await Thought.create({ message, hearts: hearts || 0, - user: req.user.username, + user: req.username.usernamn, }); res.status(201).json(newThought); } catch (err) { From 9431c531946446b0e21c4a33f41491b8699203ea Mon Sep 17 00:00:00 2001 From: Jonny Hicks Date: Sat, 14 Jun 2025 11:00:17 +0200 Subject: [PATCH 21/24] Fix user property assignment in post route --- routes/thoughts.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/thoughts.js b/routes/thoughts.js index 2863d39..3b14315 100644 --- a/routes/thoughts.js +++ b/routes/thoughts.js @@ -79,7 +79,7 @@ router.post("/", authenticationUser, async (req, res) => { const newThought = await Thought.create({ message, hearts: hearts || 0, - user: req.username.usernamn, + username: req.user.username, }); res.status(201).json(newThought); } catch (err) { From 31110d1addb18970fc15c9a12c2684648845c1d3 Mon Sep 17 00:00:00 2001 From: Jonny Hicks Date: Sat, 14 Jun 2025 12:16:46 +0200 Subject: [PATCH 22/24] fixed route for delete and patch --- routes/thoughts.js | 18 ++++++++++++------ server.js | 6 +++++- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/routes/thoughts.js b/routes/thoughts.js index 3b14315..a51f1b7 100644 --- a/routes/thoughts.js +++ b/routes/thoughts.js @@ -116,7 +116,8 @@ router.delete("/:id", authenticationUser, async (req, res) => { if (!thought) { return res.status(404).json({ error: "Thought not found" }); } - if (!thought.user.equals(req.user._id)) { + if (thought.username !== req.user.username) { + // <--- Compare username return res .status(403) .json({ error: "You can only delete your own thoughts" }); @@ -133,16 +134,21 @@ router.delete("/:id", authenticationUser, async (req, res) => { router.patch("/:id", authenticationUser, async (req, res) => { try { + const thought = await Thought.findById(req.params.id); + if (!thought) { + return res.status(404).json({ error: "Thought not found" }); + } + if (thought.username !== req.user.username) { + return res + .status(403) + .json({ error: "You can only edit your own thoughts" }); + } const updatedThought = await Thought.findByIdAndUpdate( req.params.id, req.body, { new: true, runValidators: true } ); - if (updatedThought) { - res.json(updatedThought); - } else { - res.status(404).json({ error: "Thought not found" }); - } + res.json(updatedThought); } catch (err) { res .status(400) diff --git a/server.js b/server.js index cd45439..fb5d7f3 100644 --- a/server.js +++ b/server.js @@ -14,7 +14,11 @@ mongoose.Promise = Promise; const port = process.env.PORT || 8088; const app = express(); -app.use(cors()); +app.use( + cors({ + origin: "*", // Allow all origins for development + }) +); app.use(express.json()); // Use routesrs From c8ae68833a88027bcbb9676a10ee3198da3a044e Mon Sep 17 00:00:00 2001 From: Jonny Hicks Date: Sat, 14 Jun 2025 13:37:22 +0200 Subject: [PATCH 23/24] Return success status with updated thought in patch route response --- routes/thoughts.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/thoughts.js b/routes/thoughts.js index a51f1b7..7488751 100644 --- a/routes/thoughts.js +++ b/routes/thoughts.js @@ -148,7 +148,7 @@ router.patch("/:id", authenticationUser, async (req, res) => { req.body, { new: true, runValidators: true } ); - res.json(updatedThought); + res.json({ success: true, thought: updatedThought }); } catch (err) { res .status(400) From b632444334a9fbc3dbb788ec2828be713bd80bcf Mon Sep 17 00:00:00 2001 From: Jonny Hicks Date: Sat, 14 Jun 2025 14:13:04 +0200 Subject: [PATCH 24/24] Add express-list-endpoints dependency and update root endpoint response --- package.json | 1 + server.js | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index ddadf79..dfda471 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "cors": "^2.8.5", "dotenv": "^16.5.0", "express": "^4.21.2", + "express-list-endpoints": "^7.1.1", "mongoose": "^8.15.1", "nodemon": "^3.0.1", "validator": "^13.15.15" diff --git a/server.js b/server.js index fb5d7f3..cca267f 100644 --- a/server.js +++ b/server.js @@ -13,7 +13,7 @@ mongoose.Promise = Promise; const port = process.env.PORT || 8088; const app = express(); - +const listEndpoints = require("express-list-endpoints"); app.use( cors({ origin: "*", // Allow all origins for development @@ -25,7 +25,13 @@ app.use(express.json()); app.use("/thoughts", thoughtsRoutes); app.use("/", usersRoutes); -// ...any other setup... +app.get("/", (req, res) => { + const endpoints = listEndpoints(app); + res.json({ + message: " Welcome to my Endpoint to Happy Thoughts API", + endpoints, + }); +}); app.listen(port, () => { console.log(`Server running on http://localhost:${port}`);