From e064949405497745611e4e8c89343630a9228724 Mon Sep 17 00:00:00 2001 From: Gabriella Berkowicz Date: Mon, 19 Jan 2026 13:20:37 +0100 Subject: [PATCH 01/39] add routes for all messages, for a specific date and a specific ID --- package.json | 1 + server.js | 39 ++++++++++++++++++++++++++++++--------- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index bf25bb6..00addae 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@babel/preset-env": "^7.16.11", "cors": "^2.8.5", "express": "^4.17.3", + "express-list-endpoints": "^7.1.1", "nodemon": "^3.0.1" } } diff --git a/server.js b/server.js index f47771b..a76f506 100644 --- a/server.js +++ b/server.js @@ -1,22 +1,43 @@ -import cors from "cors" -import express from "express" +import cors from "cors"; +import express from "express"; +import data 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 || 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()); // Start defining your routes here app.get("/", (req, res) => { - res.send("Hello Technigo!") + res.send("Hello Technigo!"); // TO DO: replace with documentation of your API using e.g. Express List Endpoints }) +// All messages +app.get("/messages", (req, res) => { + res.json(data); +}); + +// Messages for a specific date +app.get("/messages/date/:date", (req, res) => { + const date = req.params.date; + const messagesFromDate = data.filter((message) => message.createdAt.slice(0, 10) === date); // createdAt needs to match format "YYYY-MM-DD" + res.json(messagesFromDate); +}); + +// Message with a specific ID +app.get("/message/id/:id", (req, res) => { + const id = req.params.id; + const messageOfId = data.filter((message) => message._id === id); + res.json(messageOfId); +}); + // Start the server app.listen(port, () => { - console.log(`Server running on http://localhost:${port}`) -}) + console.log(`Server running on http://localhost:${port}`); +}); + From 79bb3c7ee48f090cd5bb59b82337493fd9e6df4a Mon Sep 17 00:00:00 2001 From: Gabriella Berkowicz Date: Mon, 19 Jan 2026 13:23:08 +0100 Subject: [PATCH 02/39] add documentation of API to / route --- server.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server.js b/server.js index a76f506..5cce9e3 100644 --- a/server.js +++ b/server.js @@ -1,6 +1,7 @@ import cors from "cors"; import express from "express"; import data from "./data.json"; +import expressListEndpoints from "express-list-endpoints"; // 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: @@ -14,7 +15,8 @@ app.use(express.json()); // Start defining your routes here app.get("/", (req, res) => { - res.send("Hello Technigo!"); // TO DO: replace with documentation of your API using e.g. Express List Endpoints + const endpoints = expressListEndpoints(app); + res.send(endpoints); }) // All messages From afb32df955d6008a0feb6054f70df92c1845dec4 Mon Sep 17 00:00:00 2001 From: Gabriella Berkowicz Date: Mon, 19 Jan 2026 13:57:36 +0100 Subject: [PATCH 03/39] add error handling to message id route --- server.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/server.js b/server.js index 5cce9e3..2b1fd46 100644 --- a/server.js +++ b/server.js @@ -32,9 +32,17 @@ app.get("/messages/date/:date", (req, res) => { }); // Message with a specific ID -app.get("/message/id/:id", (req, res) => { +app.get("/messages/id/:id", (req, res) => { const id = req.params.id; - const messageOfId = data.filter((message) => message._id === id); + const messageOfId = data.find((message) => message._id === id); + + if(!messageOfId) { + return res.status(404).json({ + error: "Message not found", + requestedId: id + }); + } + res.json(messageOfId); }); From cdc75624288f3acddf09ed678e265da3682573af Mon Sep 17 00:00:00 2001 From: Gabriella Berkowicz Date: Tue, 20 Jan 2026 09:23:56 +0100 Subject: [PATCH 04/39] add functionality for pagination in the route for all messages --- server.js | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/server.js b/server.js index 2b1fd46..73f6585 100644 --- a/server.js +++ b/server.js @@ -19,9 +19,22 @@ app.get("/", (req, res) => { res.send(endpoints); }) -// All messages + +// All messages (with pagination) app.get("/messages", (req, res) => { - res.json(data); + + // Functionality for pagination + const page = Number(req.query.page) || 1 ; // Query param + const numOfTotalMessages = data.length; + const messagesPerPage = 10; + const numOfPages = Math.ceil(numOfTotalMessages / messagesPerPage); // Always round the result up, so there will be an extra page for any remainder + + // Define where to split the array of messages for each page + const start = (page - 1) * messagesPerPage; + const end = start + messagesPerPage; + + const pageResults = data.slice(start, end); + res.json({page, numOfPages, numOfTotalMessages, pageResults}); }); // Messages for a specific date @@ -46,7 +59,7 @@ app.get("/messages/id/:id", (req, res) => { res.json(messageOfId); }); -// Start the server +//Start the server app.listen(port, () => { console.log(`Server running on http://localhost:${port}`); }); From 3bfd9349a95d984701b28f5115aa5a471c147fa3 Mon Sep 17 00:00:00 2001 From: Gabriella Berkowicz Date: Tue, 20 Jan 2026 13:50:03 +0100 Subject: [PATCH 05/39] add sorting functionality for date and likes with query parameters --- server.js | 50 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/server.js b/server.js index 73f6585..ff1a6e1 100644 --- a/server.js +++ b/server.js @@ -13,7 +13,10 @@ const app = express(); app.use(cors()); app.use(express.json()); -// Start defining your routes here + +/* --- ROUTES --- */ + + app.get("/", (req, res) => { const endpoints = expressListEndpoints(app); res.send(endpoints); @@ -23,20 +26,46 @@ app.get("/", (req, res) => { // All messages (with pagination) app.get("/messages", (req, res) => { - // Functionality for pagination + let messages = [ ...data ]; // To not mutate original array + + /* --- Functionality for sorting --- */ + const { sort, order } = req.query; + + if(sort === "date") { + messages.sort((a,b) => { + if(order === "desc") { + return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); + } else if(order === "asc") { + return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(); + } + }); + } + + if(sort === "likes") { + messages.sort((a,b) => { + if(order === "desc") { + return b.hearts - a.hearts; + } else if(order === "asc") { + return a.hearts - b.hearts; + } + }); + } + + /* --- Functionality for pagination --- */ const page = Number(req.query.page) || 1 ; // Query param - const numOfTotalMessages = data.length; + const numOfTotalMessages = messages.length; const messagesPerPage = 10; const numOfPages = Math.ceil(numOfTotalMessages / messagesPerPage); // Always round the result up, so there will be an extra page for any remainder - // Define where to split the array of messages for each page + // Define where to slice the array of messages for each page const start = (page - 1) * messagesPerPage; const end = start + messagesPerPage; - const pageResults = data.slice(start, end); + const pageResults = messages.slice(start, end); res.json({page, numOfPages, numOfTotalMessages, pageResults}); }); + // Messages for a specific date app.get("/messages/date/:date", (req, res) => { const date = req.params.date; @@ -44,21 +73,22 @@ app.get("/messages/date/:date", (req, res) => { res.json(messagesFromDate); }); + // Message with a specific ID app.get("/messages/id/:id", (req, res) => { const id = req.params.id; - const messageOfId = data.find((message) => message._id === id); + const message = data.find((message) => message._id === id); - if(!messageOfId) { + if(!message) { return res.status(404).json({ - error: "Message not found", - requestedId: id + error: `Message with id ${id} not found` }); } - res.json(messageOfId); + res.json(message); }); + //Start the server app.listen(port, () => { console.log(`Server running on http://localhost:${port}`); From efebf1b9c5fbc7833cd7ab7884b6fe46aa9c78ad Mon Sep 17 00:00:00 2001 From: Gabriella Berkowicz Date: Tue, 20 Jan 2026 14:44:37 +0100 Subject: [PATCH 06/39] add functionality for filtering --- server.js | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/server.js b/server.js index ff1a6e1..6d565d3 100644 --- a/server.js +++ b/server.js @@ -19,7 +19,11 @@ app.use(express.json()); app.get("/", (req, res) => { const endpoints = expressListEndpoints(app); - res.send(endpoints); + res.json({ + message: "Welcome to the Happy Thoughts API", + endpoints: endpoints + }); + }) @@ -28,6 +32,17 @@ app.get("/messages", (req, res) => { let messages = [ ...data ]; // To not mutate original array + /* --- Functionality for filtering --- */ + const { fromDate, minLikes } = req.query; + + if(fromDate) { + messages = messages.filter((message) => message.createdAt.slice(0, 10) > fromDate); + } + if(minLikes) { + messages = messages.filter((message) => message.hearts >= Number(minLikes)); + } + + /* --- Functionality for sorting --- */ const { sort, order } = req.query; @@ -92,5 +107,4 @@ app.get("/messages/id/:id", (req, res) => { //Start the server app.listen(port, () => { console.log(`Server running on http://localhost:${port}`); -}); - +}); \ No newline at end of file From 0a9313f86c074351ab5eb4f74fd9e5da3379f81b Mon Sep 17 00:00:00 2001 From: Gabriella Berkowicz Date: Tue, 20 Jan 2026 14:47:12 +0100 Subject: [PATCH 07/39] default to descending order in the sorting --- server.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/server.js b/server.js index 6d565d3..6923e6a 100644 --- a/server.js +++ b/server.js @@ -48,20 +48,20 @@ app.get("/messages", (req, res) => { if(sort === "date") { messages.sort((a,b) => { - if(order === "desc") { - return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); - } else if(order === "asc") { + if(order === "asc") { return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(); + } else { + return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); // Default to desc } }); } if(sort === "likes") { messages.sort((a,b) => { - if(order === "desc") { - return b.hearts - a.hearts; - } else if(order === "asc") { + if(order === "asc") { return a.hearts - b.hearts; + } else { + return b.hearts - a.hearts; // Default to desc } }); } From 46d963554bed3df2ff30564959ee58a4146a424d Mon Sep 17 00:00:00 2001 From: Gabriella Berkowicz Date: Fri, 23 Jan 2026 10:58:15 +0100 Subject: [PATCH 08/39] set up mongoDB with error handling + some first routes + temporary data base seeding function --- models/Thought.js | 20 ++++++++++++++ package.json | 1 + seedDatabase.js | 21 ++++++++++++++ server.js | 70 +++++++++++++++++++++++++++++++++++++++++------ 4 files changed, 104 insertions(+), 8 deletions(-) create mode 100644 models/Thought.js create mode 100644 seedDatabase.js diff --git a/models/Thought.js b/models/Thought.js new file mode 100644 index 0000000..31be837 --- /dev/null +++ b/models/Thought.js @@ -0,0 +1,20 @@ +import mongoose from "mongoose"; + +const ThoughtSchema = mongoose.Schema({ + message: { + type: String, + required: true, + minlength: 2, + maxlength: 140 + }, + hearts: { + type: Number, + default: 0 + }, + createdAt: { + type: Date, + default: Date.now + } +}); + +export default mongoose.model("Thought", ThoughtSchema); \ No newline at end of file diff --git a/package.json b/package.json index 00addae..214a34f 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "cors": "^2.8.5", "express": "^4.17.3", "express-list-endpoints": "^7.1.1", + "mongoose": "^9.1.5", "nodemon": "^3.0.1" } } diff --git a/seedDatabase.js b/seedDatabase.js new file mode 100644 index 0000000..8ee0716 --- /dev/null +++ b/seedDatabase.js @@ -0,0 +1,21 @@ +import Thought from "./models/Thought" + +export const seedDatabase = async () => { + await Thought.deleteMany() // To not have duplicates every time this function runs + + /* --- Using the models created to add data (Thoughts) --- */ + + await new Thought({ message: "Berlin baby", hearts: 37, createdAt: "2025-05-19T22:07:08.999Z" }).save(); + await new Thought({ message: "My family!", createdAt: "2025-05-22T22:29:32.232Z" }).save(); + await new Thought({ message: "The smell of coffee in the morning....", hearts: 23, createdAt: "2025-05-22T22:11:16.075Z" }).save(); + await new Thought({ message: "Newly washed bedlinen, kids that sleeps through the night.. FINGERS CROSSED 🤞🏼\n", hearts: 6, createdAt: "2025-05-21T21:42:23.862Z" }).save(); + await new Thought({ message: "I am happy that I feel healthy and have energy again", hearts: 13, createdAt: "2025-05-21T21:28:32.196Z" }).save(); + await new Thought({ message: "Cold beer", hearts: 2, createdAt: "2025-05-21T19:05:34.113Z" }).save(); + await new Thought({ message: "My friend is visiting this weekend! <3", hearts: 6, createdAt: "2025-05-21T18:59:58.121Z" }).save(); + await new Thought({ message: "A good joke: \nWhy did the scarecrow win an award?\nBecause he was outstanding in his field!", hearts: 12, createdAt: "2025-05-20T20:54:51.082Z" }).save(); + await new Thought({ message: "Tacos and tequila🌮🍹", hearts: 2, createdAt: "2025-05-19T20:53:18.899Z" }).save(); + await new Thought({ message: "Netflix and late night ice-cream🍦", hearts: 1, createdAt: "2025-05-18T20:51:34.494Z" }).save(); + await new Thought({ message: "The weather is nice!", hearts: 2, createdAt: "2025-05-20T15:03:22.379Z" }).save(); + await new Thought({ message: "Summer is coming...", createdAt: "2025-05-20T11:58:29.662Z" }).save(); + await new Thought({ message: "good vibes and good things", hearts: 3, createdAt: "2025-05-20T03:57:40.322Z" }).save(); +}; \ No newline at end of file diff --git a/server.js b/server.js index 6923e6a..240e0ef 100644 --- a/server.js +++ b/server.js @@ -2,10 +2,12 @@ import cors from "cors"; import express from "express"; import data from "./data.json"; import expressListEndpoints from "express-list-endpoints"; +import mongoose from "mongoose"; +import Thought from "./models/Thought"; +import { seedDatabase } from "./seedDatabase"; // 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 +// 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(); @@ -14,8 +16,17 @@ app.use(cors()); app.use(express.json()); -/* --- ROUTES --- */ +/* --- Error handling to check database (MongoDB) connection --- */ +app.use((req, res, next) => { + if(mongoose.connection.readyState === 1) { // 1 is connected + next(); // Continue on executing what comes after + } else { + res.status(503).json({error: "Service unavailable"}); + } +}); + +/* --- Routes --- */ app.get("/", (req, res) => { const endpoints = expressListEndpoints(app); @@ -26,6 +37,25 @@ app.get("/", (req, res) => { }) +app.get("/thoughts", async (req, res) => { + const thoughts = await Thought.find(); + res.json(thoughts); +}); + +app.post("/thoughts", async (req, res) => { + // Retrieve the information sent by the client to our API endpoint + const message = req.body.message; + // Use our mongoose model to create the database entry + const thought = new Thought({ message }); + + try { + const savedThought = await thought.save(); + res.status(200).json(savedThought); + } catch(err) { + res.status(400).json({message: "Failed to save thought to database", error: err.errors}); + } +}); + // All messages (with pagination) app.get("/messages", (req, res) => { @@ -42,7 +72,6 @@ app.get("/messages", (req, res) => { messages = messages.filter((message) => message.hearts >= Number(minLikes)); } - /* --- Functionality for sorting --- */ const { sort, order } = req.query; @@ -104,7 +133,32 @@ app.get("/messages/id/:id", (req, res) => { }); -//Start the server -app.listen(port, () => { - console.log(`Server running on http://localhost:${port}`); -}); \ No newline at end of file +/* --- Connect to Mongo --- */ +const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/thoughts"; +mongoose.connect(mongoUrl) + .then(async () => { + console.log('MongoDB connected'); + + + await seedDatabase(); // Temporary seeding (add async & await) + + // Start the server + app.listen(port, () => { + console.log(`Server running on http://localhost:${port}`); + }); + }) + .catch(err => console.error('MongoDB connection error:', err)); + + +// /* --- Connect to Mongo --- */ +// const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/thoughts"; +// mongoose.connect(mongoUrl) +// .then(() => console.log('MongoDB connected')) +// .catch(err => console.error('MongoDB connection error:', err)); +// mongoose.Promise = Promise; // optional (legacy) + + +// /* --- Start the server --- */ +// app.listen(port, () => { +// console.log(`Server running on http://localhost:${port}`); +// }); \ No newline at end of file From f2f15745a3bddf51ad2d808d6d9e9bd9228e9535 Mon Sep 17 00:00:00 2001 From: Gabriella Berkowicz Date: Fri, 23 Jan 2026 11:33:25 +0100 Subject: [PATCH 09/39] add delete route for deleting a thought --- seedDatabase.js | 2 +- server.js | 43 ++++++++++++++++++++++++++++--------------- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/seedDatabase.js b/seedDatabase.js index 8ee0716..c4965dc 100644 --- a/seedDatabase.js +++ b/seedDatabase.js @@ -3,7 +3,7 @@ import Thought from "./models/Thought" export const seedDatabase = async () => { await Thought.deleteMany() // To not have duplicates every time this function runs - /* --- Using the models created to add data (Thoughts) --- */ + /* --- Using the models created to add data --- */ await new Thought({ message: "Berlin baby", hearts: 37, createdAt: "2025-05-19T22:07:08.999Z" }).save(); await new Thought({ message: "My family!", createdAt: "2025-05-22T22:29:32.232Z" }).save(); diff --git a/server.js b/server.js index 240e0ef..f76b34c 100644 --- a/server.js +++ b/server.js @@ -28,6 +28,7 @@ app.use((req, res, next) => { /* --- Routes --- */ + app.get("/", (req, res) => { const endpoints = expressListEndpoints(app); res.json({ @@ -37,11 +38,35 @@ app.get("/", (req, res) => { }) + +// All thoughts app.get("/thoughts", async (req, res) => { const thoughts = await Thought.find(); res.json(thoughts); }); + +// Delete a thought +app.delete("/thoughts/id/:id", async (req, res) => { + const id = req.params.id; + + // Error handling for invalid id input + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ error: `Invalid id: ${id}` }); + } + + const deletedThought = await Thought.findById(id); + + // Error handling for no ID match + if(!deletedThought) { + return res.status(404).json({ + error: `Thought with id ${id} not found` + }); + } + res.status(200).json(deletedThought); +}); + + app.post("/thoughts", async (req, res) => { // Retrieve the information sent by the client to our API endpoint const message = req.body.message; @@ -57,6 +82,8 @@ app.post("/thoughts", async (req, res) => { }); +/* --- Old routes for statix json file --- */ + // All messages (with pagination) app.get("/messages", (req, res) => { @@ -147,18 +174,4 @@ mongoose.connect(mongoUrl) console.log(`Server running on http://localhost:${port}`); }); }) - .catch(err => console.error('MongoDB connection error:', err)); - - -// /* --- Connect to Mongo --- */ -// const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/thoughts"; -// mongoose.connect(mongoUrl) -// .then(() => console.log('MongoDB connected')) -// .catch(err => console.error('MongoDB connection error:', err)); -// mongoose.Promise = Promise; // optional (legacy) - - -// /* --- Start the server --- */ -// app.listen(port, () => { -// console.log(`Server running on http://localhost:${port}`); -// }); \ No newline at end of file + .catch(err => console.error('MongoDB connection error:', err)); \ No newline at end of file From ac9537bfb288bcfb6f30c71b80fb88fb5410fbfe Mon Sep 17 00:00:00 2001 From: Gabriella Berkowicz Date: Fri, 23 Jan 2026 14:20:14 +0100 Subject: [PATCH 10/39] add a patch route to be able to update the message of a thought + refactor the delete route with better error handling --- server.js | 55 ++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 11 deletions(-) diff --git a/server.js b/server.js index f76b34c..359e810 100644 --- a/server.js +++ b/server.js @@ -16,7 +16,7 @@ app.use(cors()); app.use(express.json()); -/* --- Error handling to check database (MongoDB) connection --- */ +/* --- Error handling to check database connection --- */ app.use((req, res, next) => { if(mongoose.connection.readyState === 1) { // 1 is connected next(); // Continue on executing what comes after @@ -35,8 +35,7 @@ app.get("/", (req, res) => { message: "Welcome to the Happy Thoughts API", endpoints: endpoints }); - -}) +}); // All thoughts @@ -55,15 +54,49 @@ app.delete("/thoughts/id/:id", async (req, res) => { return res.status(400).json({ error: `Invalid id: ${id}` }); } - const deletedThought = await Thought.findById(id); + try { + const deletedThought = await Thought.findById(id); - // Error handling for no ID match - if(!deletedThought) { - return res.status(404).json({ - error: `Thought with id ${id} not found` - }); + // Error handling for no ID match + if(!deletedThought) { + return res.status(404).json({error: `Thought with id ${id} not found`}); + } + + res.json(deletedThought); + + } catch(err) { + res.status(500).json({error: err.message}); + } +}); + + +// Update the message of a thought +app.patch("/thoughts/id/:id", async (req, res) => { + const { id } = req.params; + const { message } = req.body; + + // Error handling for invalid id input + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ error: `Invalid id: ${id}` }); + } + + try { + const updatedThought = await Thought.findByIdAndUpdate( + id, + { message }, + { new: true, runValidators: true} //Ensures the updated message gets returned, and that schema validation also is performed on the new message + ); + + // Error handling for no ID match + if(!updatedThought) { + return res.status(404).json({error: `Thought with id ${id} not found`}); + } + + res.json(updatedThought); + + } catch(err) { + res.status(500).json({error: err.message}); } - res.status(200).json(deletedThought); }); @@ -123,7 +156,7 @@ app.get("/messages", (req, res) => { } /* --- Functionality for pagination --- */ - const page = Number(req.query.page) || 1 ; // Query param + const page = Number(req.query.page) || 1; const numOfTotalMessages = messages.length; const messagesPerPage = 10; const numOfPages = Math.ceil(numOfTotalMessages / messagesPerPage); // Always round the result up, so there will be an extra page for any remainder From 65a3e439c46f6662a8131bd48a143cca191f99e1 Mon Sep 17 00:00:00 2001 From: Gabriella Berkowicz Date: Fri, 23 Jan 2026 14:59:03 +0100 Subject: [PATCH 11/39] update delete route --- server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server.js b/server.js index 359e810..69eec24 100644 --- a/server.js +++ b/server.js @@ -55,7 +55,7 @@ app.delete("/thoughts/id/:id", async (req, res) => { } try { - const deletedThought = await Thought.findById(id); + const deletedThought = await Thought.findByIdAndDelete(id); // Error handling for no ID match if(!deletedThought) { From 9069a0b7f59e832be1929dc663f27f1c532af80e Mon Sep 17 00:00:00 2001 From: Gabriella Berkowicz Date: Fri, 23 Jan 2026 15:52:14 +0100 Subject: [PATCH 12/39] add the functionality for filtering via mongoose --- server.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/server.js b/server.js index 69eec24..80f80d7 100644 --- a/server.js +++ b/server.js @@ -40,7 +40,22 @@ app.get("/", (req, res) => { // All thoughts app.get("/thoughts", async (req, res) => { - const thoughts = await Thought.find(); + + /* --- Functionality for filtering --- */ + const { fromDate, minLikes } = req.query; + const filter = {}; // To use as argument in Model.find(). Will be a criteria or object (thus retrieving all thoughts) + + //Filter on minimum of likes + if(minLikes){ + filter.hearts = { $gte: Number(minLikes) }; //gte = greater than or equal to + } + + // Filter from a date + if(fromDate) { + filter.createdAt = { $gte: new Date(fromDate) }; + } + + const thoughts = await Thought.find(filter); res.json(thoughts); }); From f798f152cd3d6beef3953797270175d28f8598e5 Mon Sep 17 00:00:00 2001 From: Gabriella Berkowicz Date: Fri, 23 Jan 2026 16:54:23 +0100 Subject: [PATCH 13/39] add functionality for dynamic sorting via mongoose --- server.js | 172 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 98 insertions(+), 74 deletions(-) diff --git a/server.js b/server.js index 80f80d7..94c2948 100644 --- a/server.js +++ b/server.js @@ -42,20 +42,44 @@ app.get("/", (req, res) => { app.get("/thoughts", async (req, res) => { /* --- Functionality for filtering --- */ + const filterCriteria = {}; // To use as argument in Model.find(). Will be a criteria or object (thus retrieving all thoughts) const { fromDate, minLikes } = req.query; - const filter = {}; // To use as argument in Model.find(). Will be a criteria or object (thus retrieving all thoughts) //Filter on minimum of likes if(minLikes){ - filter.hearts = { $gte: Number(minLikes) }; //gte = greater than or equal to + filterCriteria.hearts = { $gte: Number(minLikes) }; //gte = greater than or equal to } // Filter from a date if(fromDate) { - filter.createdAt = { $gte: new Date(fromDate) }; + filterCriteria.createdAt = { $gte: new Date(fromDate) }; } - - const thoughts = await Thought.find(filter); + + /* --- Functionality for sorting --- */ + const sortCriteria = {}; + const { sortBy, order } = req.query; + const sortingOrder = order === "asc" ? 1 : -1; + + // Translate to keep readable query parameters in the URL + let sort = sortBy; + if (sortBy === "date") { + sort = "createdAt"; + } + if (sortBy === "likes") { + sort = "hearts"; + } + + if(sort){ + // Set the key-value pair in the object sortCriteria dynamically - obj[key] = value + sortCriteria[sort] = sortingOrder; // Set the key to the value of sort and its value to sortingOrder + if (sort !== "createdAt") { + sortCriteria.createdAt = -1; // Puts creation date as secondary sorting + } + } else { + sortCriteria.createdAt = -1; // Creation date as default sorting + } + + const thoughts = await Thought.find(filterCriteria).sort(sortCriteria); res.json(thoughts); }); @@ -130,82 +154,82 @@ app.post("/thoughts", async (req, res) => { }); -/* --- Old routes for statix json file --- */ +// /* --- Old routes for statix json file --- */ -// All messages (with pagination) -app.get("/messages", (req, res) => { +// // All messages (with pagination) +// app.get("/messages", (req, res) => { - let messages = [ ...data ]; // To not mutate original array +// let messages = [ ...data ]; // To not mutate original array - /* --- Functionality for filtering --- */ - const { fromDate, minLikes } = req.query; +// /* --- Functionality for filtering --- */ +// const { fromDate, minLikes } = req.query; - if(fromDate) { - messages = messages.filter((message) => message.createdAt.slice(0, 10) > fromDate); - } - if(minLikes) { - messages = messages.filter((message) => message.hearts >= Number(minLikes)); - } +// if(fromDate) { +// messages = messages.filter((message) => message.createdAt.slice(0, 10) > fromDate); +// } +// if(minLikes) { +// messages = messages.filter((message) => message.hearts >= Number(minLikes)); +// } - /* --- Functionality for sorting --- */ - const { sort, order } = req.query; +// /* --- Functionality for sorting --- */ +// const { sort, order } = req.query; - if(sort === "date") { - messages.sort((a,b) => { - if(order === "asc") { - return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(); - } else { - return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); // Default to desc - } - }); - } - - if(sort === "likes") { - messages.sort((a,b) => { - if(order === "asc") { - return a.hearts - b.hearts; - } else { - return b.hearts - a.hearts; // Default to desc - } - }); - } - - /* --- Functionality for pagination --- */ - const page = Number(req.query.page) || 1; - const numOfTotalMessages = messages.length; - const messagesPerPage = 10; - const numOfPages = Math.ceil(numOfTotalMessages / messagesPerPage); // Always round the result up, so there will be an extra page for any remainder - - // Define where to slice the array of messages for each page - const start = (page - 1) * messagesPerPage; - const end = start + messagesPerPage; - - const pageResults = messages.slice(start, end); - res.json({page, numOfPages, numOfTotalMessages, pageResults}); -}); - - -// Messages for a specific date -app.get("/messages/date/:date", (req, res) => { - const date = req.params.date; - const messagesFromDate = data.filter((message) => message.createdAt.slice(0, 10) === date); // createdAt needs to match format "YYYY-MM-DD" - res.json(messagesFromDate); -}); - - -// Message with a specific ID -app.get("/messages/id/:id", (req, res) => { - const id = req.params.id; - const message = data.find((message) => message._id === id); +// if(sort === "date") { +// messages.sort((a,b) => { +// if(order === "asc") { +// return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(); +// } else { +// return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); // Default to desc +// } +// }); +// } + +// if(sort === "likes") { +// messages.sort((a,b) => { +// if(order === "asc") { +// return a.hearts - b.hearts; +// } else { +// return b.hearts - a.hearts; // Default to desc +// } +// }); +// } + +// /* --- Functionality for pagination --- */ +// const page = Number(req.query.page) || 1; +// const numOfTotalMessages = messages.length; +// const messagesPerPage = 10; +// const numOfPages = Math.ceil(numOfTotalMessages / messagesPerPage); // Always round the result up, so there will be an extra page for any remainder + +// // Define where to slice the array of messages for each page +// const start = (page - 1) * messagesPerPage; +// const end = start + messagesPerPage; + +// const pageResults = messages.slice(start, end); +// res.json({page, numOfPages, numOfTotalMessages, pageResults}); +// }); + + +// // Messages for a specific date +// app.get("/messages/date/:date", (req, res) => { +// const date = req.params.date; +// const messagesFromDate = data.filter((message) => message.createdAt.slice(0, 10) === date); // createdAt needs to match format "YYYY-MM-DD" +// res.json(messagesFromDate); +// }); + + +// // Message with a specific ID +// app.get("/messages/id/:id", (req, res) => { +// const id = req.params.id; +// const message = data.find((message) => message._id === id); - if(!message) { - return res.status(404).json({ - error: `Message with id ${id} not found` - }); - } - - res.json(message); -}); +// if(!message) { +// return res.status(404).json({ +// error: `Message with id ${id} not found` +// }); +// } + +// res.json(message); +// }); /* --- Connect to Mongo --- */ From bab940d3342a8c75c8fb4c61933b518a5663760e Mon Sep 17 00:00:00 2001 From: Gabriella Berkowicz Date: Mon, 26 Jan 2026 11:36:59 +0100 Subject: [PATCH 14/39] config dotenv + remove calling of seedDatabase + clean code --- package.json | 2 ++ server.js | 87 +++------------------------------------------------- 2 files changed, 6 insertions(+), 83 deletions(-) diff --git a/package.json b/package.json index 214a34f..14214eb 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,10 @@ "@babel/node": "^7.16.8", "@babel/preset-env": "^7.16.11", "cors": "^2.8.5", + "dotenv": "^17.2.3", "express": "^4.17.3", "express-list-endpoints": "^7.1.1", + "mongodb": "^7.0.0", "mongoose": "^9.1.5", "nodemon": "^3.0.1" } diff --git a/server.js b/server.js index 94c2948..0c39c69 100644 --- a/server.js +++ b/server.js @@ -1,10 +1,11 @@ import cors from "cors"; import express from "express"; -import data from "./data.json"; import expressListEndpoints from "express-list-endpoints"; import mongoose from "mongoose"; import Thought from "./models/Thought"; import { seedDatabase } from "./seedDatabase"; +import dotenv from "dotenv"; +dotenv.config(); // 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 @@ -28,7 +29,6 @@ app.use((req, res, next) => { /* --- Routes --- */ - app.get("/", (req, res) => { const endpoints = expressListEndpoints(app); res.json({ @@ -154,92 +154,13 @@ app.post("/thoughts", async (req, res) => { }); -// /* --- Old routes for statix json file --- */ - -// // All messages (with pagination) -// app.get("/messages", (req, res) => { - -// let messages = [ ...data ]; // To not mutate original array - -// /* --- Functionality for filtering --- */ -// const { fromDate, minLikes } = req.query; - -// if(fromDate) { -// messages = messages.filter((message) => message.createdAt.slice(0, 10) > fromDate); -// } -// if(minLikes) { -// messages = messages.filter((message) => message.hearts >= Number(minLikes)); -// } - -// /* --- Functionality for sorting --- */ -// const { sort, order } = req.query; - -// if(sort === "date") { -// messages.sort((a,b) => { -// if(order === "asc") { -// return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(); -// } else { -// return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); // Default to desc -// } -// }); -// } - -// if(sort === "likes") { -// messages.sort((a,b) => { -// if(order === "asc") { -// return a.hearts - b.hearts; -// } else { -// return b.hearts - a.hearts; // Default to desc -// } -// }); -// } - -// /* --- Functionality for pagination --- */ -// const page = Number(req.query.page) || 1; -// const numOfTotalMessages = messages.length; -// const messagesPerPage = 10; -// const numOfPages = Math.ceil(numOfTotalMessages / messagesPerPage); // Always round the result up, so there will be an extra page for any remainder - -// // Define where to slice the array of messages for each page -// const start = (page - 1) * messagesPerPage; -// const end = start + messagesPerPage; - -// const pageResults = messages.slice(start, end); -// res.json({page, numOfPages, numOfTotalMessages, pageResults}); -// }); - - -// // Messages for a specific date -// app.get("/messages/date/:date", (req, res) => { -// const date = req.params.date; -// const messagesFromDate = data.filter((message) => message.createdAt.slice(0, 10) === date); // createdAt needs to match format "YYYY-MM-DD" -// res.json(messagesFromDate); -// }); - - -// // Message with a specific ID -// app.get("/messages/id/:id", (req, res) => { -// const id = req.params.id; -// const message = data.find((message) => message._id === id); - -// if(!message) { -// return res.status(404).json({ -// error: `Message with id ${id} not found` -// }); -// } - -// res.json(message); -// }); - - /* --- Connect to Mongo --- */ const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/thoughts"; mongoose.connect(mongoUrl) - .then(async () => { + .then(() => { console.log('MongoDB connected'); - - await seedDatabase(); // Temporary seeding (add async & await) + // await seedDatabase(); // Temporary seeding (add async & await) // Start the server app.listen(port, () => { From 445b9ab7f17af219a4adc0a0ffe183f2ba360815 Mon Sep 17 00:00:00 2001 From: Gabriella Berkowicz Date: Mon, 26 Jan 2026 14:47:04 +0100 Subject: [PATCH 15/39] add patch route for updating the like count of a thought --- server.js | 57 ++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 44 insertions(+), 13 deletions(-) diff --git a/server.js b/server.js index 0c39c69..d50445c 100644 --- a/server.js +++ b/server.js @@ -84,6 +84,22 @@ app.get("/thoughts", async (req, res) => { }); +// Post a thought +app.post("/thoughts", async (req, res) => { + // Retrieve the information sent by the client to our API endpoint + const message = req.body.message; + // Use our mongoose model to create the database entry + const thought = new Thought({ message }); + + try { + const savedThought = await thought.save(); + res.status(200).json(savedThought); + } catch(err) { + res.status(400).json({message: "Failed to save thought to database", error: err.errors}); + } +}); + + // Delete a thought app.delete("/thoughts/id/:id", async (req, res) => { const id = req.params.id; @@ -109,10 +125,10 @@ app.delete("/thoughts/id/:id", async (req, res) => { }); -// Update the message of a thought -app.patch("/thoughts/id/:id", async (req, res) => { +// Update the like count of a thought +app.patch("/thoughts/id/:id/like", async (req, res) => { const { id } = req.params; - const { message } = req.body; + const { hearts } = req.body; // Error handling for invalid id input if (!mongoose.Types.ObjectId.isValid(id)) { @@ -122,8 +138,8 @@ app.patch("/thoughts/id/:id", async (req, res) => { try { const updatedThought = await Thought.findByIdAndUpdate( id, - { message }, - { new: true, runValidators: true} //Ensures the updated message gets returned, and that schema validation also is performed on the new message + { hearts }, + { new: true, runValidators: true} //Ensures the updated heart count gets returned, and that schema validation also is performed on the new message ); // Error handling for no ID match @@ -139,17 +155,32 @@ app.patch("/thoughts/id/:id", async (req, res) => { }); -app.post("/thoughts", async (req, res) => { - // Retrieve the information sent by the client to our API endpoint - const message = req.body.message; - // Use our mongoose model to create the database entry - const thought = new Thought({ message }); +// Update the message of a thought +app.patch("/thoughts/id/:id", async (req, res) => { + const { id } = req.params; + const { message } = req.body; + + // Error handling for invalid id input + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ error: `Invalid id: ${id}` }); + } try { - const savedThought = await thought.save(); - res.status(200).json(savedThought); + const updatedThought = await Thought.findByIdAndUpdate( + id, + { message }, + { new: true, runValidators: true} //Ensures the updated message gets returned, and that schema validation also is performed on the new message + ); + + // Error handling for no ID match + if(!updatedThought) { + return res.status(404).json({error: `Thought with id ${id} not found`}); + } + + res.json(updatedThought); + } catch(err) { - res.status(400).json({message: "Failed to save thought to database", error: err.errors}); + res.status(500).json({error: err.message}); } }); From aa1bb14f3cd2fdf7e42d4638342b09712071c44c Mon Sep 17 00:00:00 2001 From: Gabriella Berkowicz Date: Mon, 26 Jan 2026 19:12:26 +0100 Subject: [PATCH 16/39] add editToken in schema to allow edit rights only for the message creator --- models/Thought.js | 6 +++++- server.js | 10 +++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/models/Thought.js b/models/Thought.js index 31be837..43a5777 100644 --- a/models/Thought.js +++ b/models/Thought.js @@ -1,6 +1,6 @@ import mongoose from "mongoose"; -const ThoughtSchema = mongoose.Schema({ +const ThoughtSchema = new mongoose.Schema({ message: { type: String, required: true, @@ -14,6 +14,10 @@ const ThoughtSchema = mongoose.Schema({ createdAt: { type: Date, default: Date.now + }, + editToken: { + type: String, + default: () => crypto.randomUUID() } }); diff --git a/server.js b/server.js index d50445c..d6d64c1 100644 --- a/server.js +++ b/server.js @@ -79,7 +79,11 @@ app.get("/thoughts", async (req, res) => { sortCriteria.createdAt = -1; // Creation date as default sorting } - const thoughts = await Thought.find(filterCriteria).sort(sortCriteria); + const thoughts = await Thought + .find(filterCriteria) + .sort(sortCriteria) + .select("-editToken") // to exclude editToken from being exposed to users + ; res.json(thoughts); }); @@ -93,9 +97,9 @@ app.post("/thoughts", async (req, res) => { try { const savedThought = await thought.save(); - res.status(200).json(savedThought); + res.status(201).json(savedThought); } catch(err) { - res.status(400).json({message: "Failed to save thought to database", error: err.errors}); + res.status(400).json({message: "Failed to save thought to database", error: err.message}); } }); From fc5cf2952a9e4bfa21023ebc3eb753604a8dabc3 Mon Sep 17 00:00:00 2001 From: Gabriella Berkowicz Date: Tue, 27 Jan 2026 13:26:03 +0100 Subject: [PATCH 17/39] update route name --- server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server.js b/server.js index d6d64c1..8b4ec3e 100644 --- a/server.js +++ b/server.js @@ -160,7 +160,7 @@ app.patch("/thoughts/id/:id/like", async (req, res) => { // Update the message of a thought -app.patch("/thoughts/id/:id", async (req, res) => { +app.patch("/thoughts/id/:id/message", async (req, res) => { const { id } = req.params; const { message } = req.body; From aab1e6539289ed01c49242b6ec043bd63e3b1c8b Mon Sep 17 00:00:00 2001 From: Gabriella Berkowicz Date: Tue, 27 Jan 2026 15:43:45 +0100 Subject: [PATCH 18/39] change minlength of message in schema --- models/Thought.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/Thought.js b/models/Thought.js index 43a5777..e98d977 100644 --- a/models/Thought.js +++ b/models/Thought.js @@ -4,7 +4,7 @@ const ThoughtSchema = new mongoose.Schema({ message: { type: String, required: true, - minlength: 2, + minlength: 1, maxlength: 140 }, hearts: { From 64239239945440954dda09964bb265e65a333175 Mon Sep 17 00:00:00 2001 From: Gabriella Berkowicz Date: Thu, 29 Jan 2026 14:36:58 +0100 Subject: [PATCH 19/39] add user model + create routes for sign-up and login + create function for authenticating a user that can be used in an authorized only route --- models/User.js | 31 ++++++++++++++++++++ package.json | 1 + server.js | 79 ++++++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 99 insertions(+), 12 deletions(-) create mode 100644 models/User.js diff --git a/models/User.js b/models/User.js new file mode 100644 index 0000000..969d2e7 --- /dev/null +++ b/models/User.js @@ -0,0 +1,31 @@ +import mongoose from "mongoose"; +import crypto from "crypto"; + +const UserSchema = new mongoose.Schema({ + name: { + type: String, + unique: true, + required: true, + minlength: 2, + maxlength: 50 + }, + email: { + type: String, + unique: true, + required: true, + minlength: 6, + maxlength: 254 + }, + password: { + type: String, + required: true, + minlength: 8, + maxlength: 64 + }, + accessToken : { + type: String, + default: () => crypto.randomBytes(128).toString("hex") + } +}); + +export default mongoose.model("User", UserSchema); \ No newline at end of file diff --git a/package.json b/package.json index 14214eb..48e2d20 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "@babel/core": "^7.17.9", "@babel/node": "^7.16.8", "@babel/preset-env": "^7.16.11", + "bcrypt-nodejs": "^0.0.3", "cors": "^2.8.5", "dotenv": "^17.2.3", "express": "^4.17.3", diff --git a/server.js b/server.js index 8b4ec3e..188d4cd 100644 --- a/server.js +++ b/server.js @@ -3,15 +3,29 @@ import express from "express"; import expressListEndpoints from "express-list-endpoints"; import mongoose from "mongoose"; import Thought from "./models/Thought"; +import User from "./models/User"; import { seedDatabase } from "./seedDatabase"; import dotenv from "dotenv"; dotenv.config(); +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(); +// To be used in routes that should only be accessed by authorized users +const authenticateUser = async (req, res, next) => { + const user = await User.findOne({ accessToken: req.header("Authorization") }); + if(user) { + req.user = user; + next(); // Continue on executing what comes after + } else { + res.status(401).json({ loggedOut: true }); + } +}; + + // Add middlewares to enable cors and json body parsing app.use(cors()); app.use(express.json()); @@ -22,7 +36,7 @@ app.use((req, res, next) => { if(mongoose.connection.readyState === 1) { // 1 is connected next(); // Continue on executing what comes after } else { - res.status(503).json({error: "Service unavailable"}); + res.status(503).json({ error: "Service unavailable" }); } }); @@ -42,7 +56,7 @@ app.get("/", (req, res) => { app.get("/thoughts", async (req, res) => { /* --- Functionality for filtering --- */ - const filterCriteria = {}; // To use as argument in Model.find(). Will be a criteria or object (thus retrieving all thoughts) + const filterCriteria = {}; // To use as argument in Model.find(). Will be a criteria or empty object (thus retrieving all thoughts) const { fromDate, minLikes } = req.query; //Filter on minimum of likes @@ -90,16 +104,15 @@ app.get("/thoughts", async (req, res) => { // Post a thought app.post("/thoughts", async (req, res) => { - // Retrieve the information sent by the client to our API endpoint const message = req.body.message; - // Use our mongoose model to create the database entry + // Use mongoose model to create a database entry const thought = new Thought({ message }); try { const savedThought = await thought.save(); res.status(201).json(savedThought); - } catch(err) { - res.status(400).json({message: "Failed to save thought to database", error: err.message}); + } catch(error) { + res.status(400).json({ message: "Failed to save thought to database", error: error.message }); } }); @@ -118,13 +131,13 @@ app.delete("/thoughts/id/:id", async (req, res) => { // Error handling for no ID match if(!deletedThought) { - return res.status(404).json({error: `Thought with id ${id} not found`}); + return res.status(404).json({ error: `Thought with id ${id} not found` }); } res.json(deletedThought); - } catch(err) { - res.status(500).json({error: err.message}); + } catch(error) { + res.status(500).json({error: error.message}); } }); @@ -148,13 +161,13 @@ app.patch("/thoughts/id/:id/like", async (req, res) => { // Error handling for no ID match if(!updatedThought) { - return res.status(404).json({error: `Thought with id ${id} not found`}); + return res.status(404).json({ error: `Thought with id ${id} not found` }); } res.json(updatedThought); - } catch(err) { - res.status(500).json({error: err.message}); + } catch(error) { + res.status(500).json({ error: error.message }); } }); @@ -189,6 +202,48 @@ app.patch("/thoughts/id/:id/message", async (req, res) => { }); +// Create a new user (sign-up) +app.post("/users", async (req, res) => { + try { + const { name, email, password } = req.body; + // Use mongoose model to create a database entry + const salt = bcrypt.genSaltSync(); + const user = new User({ name, email, password: bcrypt.hashSync(password, salt) }); + const savedUser = await user.save(); + + res.status(201).json({ + success: true, + message: "User created", + id: user._id, + accessToken: user.accessToken + }); + } catch(error) { + res.status(400).json({ + success: false, + message: "Failed to create user", + error: error.errors}); + } +}); + + +// Login +app.post("/sessions", async (req, res) => { + try{ + const { email, password } = req.body; + const user = await User.findOne({email: email}); + + if(!user || !bcrypt.compareSync(password, user.password)) { + return res.status(401).json({ error: "Invalid user credentials" }); + } + + res.json({ userId: user._id, accessToken: user.accessToken }); + + } catch(error) { + res.status(500).json({ error: "Server error" }); + } +}); + + /* --- Connect to Mongo --- */ const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/thoughts"; mongoose.connect(mongoUrl) From c955e246f698a1b005d695641f243c6d256f7fe8 Mon Sep 17 00:00:00 2001 From: Gabriella Berkowicz Date: Thu, 29 Jan 2026 17:40:11 +0100 Subject: [PATCH 20/39] remove unique criteria from name in user schema --- models/User.js | 1 - 1 file changed, 1 deletion(-) diff --git a/models/User.js b/models/User.js index 969d2e7..4d22652 100644 --- a/models/User.js +++ b/models/User.js @@ -4,7 +4,6 @@ import crypto from "crypto"; const UserSchema = new mongoose.Schema({ name: { type: String, - unique: true, required: true, minlength: 2, maxlength: 50 From 706771e479266f8ce0ea245eab4ecd565623dacc Mon Sep 17 00:00:00 2001 From: Gabriella Berkowicz Date: Fri, 30 Jan 2026 08:26:40 +0100 Subject: [PATCH 21/39] remove length limitations on password in user schema --- models/User.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/models/User.js b/models/User.js index 4d22652..c7b740b 100644 --- a/models/User.js +++ b/models/User.js @@ -17,9 +17,7 @@ const UserSchema = new mongoose.Schema({ }, password: { type: String, - required: true, - minlength: 8, - maxlength: 64 + required: true }, accessToken : { type: String, From 9b9c36731531fc0a10097cfabf759eba98340abf Mon Sep 17 00:00:00 2001 From: Gabriella Berkowicz Date: Fri, 30 Jan 2026 09:36:15 +0100 Subject: [PATCH 22/39] remove all length limitations in user schema --- models/User.js | 8 ++------ server.js | 6 ++++++ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/models/User.js b/models/User.js index c7b740b..6383d2d 100644 --- a/models/User.js +++ b/models/User.js @@ -4,16 +4,12 @@ import crypto from "crypto"; const UserSchema = new mongoose.Schema({ name: { type: String, - required: true, - minlength: 2, - maxlength: 50 + required: true }, email: { type: String, unique: true, - required: true, - minlength: 6, - maxlength: 254 + required: true }, password: { type: String, diff --git a/server.js b/server.js index 188d4cd..4c246ec 100644 --- a/server.js +++ b/server.js @@ -218,6 +218,12 @@ app.post("/users", async (req, res) => { accessToken: user.accessToken }); } catch(error) { + console.error("FULL ERROR:", error); + console.error("ERROR NAME:", error.name); + console.error("ERROR MESSAGE:", error.message); + console.error("ERROR CODE:", error.code); + console.error("ERROR ERRORS:", error.errors); + res.status(400).json({ success: false, message: "Failed to create user", From 3fdebaa66b1d4cb4f074647807ab8dd59f3e0740 Mon Sep 17 00:00:00 2001 From: Gabriella Berkowicz Date: Fri, 30 Jan 2026 12:01:55 +0100 Subject: [PATCH 23/39] add user name in the server response in login and signup route --- server.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/server.js b/server.js index 4c246ec..b8c1105 100644 --- a/server.js +++ b/server.js @@ -215,15 +215,10 @@ app.post("/users", async (req, res) => { success: true, message: "User created", id: user._id, - accessToken: user.accessToken + accessToken: user.accessToken, + name: user.name }); } catch(error) { - console.error("FULL ERROR:", error); - console.error("ERROR NAME:", error.name); - console.error("ERROR MESSAGE:", error.message); - console.error("ERROR CODE:", error.code); - console.error("ERROR ERRORS:", error.errors); - res.status(400).json({ success: false, message: "Failed to create user", @@ -242,7 +237,11 @@ app.post("/sessions", async (req, res) => { return res.status(401).json({ error: "Invalid user credentials" }); } - res.json({ userId: user._id, accessToken: user.accessToken }); + res.json({ + userId: user._id, + accessToken: user.accessToken, + name: user.name + }); } catch(error) { res.status(500).json({ error: "Server error" }); From c5ab35c130a9440e07c735c1ab44a55d7342a976 Mon Sep 17 00:00:00 2001 From: Gabriella Berkowicz Date: Fri, 30 Jan 2026 14:48:01 +0100 Subject: [PATCH 24/39] add userId property in Thought schema for logged in users creating a thought --- models/Thought.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/models/Thought.js b/models/Thought.js index e98d977..986670c 100644 --- a/models/Thought.js +++ b/models/Thought.js @@ -18,6 +18,11 @@ const ThoughtSchema = new mongoose.Schema({ editToken: { type: String, default: () => crypto.randomUUID() + }, + // For logged-in users: + userId: { + type: mongoose.Schema.Types.ObjectId, + default: null } }); From 7238ae67dde722f51b37deecbaafee77aca6a9a1 Mon Sep 17 00:00:00 2001 From: Gabriella Berkowicz Date: Fri, 30 Jan 2026 17:40:57 +0100 Subject: [PATCH 25/39] add userId to each created message of the matching user to the accessToken sent in the request headers --- server.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/server.js b/server.js index b8c1105..32ccd1d 100644 --- a/server.js +++ b/server.js @@ -96,7 +96,7 @@ app.get("/thoughts", async (req, res) => { const thoughts = await Thought .find(filterCriteria) .sort(sortCriteria) - .select("-editToken") // to exclude editToken from being exposed to users + .select("-editToken -userId"); // to exclude editToken & userId from being exposed to users ; res.json(thoughts); }); @@ -104,12 +104,19 @@ app.get("/thoughts", async (req, res) => { // Post a thought app.post("/thoughts", async (req, res) => { - const message = req.body.message; - // Use mongoose model to create a database entry - const thought = new Thought({ message }); - try { - const savedThought = await thought.save(); + const message = req.body.message; + const accessToken = req.headers.authorization; + + const matchingUser = await User.findOne({ accessToken: accessToken }); + + // Use mongoose model to create a database entry + const newThought = new Thought({ + message, + userId: matchingUser ? matchingUser._id : null + }); + + const savedThought = await newThought.save(); res.status(201).json(savedThought); } catch(error) { res.status(400).json({ message: "Failed to save thought to database", error: error.message }); From b6e0887a565c7bd8bc6fd7379a09537154dc284e Mon Sep 17 00:00:00 2001 From: Gabriella Berkowicz Date: Mon, 2 Feb 2026 08:49:59 +0100 Subject: [PATCH 26/39] use middleware to attach user info to the request if there is an authorization header + use it to add an isCreator field to each thought --- server.js | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/server.js b/server.js index 32ccd1d..da38ac9 100644 --- a/server.js +++ b/server.js @@ -30,6 +30,22 @@ const authenticateUser = async (req, res, next) => { app.use(cors()); app.use(express.json()); +// Middleware for authentication +// If there is an accessToken from a logged in user in the request header, find matching user and attach it to the request +app.use(async (req, res, next) => { + const authHeader = req.headers.authorization; + + if (authHeader) { + const accessToken = authHeader.accessToken; + const matchingUser = await User.findOne({ accessToken: accessToken }); + if (matchingUser) { + req.user = matchingUser + } + } + + next(); +}); + /* --- Error handling to check database connection --- */ app.use((req, res, next) => { @@ -96,9 +112,17 @@ app.get("/thoughts", async (req, res) => { const thoughts = await Thought .find(filterCriteria) .sort(sortCriteria) - .select("-editToken -userId"); // to exclude editToken & userId from being exposed to users + .select("-editToken -userId"); // To exclude editToken & userId from being exposed to users ; - res.json(thoughts); + res.json( + thoughts.map((thought) => ({ + ...thought.toObject(), // Convert to JS object (because of Mongoose) + isCreator: req.user && thought.userId?.equals(req.user._id) // For determining edit rights + } + + )) + + ); }); From eb91d5e8d6ed0e117307906e30d4e49bbed05e8e Mon Sep 17 00:00:00 2001 From: Gabriella Berkowicz Date: Mon, 2 Feb 2026 09:08:06 +0100 Subject: [PATCH 27/39] remove code duplicates that should only be in middleware and not post route --- server.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/server.js b/server.js index da38ac9..388d236 100644 --- a/server.js +++ b/server.js @@ -33,10 +33,9 @@ app.use(express.json()); // Middleware for authentication // If there is an accessToken from a logged in user in the request header, find matching user and attach it to the request app.use(async (req, res, next) => { - const authHeader = req.headers.authorization; + const accessToken = req.headers.authorization; - if (authHeader) { - const accessToken = authHeader.accessToken; + if (accessToken) { const matchingUser = await User.findOne({ accessToken: accessToken }); if (matchingUser) { req.user = matchingUser @@ -130,20 +129,20 @@ app.get("/thoughts", async (req, res) => { app.post("/thoughts", async (req, res) => { try { const message = req.body.message; - const accessToken = req.headers.authorization; - - const matchingUser = await User.findOne({ accessToken: accessToken }); // Use mongoose model to create a database entry const newThought = new Thought({ message, - userId: matchingUser ? matchingUser._id : null + userId: req.user ? req.user._id : null }); const savedThought = await newThought.save(); res.status(201).json(savedThought); } catch(error) { - res.status(400).json({ message: "Failed to save thought to database", error: error.message }); + res.status(400).json({ + message: "Failed to save thought to database", + error: error.message + }); } }); From fbc419b05187cd6578652bbe1c21db394a571289 Mon Sep 17 00:00:00 2001 From: Gabriella Berkowicz Date: Mon, 2 Feb 2026 09:47:38 +0100 Subject: [PATCH 28/39] add temporary logs --- server.js | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/server.js b/server.js index 388d236..33fc97f 100644 --- a/server.js +++ b/server.js @@ -33,16 +33,23 @@ app.use(express.json()); // Middleware for authentication // If there is an accessToken from a logged in user in the request header, find matching user and attach it to the request app.use(async (req, res, next) => { - const accessToken = req.headers.authorization; + try { + const accessToken = req.headers.authorization; - if (accessToken) { - const matchingUser = await User.findOne({ accessToken: accessToken }); - if (matchingUser) { - req.user = matchingUser - } - } + if (accessToken) { + const matchingUser = await User.findOne({ accessToken: accessToken }); + if (matchingUser) { + req.user = matchingUser + } + } - next(); + console.log("AUTH HEADER:", req.headers.authorization); // Remove - temporary logging + console.log("REQ.USER:", req.user); // Remove - temporary logging + next(); + } catch(error) { + console.error("Authentication middleware error:", error) + next(); // Prevent blocking + } }); @@ -117,11 +124,9 @@ app.get("/thoughts", async (req, res) => { thoughts.map((thought) => ({ ...thought.toObject(), // Convert to JS object (because of Mongoose) isCreator: req.user && thought.userId?.equals(req.user._id) // For determining edit rights - } - - )) - + })) ); + console.log("Thoughts:", thoughts); // Remove - temporary logging }); @@ -137,6 +142,8 @@ app.post("/thoughts", async (req, res) => { }); const savedThought = await newThought.save(); + console.log("POST req.user:", req.user); // Remove - temporary logging + res.status(201).json(savedThought); } catch(error) { res.status(400).json({ From d44b8709eb08e9f7bad1bece056eeb1ffe0f75de Mon Sep 17 00:00:00 2001 From: Gabriella Berkowicz Date: Mon, 2 Feb 2026 10:10:58 +0100 Subject: [PATCH 29/39] add more temporary logs --- server.js | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/server.js b/server.js index 33fc97f..ec7ebe0 100644 --- a/server.js +++ b/server.js @@ -43,9 +43,8 @@ app.use(async (req, res, next) => { } } - console.log("AUTH HEADER:", req.headers.authorization); // Remove - temporary logging - console.log("REQ.USER:", req.user); // Remove - temporary logging next(); + } catch(error) { console.error("Authentication middleware error:", error) next(); // Prevent blocking @@ -120,19 +119,36 @@ app.get("/thoughts", async (req, res) => { .sort(sortCriteria) .select("-editToken -userId"); // To exclude editToken & userId from being exposed to users ; + + // Remove - temporary logging: + thoughts.forEach(thought => { + console.log("thought.userId:", thought.userId); + console.log("req.user:", req.user); + console.log("req.user._id:", req.user?._id); + console.log( + "ID comparison:", + req.user + ? thought.userId?.equals(req.user._id) + : "failed to compare" + ); + }); + res.json( thoughts.map((thought) => ({ ...thought.toObject(), // Convert to JS object (because of Mongoose) isCreator: req.user && thought.userId?.equals(req.user._id) // For determining edit rights })) ); - console.log("Thoughts:", thoughts); // Remove - temporary logging }); // Post a thought app.post("/thoughts", async (req, res) => { try { + + console.log("POST /thoughts auth header:", req.headers.authorization); // Remove - temporary logging + console.log("POST /thoughts req.user:", req.user); // Remove - temporary logging + console.log("POST req.user:", req.user); // Remove - temporary logging const message = req.body.message; // Use mongoose model to create a database entry @@ -142,7 +158,6 @@ app.post("/thoughts", async (req, res) => { }); const savedThought = await newThought.save(); - console.log("POST req.user:", req.user); // Remove - temporary logging res.status(201).json(savedThought); } catch(error) { From 5bdd5563099721b8070356e3745b388a1e927314 Mon Sep 17 00:00:00 2001 From: Gabriella Berkowicz Date: Mon, 2 Feb 2026 11:17:45 +0100 Subject: [PATCH 30/39] fix bug (caused by removal of userId to early in GET route) so that isCreator can be computed on an object that includes the uderId --- server.js | 32 +++++++++----------------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/server.js b/server.js index ec7ebe0..99851e5 100644 --- a/server.js +++ b/server.js @@ -117,38 +117,24 @@ app.get("/thoughts", async (req, res) => { const thoughts = await Thought .find(filterCriteria) .sort(sortCriteria) - .select("-editToken -userId"); // To exclude editToken & userId from being exposed to users + .select("-editToken"); // To exclude editToken from being exposed to users ; - - // Remove - temporary logging: - thoughts.forEach(thought => { - console.log("thought.userId:", thought.userId); - console.log("req.user:", req.user); - console.log("req.user._id:", req.user?._id); - console.log( - "ID comparison:", - req.user - ? thought.userId?.equals(req.user._id) - : "failed to compare" - ); - }); - res.json( - thoughts.map((thought) => ({ - ...thought.toObject(), // Convert to JS object (because of Mongoose) + const result = thoughts.map((thought) => { + const thoughtObj = thought.toObject(); // Convert to JS object (because of Mongoose) + delete thoughtObj.userId; // remove userId to be on front-end + return { + ...thoughtObj, isCreator: req.user && thought.userId?.equals(req.user._id) // For determining edit rights - })) - ); + } + }); + res.json(result); }); // Post a thought app.post("/thoughts", async (req, res) => { try { - - console.log("POST /thoughts auth header:", req.headers.authorization); // Remove - temporary logging - console.log("POST /thoughts req.user:", req.user); // Remove - temporary logging - console.log("POST req.user:", req.user); // Remove - temporary logging const message = req.body.message; // Use mongoose model to create a database entry From b60925d96497bbe6e9b5b34c9573c3504cd07cbf Mon Sep 17 00:00:00 2001 From: Gabriella Berkowicz Date: Mon, 2 Feb 2026 13:55:34 +0100 Subject: [PATCH 31/39] =?UTF-8?q?change=20hearts=20in=20thought=20schema?= =?UTF-8?q?=20to=20an=20array=20of=20id's=20and=20update=20the=20patch=20r?= =?UTF-8?q?oute=20for=20updating=20likes=20according=20the=20new=20schema?= =?UTF-8?q?=20+=20create=20authenticated=20route=20for=20a=20user=C2=B4s?= =?UTF-8?q?=20liked=20thoughts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- models/Thought.js | 9 ++--- server.js | 90 +++++++++++++++++++++++++++++------------------ 2 files changed, 61 insertions(+), 38 deletions(-) diff --git a/models/Thought.js b/models/Thought.js index 986670c..f82eb3f 100644 --- a/models/Thought.js +++ b/models/Thought.js @@ -7,10 +7,11 @@ const ThoughtSchema = new mongoose.Schema({ minlength: 1, maxlength: 140 }, - hearts: { - type: Number, - default: 0 - }, + hearts: [ + { + userId: { type: mongoose.Schema.Types.ObjectId, default: null } + } + ], createdAt: { type: Date, default: Date.now diff --git a/server.js b/server.js index 99851e5..135e291 100644 --- a/server.js +++ b/server.js @@ -76,21 +76,22 @@ app.get("/", (req, res) => { // All thoughts app.get("/thoughts", async (req, res) => { - /* --- Functionality for filtering --- */ + try { + /* --- Functionality for filtering --- */ const filterCriteria = {}; // To use as argument in Model.find(). Will be a criteria or empty object (thus retrieving all thoughts) const { fromDate, minLikes } = req.query; //Filter on minimum of likes - if(minLikes){ + if (minLikes) { filterCriteria.hearts = { $gte: Number(minLikes) }; //gte = greater than or equal to } // Filter from a date - if(fromDate) { - filterCriteria.createdAt = { $gte: new Date(fromDate) }; + if (fromDate) { + filterCriteria.createdAt = { $gte: new Date(fromDate) }; } - /* --- Functionality for sorting --- */ + /* --- Functionality for sorting --- */ const sortCriteria = {}; const { sortBy, order } = req.query; const sortingOrder = order === "asc" ? 1 : -1; @@ -104,9 +105,9 @@ app.get("/thoughts", async (req, res) => { sort = "hearts"; } - if(sort){ - // Set the key-value pair in the object sortCriteria dynamically - obj[key] = value - sortCriteria[sort] = sortingOrder; // Set the key to the value of sort and its value to sortingOrder + if (sort) { + // Set the key-value pair in the object sortCriteria dynamically - obj[key] = value + sortCriteria[sort] = sortingOrder; // Set the key to the value of sort and its value to sortingOrder if (sort !== "createdAt") { sortCriteria.createdAt = -1; // Puts creation date as secondary sorting } @@ -114,21 +115,25 @@ app.get("/thoughts", async (req, res) => { sortCriteria.createdAt = -1; // Creation date as default sorting } - const thoughts = await Thought - .find(filterCriteria) - .sort(sortCriteria) - .select("-editToken"); // To exclude editToken from being exposed to users - ; - - const result = thoughts.map((thought) => { - const thoughtObj = thought.toObject(); // Convert to JS object (because of Mongoose) - delete thoughtObj.userId; // remove userId to be on front-end - return { - ...thoughtObj, - isCreator: req.user && thought.userId?.equals(req.user._id) // For determining edit rights - } - }); - res.json(result); + const thoughts = await Thought + .find(filterCriteria) + .sort(sortCriteria) + .select("-editToken"); // To exclude editToken from being exposed to users + ; + + const result = thoughts.map((thought) => { + const thoughtObj = thought.toObject(); // Convert to JS object (because of Mongoose) + delete thoughtObj.userId; // remove userId to be on front-end + return { + ...thoughtObj, + isCreator: req.user && thought.userId?.equals(req.user._id) // For determining edit rights (computed on thought and not thoughtObj that has the uderId removed) + } + }); + res.json(result); + } catch (error) { + console.error("GET /thoughts error:", error); + res.status(500).json({ message: "Failed to fetch thoughts", error: error.message }); + } }); @@ -182,19 +187,17 @@ app.delete("/thoughts/id/:id", async (req, res) => { // Update the like count of a thought app.patch("/thoughts/id/:id/like", async (req, res) => { - const { id } = req.params; - const { hearts } = req.body; - - // Error handling for invalid id input - if (!mongoose.Types.ObjectId.isValid(id)) { - return res.status(400).json({ error: `Invalid id: ${id}` }); - } - try { + const { id } = req.params; + + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ error: `Invalid id: ${id}` }); + } + const updatedThought = await Thought.findByIdAndUpdate( - id, - { hearts }, - { new: true, runValidators: true} //Ensures the updated heart count gets returned, and that schema validation also is performed on the new message + id, + { $push: { hearts: { userId: req.user ? req.user._id : null } } }, //Ensures the updated heart count gets returned, and that schema validation also is performed + { new: true, runValidators: true } ); // Error handling for no ID match @@ -287,6 +290,25 @@ app.post("/sessions", async (req, res) => { }); +/* --- Authenticated only routes ---*/ + + +// Liked thoughts +app.get("/thoughts/liked", authenticateUser, async (req, res) => { + try { + const likedThoughts = await Thought + .find({ "hearts.userId": req.user._id }) + .sort({ createdAt: -1 }); + + res.json(likedThoughts); + + } catch (error) { + console.error("GET /thoughts error:", error); + res.status(500).json({ message: "Failed to fetch liked thoughts", error: error.message }); + } +}); + + /* --- Connect to Mongo --- */ const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/thoughts"; mongoose.connect(mongoUrl) From 8339ebc434391ad60220fd05bc3000470e4361ae Mon Sep 17 00:00:00 2001 From: Gabriella Berkowicz Date: Mon, 2 Feb 2026 14:55:53 +0100 Subject: [PATCH 32/39] update authenticateUser for better error handling --- server.js | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/server.js b/server.js index 135e291..6c54119 100644 --- a/server.js +++ b/server.js @@ -14,17 +14,6 @@ import bcrypt from "bcrypt-nodejs"; const port = process.env.PORT || 8080; const app = express(); -// To be used in routes that should only be accessed by authorized users -const authenticateUser = async (req, res, next) => { - const user = await User.findOne({ accessToken: req.header("Authorization") }); - if(user) { - req.user = user; - next(); // Continue on executing what comes after - } else { - res.status(401).json({ loggedOut: true }); - } -}; - // Add middlewares to enable cors and json body parsing app.use(cors()); @@ -52,6 +41,31 @@ app.use(async (req, res, next) => { }); +// To be used in routes that should only be accessed by authorized users +const authenticateUser = async (req, res, next) => { + try { + const accessToken = req.header("Authorization"); + + if (!accessToken) { + return res.status(401).json({ loggedOut: true }); + } + + const user = await User.findOne({ accessToken }); + + if (!user) { + return res.status(401).json({ loggedOut: true }); + } + + req.user = user; + next(); // Continue on executing what comes after + + } catch (error) { + console.error("Auth middleware error:", error); + res.status(500).json({ error: "Authentication failed" }); + } +}; + + /* --- Error handling to check database connection --- */ app.use((req, res, next) => { if(mongoose.connection.readyState === 1) { // 1 is connected From 990688a8c2f3e540698183d16248eb29808dd334 Mon Sep 17 00:00:00 2001 From: Gabriella Berkowicz Date: Mon, 2 Feb 2026 17:28:29 +0100 Subject: [PATCH 33/39] fix bug with filtering on hearts due to change in schema --- seedDatabase.js | 22 +++++++++++----------- server.js | 35 ++++++++++------------------------- 2 files changed, 21 insertions(+), 36 deletions(-) diff --git a/seedDatabase.js b/seedDatabase.js index c4965dc..29b0f0a 100644 --- a/seedDatabase.js +++ b/seedDatabase.js @@ -5,17 +5,17 @@ export const seedDatabase = async () => { /* --- Using the models created to add data --- */ - await new Thought({ message: "Berlin baby", hearts: 37, createdAt: "2025-05-19T22:07:08.999Z" }).save(); + await new Thought({ message: "Berlin baby", hearts: [{ userId: null }, { userId: null }, { userId: null }, { userId: null }, { userId: null }, { userId: null }, { userId: null }, { userId: null }, { userId: null }, { userId: null }], createdAt: "2025-05-19T22:07:08.999Z" }).save(); await new Thought({ message: "My family!", createdAt: "2025-05-22T22:29:32.232Z" }).save(); - await new Thought({ message: "The smell of coffee in the morning....", hearts: 23, createdAt: "2025-05-22T22:11:16.075Z" }).save(); - await new Thought({ message: "Newly washed bedlinen, kids that sleeps through the night.. FINGERS CROSSED 🤞🏼\n", hearts: 6, createdAt: "2025-05-21T21:42:23.862Z" }).save(); - await new Thought({ message: "I am happy that I feel healthy and have energy again", hearts: 13, createdAt: "2025-05-21T21:28:32.196Z" }).save(); - await new Thought({ message: "Cold beer", hearts: 2, createdAt: "2025-05-21T19:05:34.113Z" }).save(); - await new Thought({ message: "My friend is visiting this weekend! <3", hearts: 6, createdAt: "2025-05-21T18:59:58.121Z" }).save(); - await new Thought({ message: "A good joke: \nWhy did the scarecrow win an award?\nBecause he was outstanding in his field!", hearts: 12, createdAt: "2025-05-20T20:54:51.082Z" }).save(); - await new Thought({ message: "Tacos and tequila🌮🍹", hearts: 2, createdAt: "2025-05-19T20:53:18.899Z" }).save(); - await new Thought({ message: "Netflix and late night ice-cream🍦", hearts: 1, createdAt: "2025-05-18T20:51:34.494Z" }).save(); - await new Thought({ message: "The weather is nice!", hearts: 2, createdAt: "2025-05-20T15:03:22.379Z" }).save(); + await new Thought({ message: "The smell of coffee in the morning....", hearts: [{ userId: null }, { userId: null }, { userId: null }, { userId: null }, { userId: null }, { userId: null } ], createdAt: "2025-05-22T22:11:16.075Z" }).save(); + await new Thought({ message: "Newly washed bedlinen, kids that sleeps through the night.. FINGERS CROSSED 🤞🏼\n", hearts: [{ userId: null }, { userId: null }, { userId: null }, { userId: null }], createdAt: "2025-05-21T21:42:23.862Z" }).save(); + await new Thought({ message: "I am happy that I feel healthy and have energy again", hearts: [{ userId: null }, { userId: null }, { userId: null }, { userId: null }], createdAt: "2025-05-21T21:28:32.196Z" }).save(); + await new Thought({ message: "Cold beer", hearts: [{ userId: null }, { userId: null }, { userId: null }, { userId: null }, { userId: null }, { userId: null }, { userId: null }, { userId: null }, { userId: null }, { userId: null }, { userId: null }, { userId: null }], createdAt: "2025-05-21T19:05:34.113Z" }).save(); + await new Thought({ message: "My friend is visiting this weekend! <3", hearts: [{ userId: null }, { userId: null }, { userId: null }, { userId: null }, { userId: null }, { userId: null }, { userId: null }], createdAt: "2025-05-21T18:59:58.121Z" }).save(); + await new Thought({ message: "A good joke: \nWhy did the scarecrow win an award?\nBecause he was outstanding in his field!", hearts: [{ userId: null }, { userId: null }, { userId: null }, { userId: null }, { userId: null }, { userId: null }, { userId: null }], createdAt: "2025-05-20T20:54:51.082Z" }).save(); + await new Thought({ message: "Tacos and tequila🌮🍹", hearts: [{ userId: null }, { userId: null }], createdAt: "2025-05-19T20:53:18.899Z" }).save(); + await new Thought({ message: "Netflix and late night ice-cream🍦", hearts: [{ userId: null }], createdAt: "2025-05-18T20:51:34.494Z" }).save(); + await new Thought({ message: "The weather is nice!", hearts: [{ userId: null }, { userId: null }], createdAt: "2025-05-20T15:03:22.379Z" }).save(); await new Thought({ message: "Summer is coming...", createdAt: "2025-05-20T11:58:29.662Z" }).save(); - await new Thought({ message: "good vibes and good things", hearts: 3, createdAt: "2025-05-20T03:57:40.322Z" }).save(); + await new Thought({ message: "good vibes and good things", hearts: [{ userId: null }, { userId: null }, { userId: null }], createdAt: "2025-05-20T03:57:40.322Z" }).save(); }; \ No newline at end of file diff --git a/server.js b/server.js index 6c54119..3478850 100644 --- a/server.js +++ b/server.js @@ -25,8 +25,13 @@ app.use(async (req, res, next) => { try { const accessToken = req.headers.authorization; + if (!accessToken) { + return next(); + } + if (accessToken) { const matchingUser = await User.findOne({ accessToken: accessToken }); + if (matchingUser) { req.user = matchingUser } @@ -42,27 +47,12 @@ app.use(async (req, res, next) => { // To be used in routes that should only be accessed by authorized users -const authenticateUser = async (req, res, next) => { - try { - const accessToken = req.header("Authorization"); - - if (!accessToken) { - return res.status(401).json({ loggedOut: true }); - } +const authenticateUser = (req, res, next) => { - const user = await User.findOne({ accessToken }); - - if (!user) { - return res.status(401).json({ loggedOut: true }); - } - - req.user = user; - next(); // Continue on executing what comes after - - } catch (error) { - console.error("Auth middleware error:", error); - res.status(500).json({ error: "Authentication failed" }); + if (!req.user) { + return res.status(401).json({ loggedOut: true }); } + next(); }; @@ -97,12 +87,7 @@ app.get("/thoughts", async (req, res) => { //Filter on minimum of likes if (minLikes) { - filterCriteria.hearts = { $gte: Number(minLikes) }; //gte = greater than or equal to - } - - // Filter from a date - if (fromDate) { - filterCriteria.createdAt = { $gte: new Date(fromDate) }; + filterCriteria.hearts.length = { $gte: Number(minLikes) }; //gte = greater than or equal to } /* --- Functionality for sorting --- */ From b2f0f30f251cd76eda4ed1f26bae7b9baa6c1e0e Mon Sep 17 00:00:00 2001 From: Gabriella Berkowicz Date: Mon, 2 Feb 2026 17:33:52 +0100 Subject: [PATCH 34/39] try another syntax for previous bug --- .vscode/settings.json | 3 +++ server.js | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..aef8443 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "liveServer.settings.port": 5501 +} \ No newline at end of file diff --git a/server.js b/server.js index 3478850..2c1b5f8 100644 --- a/server.js +++ b/server.js @@ -87,7 +87,7 @@ app.get("/thoughts", async (req, res) => { //Filter on minimum of likes if (minLikes) { - filterCriteria.hearts.length = { $gte: Number(minLikes) }; //gte = greater than or equal to + filterCriteria.$expr = { $gte: [{ $size: "$hearts" }, Number(minLikes)] }; //gte = greater than or equal to } /* --- Functionality for sorting --- */ From 891c8b170eb08e45b9d835061d8aa18dede8ab83 Mon Sep 17 00:00:00 2001 From: Gabriella Berkowicz Date: Tue, 3 Feb 2026 08:39:53 +0100 Subject: [PATCH 35/39] change filterCritera for minimum likes --- server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server.js b/server.js index 2c1b5f8..6279047 100644 --- a/server.js +++ b/server.js @@ -87,7 +87,7 @@ app.get("/thoughts", async (req, res) => { //Filter on minimum of likes if (minLikes) { - filterCriteria.$expr = { $gte: [{ $size: "$hearts" }, Number(minLikes)] }; //gte = greater than or equal to + filterCriteria[`hearts.${Number(minLikes) - 1}`] = { $exists: true }; //gte = greater than or equal to } /* --- Functionality for sorting --- */ From 4dfbb95fe6d4047a74a80f0b458d08d9bafd4c6e Mon Sep 17 00:00:00 2001 From: Gabriella Berkowicz Date: Tue, 3 Feb 2026 14:11:20 +0100 Subject: [PATCH 36/39] add readme --- README.md | 36 ++++++++++++++++++++++++++++++------ server.js | 4 +++- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 0f9f073..8d863fa 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,35 @@ -# Project API +# Happy Thoughts 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. +This repository contains the backend API for Happy Thoughts, built with Node.js, Express, and MongoDB. The API handles authentication, authorization, data validation, and all CRUD operations for thoughts and users. -## Getting started +The API is fully RESTful and deployed to Render. -Install dependencies with `npm install`, then start the server by running `npm run dev` +## Live Site: https://happysharing.netlify.app/ -## 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. +## Features + +- User authentication (sign up & login) +- Password hashing with bcrypt +- Token-based authorization +- Create, read, update & delete thoughts +- Allow anonymous posting +- Like thoughts (authenticated & anonymous) +- Track which users liked which thoughts +- Fetch thoughts liked by the logged-in user +- Filtering & sorting thoughts: By date and number of likes +- Input validation & error handling +- Secure routes for authenticated actions only + +--- + +## Tech Stack + +- Node.js +- Express +- MongoDB +- Mongoose +- bcrypt +- RESTful API design +- Render (deployment) diff --git a/server.js b/server.js index 6279047..f1bb0fa 100644 --- a/server.js +++ b/server.js @@ -277,7 +277,9 @@ app.post("/sessions", async (req, res) => { return res.status(401).json({ error: "Invalid user credentials" }); } - res.json({ + res.status(200).json({ + success: true, + message: "Login success", userId: user._id, accessToken: user.accessToken, name: user.name From a30870d4c40508cd8348aa86dcbb747362ba2c3d Mon Sep 17 00:00:00 2001 From: Gabriella Berkowicz Date: Tue, 3 Feb 2026 14:19:50 +0100 Subject: [PATCH 37/39] clean code --- server.js | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/server.js b/server.js index f1bb0fa..fd29bb0 100644 --- a/server.js +++ b/server.js @@ -87,7 +87,7 @@ app.get("/thoughts", async (req, res) => { //Filter on minimum of likes if (minLikes) { - filterCriteria[`hearts.${Number(minLikes) - 1}`] = { $exists: true }; //gte = greater than or equal to + filterCriteria[`hearts.${Number(minLikes) - 1}`] = { $exists: true }; } /* --- Functionality for sorting --- */ @@ -246,14 +246,15 @@ app.patch("/thoughts/id/:id/message", async (req, res) => { app.post("/users", async (req, res) => { try { const { name, email, password } = req.body; - // Use mongoose model to create a database entry const salt = bcrypt.genSaltSync(); + + // Use mongoose model to create a database entry const user = new User({ name, email, password: bcrypt.hashSync(password, salt) }); const savedUser = await user.save(); - res.status(201).json({ + res.status(200).json({ success: true, - message: "User created", + message: "User created successfully", id: user._id, accessToken: user.accessToken, name: user.name @@ -277,7 +278,7 @@ app.post("/sessions", async (req, res) => { return res.status(401).json({ error: "Invalid user credentials" }); } - res.status(200).json({ + res.json({ success: true, message: "Login success", userId: user._id, @@ -286,7 +287,11 @@ app.post("/sessions", async (req, res) => { }); } catch(error) { - res.status(500).json({ error: "Server error" }); + res.status(500).json({ + success: false, + message: "Something went wrong", + response: error, + }); } }); From 40d9670b25da00d7d57f0567aecbde57506c18a0 Mon Sep 17 00:00:00 2001 From: Gabriella Berkowicz Date: Tue, 3 Feb 2026 18:07:10 +0100 Subject: [PATCH 38/39] fix big with filter and sorting combined after changing hearts in thought schema --- server.js | 68 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/server.js b/server.js index fd29bb0..c6d95e0 100644 --- a/server.js +++ b/server.js @@ -81,52 +81,54 @@ app.get("/", (req, res) => { app.get("/thoughts", async (req, res) => { try { - /* --- Functionality for filtering --- */ - const filterCriteria = {}; // To use as argument in Model.find(). Will be a criteria or empty object (thus retrieving all thoughts) - const { fromDate, minLikes } = req.query; + const { minLikes, sortBy, order } = req.query; + const sortingOrder = order === "asc" ? 1 : -1; + + // Variable for telling MongoDB how to prepare the data + const filterAndSort = []; - //Filter on minimum of likes + // Compute the like count from the hearts array, to use in the filtering + filterAndSort.push({ + $addFields: { + likesCount: { $size: { $ifNull: ["$hearts", []] } } // Handle empty/null hearts + } + }); + + /* --- Functionality for filtering --- */ if (minLikes) { - filterCriteria[`hearts.${Number(minLikes) - 1}`] = { $exists: true }; + filterAndSort.push({ + $match: { likesCount: { $gte: Number(minLikes) } } //gte = Greater than or equals to + }); } /* --- Functionality for sorting --- */ const sortCriteria = {}; - const { sortBy, order } = req.query; - const sortingOrder = order === "asc" ? 1 : -1; - - // Translate to keep readable query parameters in the URL - let sort = sortBy; if (sortBy === "date") { - sort = "createdAt"; - } - if (sortBy === "likes") { - sort = "hearts"; - } - - if (sort) { - // Set the key-value pair in the object sortCriteria dynamically - obj[key] = value - sortCriteria[sort] = sortingOrder; // Set the key to the value of sort and its value to sortingOrder - if (sort !== "createdAt") { - sortCriteria.createdAt = -1; // Puts creation date as secondary sorting - } + sortCriteria.createdAt = sortingOrder; + } else if (sortBy === "likes") { + sortCriteria.likesCount = sortingOrder; + sortCriteria.createdAt = -1; // Secondary sort by date } else { - sortCriteria.createdAt = -1; // Creation date as default sorting + sortCriteria.createdAt = -1; // Default sorting } - const thoughts = await Thought - .find(filterCriteria) - .sort(sortCriteria) - .select("-editToken"); // To exclude editToken from being exposed to users - ; + filterAndSort.push({ $sort: sortCriteria }); + + /// Remove editToken to prevent it being exposed to users + filterAndSort.push({ + $project: { editToken: 0 } + }); + + /* --- Execute filter and sorting --- */ + const thoughts = await Thought.aggregate(filterAndSort); const result = thoughts.map((thought) => { - const thoughtObj = thought.toObject(); // Convert to JS object (because of Mongoose) - delete thoughtObj.userId; // remove userId to be on front-end + const isCreator = req.user && thought.userId?.equals(req.user._id); + delete thought.userId; // Remove userId (after isCreator is computed) to prevent it from being exposed on front-end return { - ...thoughtObj, - isCreator: req.user && thought.userId?.equals(req.user._id) // For determining edit rights (computed on thought and not thoughtObj that has the uderId removed) - } + ...thought, + isCreator + }; }); res.json(result); } catch (error) { From f54a00f8b83bc045a6b30506c8f9a86375528149 Mon Sep 17 00:00:00 2001 From: Gabriella Berkowicz Date: Fri, 6 Feb 2026 10:15:20 +0100 Subject: [PATCH 39/39] put routes and auth middleware in separate files for better organization --- middlewares/authMiddleware.js | 36 +++++ routes/thoughtRoutes.js | 198 +++++++++++++++++++++++ routes/userRoutes.js | 64 ++++++++ server.js | 294 +--------------------------------- 4 files changed, 305 insertions(+), 287 deletions(-) create mode 100644 middlewares/authMiddleware.js create mode 100644 routes/thoughtRoutes.js create mode 100644 routes/userRoutes.js diff --git a/middlewares/authMiddleware.js b/middlewares/authMiddleware.js new file mode 100644 index 0000000..c379e98 --- /dev/null +++ b/middlewares/authMiddleware.js @@ -0,0 +1,36 @@ +import User from "../models/User"; + + +// Global middleware for authentication - To attach req.user everywhere +// I.e. if there is an accessToken in request header, find the matching user of it and attach it to every request +export const optionalAuth = async (req, res, next) => { + try { + const accessToken = req.headers.authorization; + + if (!accessToken) { + return next(); + } + + const matchingUser = await User.findOne({ accessToken: accessToken }); + + if (matchingUser) { + req.user = matchingUser + } + + next(); + + } catch(error) { + console.error("Optional auth error:", error) + next(); + } +}; + + +// To be used in routes that should only be accessed by authorized users +export const authenticateUser = (req, res, next) => { + + if (!req.user) { + return res.status(401).json({ loggedOut: true }); + } + next(); +}; diff --git a/routes/thoughtRoutes.js b/routes/thoughtRoutes.js new file mode 100644 index 0000000..f0d1895 --- /dev/null +++ b/routes/thoughtRoutes.js @@ -0,0 +1,198 @@ +import express from "express"; +import mongoose from "mongoose"; +import Thought from "../models/Thought"; +import { authenticateUser } from "../middlewares/authMiddleware"; +import dotenv from "dotenv"; +dotenv.config(); + +// Endpoint is /thoughts +const router = express.Router(); + + +// All thoughts +router.get("/", async (req, res) => { + + try { + const { minLikes, sortBy, order } = req.query; + const sortingOrder = order === "asc" ? 1 : -1; + + // Variable for telling MongoDB how to prepare the data + const filterAndSort = []; + + // Compute the like count from the hearts array, to use in the filtering + filterAndSort.push({ + $addFields: { + likesCount: { $size: { $ifNull: ["$hearts", []] } } // Handle empty/null hearts + } + }); + + /* --- Functionality for filtering --- */ + if (minLikes) { + filterAndSort.push({ + $match: { likesCount: { $gte: Number(minLikes) } } //gte = Greater than or equals to + }); + } + + /* --- Functionality for sorting --- */ + const sortCriteria = {}; + if (sortBy === "date") { + sortCriteria.createdAt = sortingOrder; + } else if (sortBy === "likes") { + sortCriteria.likesCount = sortingOrder; + sortCriteria.createdAt = -1; // Secondary sort by date + } else { + sortCriteria.createdAt = -1; // Default sorting + } + + filterAndSort.push({ $sort: sortCriteria }); + + /// Remove editToken to prevent it being exposed to users + filterAndSort.push({ + $project: { editToken: 0 } + }); + + /* --- Execute filter and sorting --- */ + const thoughts = await Thought.aggregate(filterAndSort); + + const result = thoughts.map((thought) => { + const isCreator = req.user && thought.userId?.equals(req.user._id); + delete thought.userId; // Remove userId (after isCreator is computed) to prevent it from being exposed on front-end + return { + ...thought, + isCreator + }; + }); + res.json(result); + } catch (error) { + console.error("GET /thoughts error:", error); + res.status(500).json({ message: "Failed to fetch thoughts", error: error.message }); + } +}); + + +// Post a thought +router.post("/", async (req, res) => { + try { + const message = req.body.message; + + // Use mongoose model to create a database entry + const newThought = new Thought({ + message, + userId: req.user ? req.user._id : null + }); + + const savedThought = await newThought.save(); + + res.status(201).json(savedThought); + } catch(error) { + res.status(400).json({ + message: "Failed to save thought to database", + error: error.message + }); + } +}); + + +// Delete a thought +router.delete("/id/:id", async (req, res) => { + const id = req.params.id; + + // Error handling for invalid id input + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ error: `Invalid id: ${id}` }); + } + + try { + const deletedThought = await Thought.findByIdAndDelete(id); + + // Error handling for no ID match + if(!deletedThought) { + return res.status(404).json({ error: `Thought with id ${id} not found` }); + } + + res.json(deletedThought); + + } catch(error) { + res.status(500).json({error: error.message}); + } +}); + + +// Update the like count of a thought +router.patch("/id/:id/like", async (req, res) => { + try { + const { id } = req.params; + + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ error: `Invalid id: ${id}` }); + } + + const updatedThought = await Thought.findByIdAndUpdate( + id, + { $push: { hearts: { userId: req.user ? req.user._id : null } } }, //Ensures the updated heart count gets returned, and that schema validation also is performed + { new: true, runValidators: true } + ); + + // Error handling for no ID match + if(!updatedThought) { + return res.status(404).json({ error: `Thought with id ${id} not found` }); + } + + res.json(updatedThought); + + } catch(error) { + res.status(500).json({ error: error.message }); + } +}); + + +// Update the message of a thought +router.patch("/id/:id/message", async (req, res) => { + const { id } = req.params; + const { message } = req.body; + + // Error handling for invalid id input + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ error: `Invalid id: ${id}` }); + } + + try { + const updatedThought = await Thought.findByIdAndUpdate( + id, + { message }, + { new: true, runValidators: true} //Ensures the updated message gets returned, and that schema validation also is performed on the new message + ); + + // Error handling for no ID match + if(!updatedThought) { + return res.status(404).json({error: `Thought with id ${id} not found`}); + } + + res.json(updatedThought); + + } catch(err) { + res.status(500).json({error: err.message}); + } +}); + + +/* --- Authenticated only routes ---*/ + + +// Liked thoughts +router.get("/liked", authenticateUser, async (req, res) => { + try { + const likedThoughts = await Thought + .find({ "hearts.userId": req.user._id }) + .sort({ createdAt: -1 }); + + res.json(likedThoughts); + + } catch (error) { + console.error("GET /thoughts error:", error); + res.status(500).json({ message: "Failed to fetch liked thoughts", error: error.message }); + } +}); + + +export default router; \ No newline at end of file diff --git a/routes/userRoutes.js b/routes/userRoutes.js new file mode 100644 index 0000000..b651495 --- /dev/null +++ b/routes/userRoutes.js @@ -0,0 +1,64 @@ +import express from "express"; +import User from "../models/User"; +import dotenv from "dotenv"; +dotenv.config(); +import bcrypt from "bcrypt-nodejs"; + +// Endpoint is /users +const router = express.Router(); + +// Create a new user (sign-up) +router.post("/signup", async (req, res) => { + try { + const { name, email, password } = req.body; + const salt = bcrypt.genSaltSync(); + + // Use mongoose model to create a database entry + const user = new User({ name, email, password: bcrypt.hashSync(password, salt) }); + await user.save(); + + res.status(200).json({ + success: true, + message: "User created successfully", + id: user._id, + accessToken: user.accessToken, + name: user.name + }); + } catch(error) { + res.status(400).json({ + success: false, + message: "Failed to create user", + error: error.errors}); + } +}); + + +// Login +router.post("/login", async (req, res) => { + try{ + const { email, password } = req.body; + const user = await User.findOne({email: email}); + + if(!user || !bcrypt.compareSync(password, user.password)) { + return res.status(401).json({ error: "Invalid user credentials" }); + } + + res.json({ + success: true, + message: "Login success", + userId: user._id, + accessToken: user.accessToken, + name: user.name + }); + + } catch(error) { + res.status(500).json({ + success: false, + message: "Something went wrong", + response: error, + }); + } +}); + + +export default router; \ No newline at end of file diff --git a/server.js b/server.js index c6d95e0..7ead05d 100644 --- a/server.js +++ b/server.js @@ -2,12 +2,12 @@ import cors from "cors"; import express from "express"; import expressListEndpoints from "express-list-endpoints"; import mongoose from "mongoose"; -import Thought from "./models/Thought"; -import User from "./models/User"; import { seedDatabase } from "./seedDatabase"; import dotenv from "dotenv"; dotenv.config(); -import bcrypt from "bcrypt-nodejs"; +import { optionalAuth } from "./middlewares/authMiddleware.js"; +import thoughtRoutes from "./routes/thoughtRoutes.js"; +import userRoutes from "./routes/userRoutes.js"; // 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 @@ -19,51 +19,7 @@ const app = express(); app.use(cors()); app.use(express.json()); -// Middleware for authentication -// If there is an accessToken from a logged in user in the request header, find matching user and attach it to the request -app.use(async (req, res, next) => { - try { - const accessToken = req.headers.authorization; - - if (!accessToken) { - return next(); - } - - if (accessToken) { - const matchingUser = await User.findOne({ accessToken: accessToken }); - - if (matchingUser) { - req.user = matchingUser - } - } - - next(); - - } catch(error) { - console.error("Authentication middleware error:", error) - next(); // Prevent blocking - } -}); - - -// To be used in routes that should only be accessed by authorized users -const authenticateUser = (req, res, next) => { - - if (!req.user) { - return res.status(401).json({ loggedOut: true }); - } - next(); -}; - - -/* --- Error handling to check database connection --- */ -app.use((req, res, next) => { - if(mongoose.connection.readyState === 1) { // 1 is connected - next(); // Continue on executing what comes after - } else { - res.status(503).json({ error: "Service unavailable" }); - } -}); +app.use(optionalAuth); // Global middleware for authentication - To attach req.user everywhere if there is an accessToken in the request header /* --- Routes --- */ @@ -76,245 +32,9 @@ app.get("/", (req, res) => { }); }); - -// All thoughts -app.get("/thoughts", async (req, res) => { - - try { - const { minLikes, sortBy, order } = req.query; - const sortingOrder = order === "asc" ? 1 : -1; - - // Variable for telling MongoDB how to prepare the data - const filterAndSort = []; - - // Compute the like count from the hearts array, to use in the filtering - filterAndSort.push({ - $addFields: { - likesCount: { $size: { $ifNull: ["$hearts", []] } } // Handle empty/null hearts - } - }); - - /* --- Functionality for filtering --- */ - if (minLikes) { - filterAndSort.push({ - $match: { likesCount: { $gte: Number(minLikes) } } //gte = Greater than or equals to - }); - } - - /* --- Functionality for sorting --- */ - const sortCriteria = {}; - if (sortBy === "date") { - sortCriteria.createdAt = sortingOrder; - } else if (sortBy === "likes") { - sortCriteria.likesCount = sortingOrder; - sortCriteria.createdAt = -1; // Secondary sort by date - } else { - sortCriteria.createdAt = -1; // Default sorting - } - - filterAndSort.push({ $sort: sortCriteria }); - - /// Remove editToken to prevent it being exposed to users - filterAndSort.push({ - $project: { editToken: 0 } - }); - - /* --- Execute filter and sorting --- */ - const thoughts = await Thought.aggregate(filterAndSort); - - const result = thoughts.map((thought) => { - const isCreator = req.user && thought.userId?.equals(req.user._id); - delete thought.userId; // Remove userId (after isCreator is computed) to prevent it from being exposed on front-end - return { - ...thought, - isCreator - }; - }); - res.json(result); - } catch (error) { - console.error("GET /thoughts error:", error); - res.status(500).json({ message: "Failed to fetch thoughts", error: error.message }); - } -}); - - -// Post a thought -app.post("/thoughts", async (req, res) => { - try { - const message = req.body.message; - - // Use mongoose model to create a database entry - const newThought = new Thought({ - message, - userId: req.user ? req.user._id : null - }); - - const savedThought = await newThought.save(); - - res.status(201).json(savedThought); - } catch(error) { - res.status(400).json({ - message: "Failed to save thought to database", - error: error.message - }); - } -}); - - -// Delete a thought -app.delete("/thoughts/id/:id", async (req, res) => { - const id = req.params.id; - - // Error handling for invalid id input - if (!mongoose.Types.ObjectId.isValid(id)) { - return res.status(400).json({ error: `Invalid id: ${id}` }); - } - - try { - const deletedThought = await Thought.findByIdAndDelete(id); - - // Error handling for no ID match - if(!deletedThought) { - return res.status(404).json({ error: `Thought with id ${id} not found` }); - } - - res.json(deletedThought); - - } catch(error) { - res.status(500).json({error: error.message}); - } -}); - - -// Update the like count of a thought -app.patch("/thoughts/id/:id/like", async (req, res) => { - try { - const { id } = req.params; - - if (!mongoose.Types.ObjectId.isValid(id)) { - return res.status(400).json({ error: `Invalid id: ${id}` }); - } - - const updatedThought = await Thought.findByIdAndUpdate( - id, - { $push: { hearts: { userId: req.user ? req.user._id : null } } }, //Ensures the updated heart count gets returned, and that schema validation also is performed - { new: true, runValidators: true } - ); - - // Error handling for no ID match - if(!updatedThought) { - return res.status(404).json({ error: `Thought with id ${id} not found` }); - } - - res.json(updatedThought); - - } catch(error) { - res.status(500).json({ error: error.message }); - } -}); - - -// Update the message of a thought -app.patch("/thoughts/id/:id/message", async (req, res) => { - const { id } = req.params; - const { message } = req.body; - - // Error handling for invalid id input - if (!mongoose.Types.ObjectId.isValid(id)) { - return res.status(400).json({ error: `Invalid id: ${id}` }); - } - - try { - const updatedThought = await Thought.findByIdAndUpdate( - id, - { message }, - { new: true, runValidators: true} //Ensures the updated message gets returned, and that schema validation also is performed on the new message - ); - - // Error handling for no ID match - if(!updatedThought) { - return res.status(404).json({error: `Thought with id ${id} not found`}); - } - - res.json(updatedThought); - - } catch(err) { - res.status(500).json({error: err.message}); - } -}); - - -// Create a new user (sign-up) -app.post("/users", async (req, res) => { - try { - const { name, email, password } = req.body; - const salt = bcrypt.genSaltSync(); - - // Use mongoose model to create a database entry - const user = new User({ name, email, password: bcrypt.hashSync(password, salt) }); - const savedUser = await user.save(); - - res.status(200).json({ - success: true, - message: "User created successfully", - id: user._id, - accessToken: user.accessToken, - name: user.name - }); - } catch(error) { - res.status(400).json({ - success: false, - message: "Failed to create user", - error: error.errors}); - } -}); - - -// Login -app.post("/sessions", async (req, res) => { - try{ - const { email, password } = req.body; - const user = await User.findOne({email: email}); - - if(!user || !bcrypt.compareSync(password, user.password)) { - return res.status(401).json({ error: "Invalid user credentials" }); - } - - res.json({ - success: true, - message: "Login success", - userId: user._id, - accessToken: user.accessToken, - name: user.name - }); - - } catch(error) { - res.status(500).json({ - success: false, - message: "Something went wrong", - response: error, - }); - } -}); - - -/* --- Authenticated only routes ---*/ - - -// Liked thoughts -app.get("/thoughts/liked", authenticateUser, async (req, res) => { - try { - const likedThoughts = await Thought - .find({ "hearts.userId": req.user._id }) - .sort({ createdAt: -1 }); - - res.json(likedThoughts); - - } catch (error) { - console.error("GET /thoughts error:", error); - res.status(500).json({ message: "Failed to fetch liked thoughts", error: error.message }); - } -}); +// The connections to the different routes with endpoints +app.use("/users", userRoutes); +app.use("/thoughts", thoughtRoutes); /* --- Connect to Mongo --- */