From adf823f2d93c25585771d48fea812ff5ba9fca7b Mon Sep 17 00:00:00 2001 From: Mimmi Eriksson Date: Wed, 28 May 2025 15:29:15 +0200 Subject: [PATCH 01/20] setting up, expanding dataset, started creating endpoints --- data.json | 488 ++++++++++++++++++++++++++++++++++++++++++++------- package.json | 3 +- server.js | 73 +++++++- 3 files changed, 501 insertions(+), 63 deletions(-) diff --git a/data.json b/data.json index a2c844f..c0592d6 100644 --- a/data.json +++ b/data.json @@ -1,121 +1,489 @@ [ - { + { "_id": "682bab8c12155b00101732ce", "message": "Berlin baby", "hearts": 37, "createdAt": "2025-05-19T22:07:08.999Z", - "__v": 0 + "__v": 0, + "tags": [ + "travel" + ] }, { - "_id": "682e53cc4fddf50010bbe739", + "_id": "682e53cc4fddf50010bbe739", "message": "My family!", "hearts": 0, "createdAt": "2025-05-22T22:29:32.232Z", - "__v": 0 + "__v": 0, + "tags": [ + "family" + ] }, { "_id": "682e4f844fddf50010bbe738", "message": "The smell of coffee in the morning....", "hearts": 23, "createdAt": "2025-05-22T22:11:16.075Z", - "__v": 0 + "__v": 0, + "tags": [ + "food" + ] }, { "_id": "682e48bf4fddf50010bbe737", - "message": "Newly washed bedlinen, kids that sleeps through the night.. FINGERS CROSSED 🤞🏼\n", + "message": "Newly washed bedlinen, kids that sleeps through the night.. FINGERS CROSSED \ud83e\udd1e\ud83c\udffc", "hearts": 6, "createdAt": "2025-05-21T21:42:23.862Z", - "__v": 0 + "__v": 0, + "tags": [ + "home", + "family" + ] }, { "_id": "682e45804fddf50010bbe736", "message": "I am happy that I feel healthy and have energy again", "hearts": 13, "createdAt": "2025-05-21T21:28:32.196Z", - "__v": 0 + "__v": 0, + "tags": [ + "wellness" + ] }, { "_id": "682e23fecf615800105107aa", "message": "cold beer", "hearts": 2, "createdAt": "2025-05-21T19:05:34.113Z", - "__v": 0 - }, - { - "_id": "682e22aecf615800105107a9", - "message": "My friend is visiting this weekend! <3", - "hearts": 6, - "createdAt": "2025-05-21T18:59:58.121Z", - "__v": 0 + "__v": 0, + "tags": [ + "food" + ] }, { "_id": "682cec1b17487d0010a298b6", "message": "A god joke: \nWhy did the scarecrow win an award?\nBecause he was outstanding in his field!", "hearts": 12, "createdAt": "2025-05-20T20:54:51.082Z", - "__v": 0 + "__v": 0, + "tags": [ + "humor" + ] }, { - "_id": "682cebbe17487d0010a298b5", - "message": "Tacos and tequila🌮🍹", - "hearts": 2, - "createdAt": "2025-05-19T20:53:18.899Z", - "__v": 0 + "_id": "07a90e8458254cd191f7fe95", + "message": "Spontaneous road trips \ud83d\ude97", + "hearts": 4, + "createdAt": "2025-05-23T13:29:38.579944Z", + "__v": 0, + "tags": [ + "friends" + ] }, { - "_id": "682ceb5617487d0010a298b4", - "message": "Netflix and late night ice-cream🍦", - "hearts": 1, - "createdAt": "2025-05-18T20:51:34.494Z", - "__v": 0 + "_id": "fcfa78207afa49f7b2594711", + "message": "Movie night with friends \ud83c\udf7f", + "hearts": 39, + "createdAt": "2025-05-21T19:29:38.580109Z", + "__v": 0, + "tags": [ + "nature" + ] }, { - "_id": "682c99ba3bff2d0010f5d44e", - "message": "Summer is coming...", + "_id": "3536a0ba77ec457b9795a289", + "message": "A peaceful morning walk \ud83c\udf04", + "hearts": 5, + "createdAt": "2025-05-21T10:29:38.580147Z", + "__v": 0, + "tags": [ + "entertainment" + ] + }, + { + "_id": "910d586b37f24972a8dc5be5", + "message": "Freshly baked bread smell \ud83e\udd56", + "hearts": 22, + "createdAt": "2025-05-26T22:29:38.580184Z", + "__v": 0, + "tags": [ + "friends", + "nature" + ] + }, + { + "_id": "9b4133f63bfe44c5942c1f2b", + "message": "Laughing until your stomach hurts \ud83d\ude02", "hearts": 2, - "createdAt": "2025-05-20T15:03:22.379Z", - "__v": 0 + "createdAt": "2025-05-18T22:29:38.580221Z", + "__v": 0, + "tags": [ + "food", + "nature" + ] }, { - "_id": "682c706c951f7a0017130024", - "message": "Exercise? I thought you said extra fries! 🍟😂", + "_id": "cfbf58267b394bbfa59194df", + "message": "The sound of rain while falling asleep \ud83c\udf27\ufe0f", + "hearts": 11, + "createdAt": "2025-05-19T20:29:38.580358Z", + "__v": 0, + "tags": [ + "humor", + "wellness" + ] + }, + { + "_id": "1e9f0cc1eec64d1380fbc872", + "message": "A clean desk and a fresh to-do list \ud83d\uddd2\ufe0f", + "hearts": 24, + "createdAt": "2025-05-18T17:29:38.580375Z", + "__v": 0, + "tags": [ + "humor", + "nature" + ] + }, + { + "_id": "ffc30cff7b2c4e53b5c1b1b1", + "message": "Camping under the stars \u2728", + "hearts": 34, + "createdAt": "2025-05-22T17:29:38.580462Z", + "__v": 0, + "tags": [ + "home" + ] + }, + { + "_id": "51b03bb41b794757847c8cf5", + "message": "A finished workout \ud83c\udfcb\ufe0f\u200d\u2642\ufe0f", + "hearts": 17, + "createdAt": "2025-05-24T09:29:38.580678Z", + "__v": 0, + "tags": [ + "friends", + "travel" + ] + }, + { + "_id": "22048fed82f14ee4bb8d0ab8", + "message": "Good coffee and a great book \u2615\ud83d\udcda", + "hearts": 5, + "createdAt": "2025-05-20T17:29:38.580724Z", + "__v": 0, + "tags": [ + "nature" + ] + }, + { + "_id": "3ba0daf6c89a412b9817f241", + "message": "Playing games with family \ud83c\udfb2", + "hearts": 18, + "createdAt": "2025-05-22T15:29:38.580753Z", + "__v": 0, + "tags": [ + "nature" + ] + }, + { + "_id": "3f2bc5aa74704e9f94563551", + "message": "Dinner with loved ones \u2764\ufe0f", + "hearts": 39, + "createdAt": "2025-05-19T18:29:38.580766Z", + "__v": 0, + "tags": [ + "friends" + ] + }, + { + "_id": "0a1363f9e2c34920a52230ca", + "message": "Jokes that never get old \ud83d\ude06", + "hearts": 33, + "createdAt": "2025-05-26T17:29:38.580778Z", + "__v": 0, + "tags": [ + "humor" + ] + }, + { + "_id": "4d11d277debd44be92901ffc", + "message": "Beautiful sunset views \ud83c\udf05", "hearts": 14, - "createdAt": "2025-05-20T12:07:08.185Z", - "__v": 0 + "createdAt": "2025-05-24T02:29:38.580798Z", + "__v": 0, + "tags": [ + "wellness" + ] }, { - "_id": "682c6fe1951f7a0017130023", - "message": "I’m on a seafood diet. I see food, and I eat it.", - "hearts": 4, - "createdAt": "2025-05-20T12:04:49.978Z", - "__v": 0 + "_id": "55dd876c62924eb4aeaad5f6", + "message": "Listening to your favorite song \ud83c\udfb6", + "hearts": 9, + "createdAt": "2025-05-25T11:29:38.580818Z", + "__v": 0, + "tags": [ + "work", + "nature" + ] }, { - "_id": "682c6f0e951f7a0017130022", - "message": "Cute monkeys🐒", - "hearts": 2, - "createdAt": "2025-05-20T12:01:18.308Z", - "__v": 0 + "_id": "a837812ccdde410d83431ae2", + "message": "Long chats with best friends \ud83d\udcac", + "hearts": 5, + "createdAt": "2025-05-22T22:29:38.580835Z", + "__v": 0, + "tags": [ + "food" + ] }, { - "_id": "682c6e65951f7a0017130021", - "message": "The weather is nice!", - "hearts": 0, - "createdAt": "2025-05-20T11:58:29.662Z", - "__v": 0 + "_id": "c046b7c46e424fb099fe36dd", + "message": "A productive day at work \ud83d\udcbc", + "hearts": 41, + "createdAt": "2025-05-25T06:29:38.580854Z", + "__v": 0, + "tags": [ + "food" + ] }, { - "_id": "682bfdb4270ca300105af221", - "message": "good vibes and good things", - "hearts": 3, - "createdAt": "2025-05-20T03:57:40.322Z", - "__v": 0 + "_id": "09c98c694a0e442f9a5b67c6", + "message": "Getting lost in a movie \ud83c\udfa5", + "hearts": 48, + "createdAt": "2025-05-18T22:29:38.580875Z", + "__v": 0, + "tags": [ + "friends", + "home" + ] }, { - "_id": "682bab8c12155b00101732ce", - "message": "Berlin baby", - "hearts": 37, - "createdAt": "2025-05-19T22:07:08.999Z", - "__v": 0 + "_id": "2600d2ec6bc046b981529b32", + "message": "Beach walks in the evening \ud83c\udf0a", + "hearts": 9, + "createdAt": "2025-05-26T19:29:38.580913Z", + "__v": 0, + "tags": [ + "food", + "friends" + ] + }, + { + "_id": "fae76c326ca948ee8cef4066", + "message": "Comfort food on a cold day \ud83c\udf72", + "hearts": 15, + "createdAt": "2025-05-17T10:29:38.580929Z", + "__v": 0, + "tags": [ + "friends" + ] + }, + { + "_id": "2a1ca4e9f5c34eb2843d4f45", + "message": "Nature hikes on weekends \ud83e\udd7e", + "hearts": 40, + "createdAt": "2025-05-20T08:29:38.580942Z", + "__v": 0, + "tags": [ + "family" + ] + }, + { + "_id": "c23b95c354cb4376b87f0c58", + "message": "Catching up with old friends \ud83e\uddd3\ud83d\udc75", + "hearts": 15, + "createdAt": "2025-05-18T03:29:38.580979Z", + "__v": 0, + "tags": [ + "family", + "food" + ] + }, + { + "_id": "d58f905bc9a64347a2b778f8", + "message": "Trying a new recipe \ud83c\udf5d", + "hearts": 13, + "createdAt": "2025-05-23T23:29:38.581029Z", + "__v": 0, + "tags": [ + "family" + ] + }, + { + "_id": "d7af831586f6445d8c88414b", + "message": "Writing in a journal \u270d\ufe0f", + "hearts": 10, + "createdAt": "2025-05-23T15:29:38.581054Z", + "__v": 0, + "tags": [ + "home" + ] + }, + { + "_id": "16694e0a6ae947ccb8910ab2", + "message": "Clean laundry smell \ud83d\udc55", + "hearts": 24, + "createdAt": "2025-05-28T00:29:38.581087Z", + "__v": 0, + "tags": [ + "food" + ] + }, + { + "_id": "0465a1a15cc24c9c91581aa2", + "message": "Cozy blankets and tea \ud83d\udecb\ufe0f\ud83c\udf75", + "hearts": 33, + "createdAt": "2025-05-23T18:29:38.581122Z", + "__v": 0, + "tags": [ + "nature" + ] + }, + { + "_id": "ef38b962265446da972a71db", + "message": "Early morning jogs \ud83c\udfc3\u200d\u2640\ufe0f", + "hearts": 12, + "createdAt": "2025-05-19T09:29:38.581165Z", + "__v": 0, + "tags": [ + "wellness", + "home" + ] + }, + { + "_id": "75ebeadadc8e4f56a4c48411", + "message": "Friday night fun \ud83c\udf89", + "hearts": 27, + "createdAt": "2025-05-27T02:29:38.581192Z", + "__v": 0, + "tags": [ + "wellness", + "nature" + ] + }, + { + "_id": "75bb4457d31b4f26a0514aa2", + "message": "Midnight snacks \ud83c\udf6a", + "hearts": 19, + "createdAt": "2025-05-19T10:29:38.581220Z", + "__v": 0, + "tags": [ + "home", + "wellness" + ] + }, + { + "_id": "22173c81b3164b11b8456b8b", + "message": "Board games night \ud83c\udfaf", + "hearts": 12, + "createdAt": "2025-05-24T14:29:38.581238Z", + "__v": 0, + "tags": [ + "wellness" + ] + }, + { + "_id": "916e209a11b44299abe6f4d9", + "message": "City lights and night walks \ud83c\udf03", + "hearts": 43, + "createdAt": "2025-05-27T22:29:38.581256Z", + "__v": 0, + "tags": [ + "entertainment", + "food" + ] + }, + { + "_id": "2916877bff6d4f6893efeb20", + "message": "Seeing dogs at the park \ud83d\udc36", + "hearts": 12, + "createdAt": "2025-05-17T14:29:38.581285Z", + "__v": 0, + "tags": [ + "food" + ] + }, + { + "_id": "f4171193f85c40c8bd4b62af", + "message": "Organizing the closet \ud83e\uddfa", + "hearts": 43, + "createdAt": "2025-05-25T17:29:38.581308Z", + "__v": 0, + "tags": [ + "home" + ] + }, + { + "_id": "3c69c11d64df4e44b26530ba", + "message": "First sip of soda \ud83e\udd64", + "hearts": 14, + "createdAt": "2025-05-18T04:29:38.581328Z", + "__v": 0, + "tags": [ + "nature" + ] + }, + { + "_id": "721b8bded1e0403db8e68897", + "message": "Weekend brunch \ud83c\udf73", + "hearts": 28, + "createdAt": "2025-05-27T10:29:38.581366Z", + "__v": 0, + "tags": [ + "entertainment" + ] + }, + { + "_id": "598891b532a2475a997b9f89", + "message": "Painting and creativity \ud83c\udfa8", + "hearts": 10, + "createdAt": "2025-05-25T09:29:38.581384Z", + "__v": 0, + "tags": [ + "nature" + ] + }, + { + "_id": "142cf087096a4a34b4fd8975", + "message": "Sunlight through the window \u2600\ufe0f", + "hearts": 14, + "createdAt": "2025-05-22T03:29:38.581428Z", + "__v": 0, + "tags": [ + "wellness", + "work" + ] + }, + { + "_id": "a9c15b1fa3404013a7f919c6", + "message": "Solving a puzzle \ud83e\udde9", + "hearts": 39, + "createdAt": "2025-05-22T08:29:38.581453Z", + "__v": 0, + "tags": [ + "nature" + ] + }, + { + "_id": "e3cd420f6271431db0129824", + "message": "Lunch breaks outdoors \ud83c\udf33", + "hearts": 47, + "createdAt": "2025-05-21T10:29:38.581473Z", + "__v": 0, + "tags": [ + "entertainment", + "work" + ] + }, + { + "_id": "3163a957d8dd4213a8c4cedc", + "message": "Silent mornings before the world wakes up \ud83d\udd4a\ufe0f", + "hearts": 32, + "createdAt": "2025-05-26T03:29:38.581513Z", + "__v": 0, + "tags": [ + "home", + "travel" + ] } ] \ No newline at end of file diff --git a/package.json b/package.json index bf25bb6..e58bc75 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", - "nodemon": "^3.0.1" + "express-list-endpoints": "^7.1.1", + "nodemon": "^3.1.10" } } diff --git a/server.js b/server.js index f47771b..2a0c7bd 100644 --- a/server.js +++ b/server.js @@ -1,5 +1,7 @@ import cors from "cors" import express from "express" +import listEndpoints from "express-list-endpoints" +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: @@ -11,12 +13,79 @@ const app = express() app.use(cors()) app.use(express.json()) -// Start defining your routes here +// endpoint for documentation of the API app.get("/", (req, res) => { - res.send("Hello Technigo!") + const endpoints = listEndpoints(app) + res.json({ + message: "Welcome to the Happy Thoughts API", + endpoints: endpoints + }) +}) + +// endpoint to get all thoughts +app.get("/thoughts", (req, res) => { + const page = req.query.page || 1 + const limit = req.query.limit || 10 + const tag = req.query.tag + const sort = req.query.sort + // filter thoughts + // filter on: tags, + let filteredThoughts = data + if (tag) { + filteredThoughts = filteredThoughts.filter(thought => + thought.tags.some(word => word.toLowerCase() === tag.toLowerCase()) + ) + } + // sort thoughts + // sort on: createdAt, hearts, + + // paginate results + const paginatedThoughts = filteredThoughts.slice((page - 1) * limit, page * limit) + res.json(paginatedThoughts) +}) + +// endpoint to get one thought +app.get("/thoughts/:id", (req, res) => { + const thought = data.filter(thought => thought._id === req.params.id) + // if id doesn't exist - return not found + if (!thought || thought.length === 0) { + return res.status(404).json({ error: "Thought not found!" }) + } + res.json(thought) }) // Start the server app.listen(port, () => { console.log(`Server running on http://localhost:${port}`) }) + + +// endpoint ideas: + +// GET /messages +// Returns all messages (optionally paginated). +// Query params: ?page=1&limit=20, ?sort=hearts, ?tag=travel + +// GET /messages/recent +// shortcut for newest messages (e.g., limit=10 and sorted by createdAt) + +// GET /messages/popular +// Returns messages sorted by most hearts or engagement. + +// GET /messages/search +// Query param: ?q=coffee – search messages by keyword (basic or fuzzy). + +// tags +// GET /tags - Returns the list of available tags/categories + + + +// POST /messages +// Creates a new message. +// Body: { message: "string", tags: ["tag1", "tag2"] } + +// DELETE /messages/:id +// Deletes a specific message. + +// PATCH /messages/:id +// Allows editing a message (like fixing a typo). \ No newline at end of file From 7e877cec4e86d735e1fb90bbe42a40f1708d9b85 Mon Sep 17 00:00:00 2001 From: Mimmi Eriksson Date: Wed, 28 May 2025 15:47:49 +0200 Subject: [PATCH 02/20] added popular endpoint to get most liked messages --- server.js | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/server.js b/server.js index 2a0c7bd..195aa5b 100644 --- a/server.js +++ b/server.js @@ -23,28 +23,46 @@ app.get("/", (req, res) => { }) // endpoint to get all thoughts +// query params: ?page=1, ?limit=20, ?sort=hearts, ?tag=travel app.get("/thoughts", (req, res) => { const page = req.query.page || 1 const limit = req.query.limit || 10 const tag = req.query.tag const sort = req.query.sort + let thoughts = data // filter thoughts // filter on: tags, - let filteredThoughts = data if (tag) { - filteredThoughts = filteredThoughts.filter(thought => + thoughts = thoughts.filter(thought => thought.tags.some(word => word.toLowerCase() === tag.toLowerCase()) ) } // sort thoughts // sort on: createdAt, hearts, + // continue here // paginate results - const paginatedThoughts = filteredThoughts.slice((page - 1) * limit, page * limit) - res.json(paginatedThoughts) + thoughts = thoughts.slice((page - 1) * limit, page * limit) + res.json(thoughts) }) -// endpoint to get one thought +// endpoint to get most liked messages +app.get("/thoughts/popular", (req, res) => { + const page = req.query.page || 1 + const limit = req.query.limit || 10 + // sort on most hearts + let popularThoughts = data.sort((a, b) => b.hearts - a.hearts) + // paginate results + popularThoughts = popularThoughts.slice((page - 1) * limit, page * limit) + res.json(popularThoughts) +}) + +// endpoint to get most recent messages +app.get("/thoughts/recent", (req, res) => { + // continue here +}) + +// endpoint to get one thought by id app.get("/thoughts/:id", (req, res) => { const thought = data.filter(thought => thought._id === req.params.id) // if id doesn't exist - return not found @@ -62,18 +80,14 @@ app.listen(port, () => { // endpoint ideas: -// GET /messages -// Returns all messages (optionally paginated). -// Query params: ?page=1&limit=20, ?sort=hearts, ?tag=travel - // GET /messages/recent // shortcut for newest messages (e.g., limit=10 and sorted by createdAt) // GET /messages/popular -// Returns messages sorted by most hearts or engagement. +// Returns messages sorted by most hearts // GET /messages/search -// Query param: ?q=coffee – search messages by keyword (basic or fuzzy). +// Query param: ?q=coffee - search messages by keyword // tags // GET /tags - Returns the list of available tags/categories From 3a1fb77711d2782b987eb6f25f7831d226e54a9f Mon Sep 17 00:00:00 2001 From: Mimmi Eriksson Date: Fri, 30 May 2025 09:46:35 +0200 Subject: [PATCH 03/20] added sorting option on hearts --- server.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/server.js b/server.js index 195aa5b..29ed275 100644 --- a/server.js +++ b/server.js @@ -31,14 +31,18 @@ app.get("/thoughts", (req, res) => { const sort = req.query.sort let thoughts = data // filter thoughts - // filter on: tags, + // filter on tag if (tag) { thoughts = thoughts.filter(thought => thought.tags.some(word => word.toLowerCase() === tag.toLowerCase()) ) } // sort thoughts - // sort on: createdAt, hearts, + // sort on hearts + if (sort) { + thoughts = thoughts.sort((a, b) => b.hearts - a.hearts) + } + // sort on createdAt // continue here // paginate results From b9307a11620b8dfbd38e1688d4782cd00ed1bfa8 Mon Sep 17 00:00:00 2001 From: Mimmi Eriksson Date: Fri, 30 May 2025 09:58:56 +0200 Subject: [PATCH 04/20] added sorting option on createdAt and recent thoughts endpoint --- server.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/server.js b/server.js index 29ed275..a807f88 100644 --- a/server.js +++ b/server.js @@ -39,12 +39,13 @@ app.get("/thoughts", (req, res) => { } // sort thoughts // sort on hearts - if (sort) { + if (sort === "likes") { thoughts = thoughts.sort((a, b) => b.hearts - a.hearts) } // sort on createdAt - // continue here - + if (sort === "time") { + thoughts = thoughts.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) + } // paginate results thoughts = thoughts.slice((page - 1) * limit, page * limit) res.json(thoughts) @@ -63,7 +64,13 @@ app.get("/thoughts/popular", (req, res) => { // endpoint to get most recent messages app.get("/thoughts/recent", (req, res) => { - // continue here + const page = req.query.page || 1 + const limit = req.query.limit || 10 + // sort on most hearts + let recentThoughts = data.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) + // paginate results + recentThoughts = recentThoughts.slice((page - 1) * limit, page * limit) + res.json(recentThoughts) }) // endpoint to get one thought by id From 9b3a5d6f06156e33b6830ea29525f7a7bd3ec2ec Mon Sep 17 00:00:00 2001 From: Mimmi Eriksson Date: Tue, 3 Jun 2025 14:47:58 +0200 Subject: [PATCH 05/20] setting up database connection, created model for data and seeded data into the database --- package.json | 1 + server.js | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/package.json b/package.json index e58bc75..2c90a07 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": "^8.15.1", "nodemon": "^3.1.10" } } diff --git a/server.js b/server.js index a807f88..e6c1a72 100644 --- a/server.js +++ b/server.js @@ -1,14 +1,57 @@ import cors from "cors" import express from "express" import listEndpoints from "express-list-endpoints" +import mongoose from "mongoose" + import data from "./data.json" +// setting up database connection +const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/thoughts" +mongoose.connect(mongoUrl) + // 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() +// creating a schema and model for messages in database +const thoughtSchema = new mongoose.Schema({ + message: { + type: String, + required: true, + minLength: 5, + maxLength: 140 + }, + hearts: { + type: Number, + default: 0, + min: 0 + }, + tags: { + type: [String], + required: true, + enum: ["travel", "food", "family", "friends", "humor", "nature", "wellness", "home", "entertainment", "work", "other"], + default: "other" + }, + createdAt: { + type: Date, + default: Date.now() + } +}) +const Thought = mongoose.model("Thought", thoughtSchema) + +// seeding data to database +if (process.env.RESET_DATABASE) { + const seedDatabase = async () => { + await Thought.deleteMany({}) + data.forEach(thought => { + new Thought(thought).save() + }) + } + seedDatabase() +} + // Add middlewares to enable cors and json body parsing app.use(cors()) app.use(express.json()) From 019eea9ca73a8ad16ce2a240285d0bae1fba1d4f Mon Sep 17 00:00:00 2001 From: Mimmi Eriksson Date: Tue, 3 Jun 2025 15:43:51 +0200 Subject: [PATCH 06/20] updated endpoints thoughts and popular to use mongoose --- server.js | 80 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 56 insertions(+), 24 deletions(-) diff --git a/server.js b/server.js index e6c1a72..fd8f82b 100644 --- a/server.js +++ b/server.js @@ -66,47 +66,79 @@ app.get("/", (req, res) => { }) // endpoint to get all thoughts -// query params: ?page=1, ?limit=20, ?sort=hearts, ?tag=travel -app.get("/thoughts", (req, res) => { +app.get("/thoughts", async (req, res) => { const page = req.query.page || 1 const limit = req.query.limit || 10 const tag = req.query.tag - const sort = req.query.sort - let thoughts = data - // filter thoughts - // filter on tag + const sort = req.query.sort || "time" // sort on most recent by default + + const query = {} + const sortOptions = {} + if (tag) { - thoughts = thoughts.filter(thought => - thought.tags.some(word => word.toLowerCase() === tag.toLowerCase()) - ) + query.tags = tag } - // sort thoughts - // sort on hearts + if (sort === "likes") { - thoughts = thoughts.sort((a, b) => b.hearts - a.hearts) + sortOptions.hearts = -1 } - // sort on createdAt + if (sort === "time") { - thoughts = thoughts.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) + sortOptions.createdAt = -1 + } + + try { + const filteredThoughts = await Thought.find(query).sort(sortOptions).skip((page - 1) * limit).limit(limit) + + if (filteredThoughts.length === 0) { + res.status(404).json({ + success: false, + response: [], + message: "No thoughts found on that query. Try another one." + }) + } + res.status(200).json({ + success: true, + response: filteredThoughts, + }) + } catch (error) { + res.status(400).json({ + success: false, + response: error, + message: "Failed to fetch thoughts." + }) } - // paginate results - thoughts = thoughts.slice((page - 1) * limit, page * limit) - res.json(thoughts) }) // endpoint to get most liked messages -app.get("/thoughts/popular", (req, res) => { +app.get("/thoughts/popular", async (req, res) => { const page = req.query.page || 1 const limit = req.query.limit || 10 - // sort on most hearts - let popularThoughts = data.sort((a, b) => b.hearts - a.hearts) - // paginate results - popularThoughts = popularThoughts.slice((page - 1) * limit, page * limit) - res.json(popularThoughts) + try { + const popularThoughts = await Thought.find().sort({ hearts: -1 }).skip((page - 1) * limit).limit(limit) + + if (popularThoughts.length === 0) { + return res.status(404).json({ + success: false, + response: [], + message: "No thoughts found." + }) + } + res.status(200).json({ + success: true, + response: popularThoughts, + }) + } catch (error) { + res.status(400).json({ + success: false, + response: error, + message: "Failed to fetch popular thoughts." + }) + } }) // endpoint to get most recent messages -app.get("/thoughts/recent", (req, res) => { +app.get("/thoughts/recent", async (req, res) => { const page = req.query.page || 1 const limit = req.query.limit || 10 // sort on most hearts From 53f5cd9300adb28b290d30f4c25cf5e477350295 Mon Sep 17 00:00:00 2001 From: Mimmi Eriksson Date: Wed, 4 Jun 2025 10:08:12 +0200 Subject: [PATCH 07/20] updated recent endpoint and endpoint to get one thougth by id --- server.js | 108 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 80 insertions(+), 28 deletions(-) diff --git a/server.js b/server.js index fd8f82b..9eb79d6 100644 --- a/server.js +++ b/server.js @@ -3,7 +3,7 @@ import express from "express" import listEndpoints from "express-list-endpoints" import mongoose from "mongoose" -import data from "./data.json" +// import data from "./data.json" // setting up database connection const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/thoughts" @@ -42,15 +42,15 @@ const thoughtSchema = new mongoose.Schema({ const Thought = mongoose.model("Thought", thoughtSchema) // seeding data to database -if (process.env.RESET_DATABASE) { - const seedDatabase = async () => { - await Thought.deleteMany({}) - data.forEach(thought => { - new Thought(thought).save() - }) - } - seedDatabase() -} +// if (process.env.RESET_DATABASE) { +// const seedDatabase = async () => { +// await Thought.deleteMany({}) +// data.forEach(thought => { +// new Thought(thought).save() +// }) +// } +// seedDatabase() +// } // Add middlewares to enable cors and json body parsing app.use(cors()) @@ -74,15 +74,12 @@ app.get("/thoughts", async (req, res) => { const query = {} const sortOptions = {} - if (tag) { query.tags = tag } - if (sort === "likes") { sortOptions.hearts = -1 } - if (sort === "time") { sortOptions.createdAt = -1 } @@ -91,7 +88,7 @@ app.get("/thoughts", async (req, res) => { const filteredThoughts = await Thought.find(query).sort(sortOptions).skip((page - 1) * limit).limit(limit) if (filteredThoughts.length === 0) { - res.status(404).json({ + return res.status(404).json({ success: false, response: [], message: "No thoughts found on that query. Try another one." @@ -114,14 +111,21 @@ app.get("/thoughts", async (req, res) => { app.get("/thoughts/popular", async (req, res) => { const page = req.query.page || 1 const limit = req.query.limit || 10 + const tag = req.query.tag + + const query = {} + if (tag) { + query.tags = tag + } + try { - const popularThoughts = await Thought.find().sort({ hearts: -1 }).skip((page - 1) * limit).limit(limit) + const popularThoughts = await Thought.find(query).sort({ hearts: -1 }).skip((page - 1) * limit).limit(limit) if (popularThoughts.length === 0) { return res.status(404).json({ success: false, response: [], - message: "No thoughts found." + message: "No thoughts found on that query. Try another one." }) } res.status(200).json({ @@ -129,7 +133,7 @@ app.get("/thoughts/popular", async (req, res) => { response: popularThoughts, }) } catch (error) { - res.status(400).json({ + res.status(500).json({ success: false, response: error, message: "Failed to fetch popular thoughts." @@ -141,21 +145,69 @@ app.get("/thoughts/popular", async (req, res) => { app.get("/thoughts/recent", async (req, res) => { const page = req.query.page || 1 const limit = req.query.limit || 10 - // sort on most hearts - let recentThoughts = data.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) - // paginate results - recentThoughts = recentThoughts.slice((page - 1) * limit, page * limit) - res.json(recentThoughts) + const tag = req.query.tag + + const query = {} + if (tag) { + query.tags = tag + } + + try { + const recentThoughts = await Thought.find(query).sort({ createdAt: -1 }).skip((page - 1) * limit).limit(limit) + + if (recentThoughts.length === 0) { + return res.status(404).json({ + success: false, + response: [], + message: "No thoughts found on that query. Try another one." + }) + } + res.status(200).json({ + success: true, + response: recentThoughts, + }) + } catch (error) { + res.status(500).json({ + success: false, + response: error, + message: "Failed to fetch recent thoughts." + }) + } }) // endpoint to get one thought by id -app.get("/thoughts/:id", (req, res) => { - const thought = data.filter(thought => thought._id === req.params.id) - // if id doesn't exist - return not found - if (!thought || thought.length === 0) { - return res.status(404).json({ error: "Thought not found!" }) +app.get("/thoughts/:id", async (req, res) => { + const { id } = req.params + + try { + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ + sucess: false, + response: null, + message: "Invalid ID format." + }) + } + + const thought = await Thought.findById(id) + if (!thought) { + return res.status(404).json({ + success: false, + response: null, + message: "Thought not found!" + }) + } + + res.status(200).json({ + success: true, + response: thought + }) + } catch (error) { + return res.status(500).json({ + success: false, + response: error, + message: "Failed to fetch thought." + }) } - res.json(thought) }) // Start the server From 8a8e4c42037411b51d959a8e0e96439bff44e97c Mon Sep 17 00:00:00 2001 From: Mimmi Eriksson Date: Wed, 4 Jun 2025 14:29:31 +0200 Subject: [PATCH 08/20] added endpoints to post, delete, edit and like thoughts --- server.js | 185 +++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 156 insertions(+), 29 deletions(-) diff --git a/server.js b/server.js index 9eb79d6..0690d4f 100644 --- a/server.js +++ b/server.js @@ -66,26 +66,24 @@ app.get("/", (req, res) => { }) // endpoint to get all thoughts +// ADD search functionality app.get("/thoughts", async (req, res) => { const page = req.query.page || 1 const limit = req.query.limit || 10 + const sortBy = req.query.sort_by || "-createdAt" // sort on most recent by default const tag = req.query.tag - const sort = req.query.sort || "time" // sort on most recent by default + const likes = req.query.likes const query = {} - const sortOptions = {} if (tag) { query.tags = tag } - if (sort === "likes") { - sortOptions.hearts = -1 - } - if (sort === "time") { - sortOptions.createdAt = -1 + if (likes) { + query.hearts = { $gte: likes } } try { - const filteredThoughts = await Thought.find(query).sort(sortOptions).skip((page - 1) * limit).limit(limit) + const filteredThoughts = await Thought.find(query).sort(sortBy).skip((page - 1) * limit).limit(limit) if (filteredThoughts.length === 0) { return res.status(404).json({ @@ -99,7 +97,7 @@ app.get("/thoughts", async (req, res) => { response: filteredThoughts, }) } catch (error) { - res.status(400).json({ + res.status(500).json({ success: false, response: error, message: "Failed to fetch thoughts." @@ -119,7 +117,7 @@ app.get("/thoughts/popular", async (req, res) => { } try { - const popularThoughts = await Thought.find(query).sort({ hearts: -1 }).skip((page - 1) * limit).limit(limit) + const popularThoughts = await Thought.find(query).sort("-hearts").skip((page - 1) * limit).limit(limit) if (popularThoughts.length === 0) { return res.status(404).json({ @@ -153,7 +151,7 @@ app.get("/thoughts/recent", async (req, res) => { } try { - const recentThoughts = await Thought.find(query).sort({ createdAt: -1 }).skip((page - 1) * limit).limit(limit) + const recentThoughts = await Thought.find(query).sort("-createdAt").skip((page - 1) * limit).limit(limit) if (recentThoughts.length === 0) { return res.status(404).json({ @@ -196,7 +194,6 @@ app.get("/thoughts/:id", async (req, res) => { message: "Thought not found!" }) } - res.status(200).json({ success: true, response: thought @@ -210,6 +207,153 @@ app.get("/thoughts/:id", async (req, res) => { } }) +// endpoint to add a thought +app.post("/thoughts", async (req, res) => { + const { message, tags } = req.body + + try { + // validate input + if (!message || message.length < 5 || message.length > 140) { + return res.status(400).json({ + success: false, + response: null, + message: "Message must be between 5 and 140 characters." + }) + } + + const newThought = await new Thought({ message, tags }).save() + if (!newThought) { + return res.status(400).json({ + success: false, + response: null, + message: "Failed to post thought." + }) + } + res.status(201).json({ + success: true, + response: newThought, + message: "Thought successfully posted!" + }) + } catch (error) { + res.status(500).json({ + success: false, + response: error, + message: "Failed to create thought." + }) + } +}) + +// endpoint to delete a thought +app.delete("/thoughts/:id", async (req, res) => { + const { id } = req.params + + try { + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ + sucess: false, + response: null, + message: "Invalid ID format." + }) + } + const thought = await Thought.findByIdAndDelete(id) + if (!thought) { + return res.status(404).json({ + success: false, + response: null, + message: "Thought not found!" + }) + } + res.status(200).json({ + success: true, + response: thought, + message: "Thought successfully deleted!" + }) + } catch (error) { + res.status(500).json({ + success: false, + response: error, + message: "Failed to delete thought." + }) + } +}) + +// endpoint to edit a thought +app.patch("/thoughts/:id", async (req, res) => { + const { id } = req.params + const { message } = req.body + + try { + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ + sucess: false, + response: null, + message: "Invalid ID format." + }) + } + if (message.length < 5 || message.length > 140) { + return res.status(400).json({ + success: false, + response: null, + message: "Message must be between 5 and 140 characters." + }) + } + + const thought = await Thought.findByIdAndUpdate(id, { message }, { new: true, runValidators: true }) + if (!thought) { + return res.status(404).json({ + success: false, + response: null, + message: "Thought not found!" + }) + } + res.status(200).json({ + success: true, + response: thought, + message: "Thought successfully edited!" + }) + } catch (error) { + res.status(500).json({ + success: false, + response: error, + message: "Failed to edit thought." + }) + } +}) + +// endpoint to like a thought +app.post("/thoughts/:id/like", async (req, res) => { + const { id } = req.params + + try { + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ + sucess: false, + response: null, + message: "Invalid ID format." + }) + } + const thought = await Thought.findByIdAndUpdate(id, { $inc: { hearts: 1 } }, { new: true, runValidators: true }) + if (!thought) { + return res.status(404).json({ + success: false, + response: null, + message: "Thought not found!" + }) + } + res.status(200).json({ + success: true, + response: thought, + message: "Thought successfully liked!" + }) + } catch (error) { + res.status(500).json({ + success: false, + response: error, + message: "Failed to like thought." + }) + } +}) + // Start the server app.listen(port, () => { console.log(`Server running on http://localhost:${port}`) @@ -218,26 +362,9 @@ app.listen(port, () => { // endpoint ideas: -// GET /messages/recent -// shortcut for newest messages (e.g., limit=10 and sorted by createdAt) - -// GET /messages/popular -// Returns messages sorted by most hearts - // GET /messages/search // Query param: ?q=coffee - search messages by keyword // tags // GET /tags - Returns the list of available tags/categories - - -// POST /messages -// Creates a new message. -// Body: { message: "string", tags: ["tag1", "tag2"] } - -// DELETE /messages/:id -// Deletes a specific message. - -// PATCH /messages/:id -// Allows editing a message (like fixing a typo). \ No newline at end of file From a32d255bd272dfd43b4e7d8ae1020e8200818b9a Mon Sep 17 00:00:00 2001 From: Mimmi Eriksson Date: Wed, 4 Jun 2025 15:40:21 +0200 Subject: [PATCH 09/20] changed like endpoint to patch instead of post --- server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server.js b/server.js index 0690d4f..6650f19 100644 --- a/server.js +++ b/server.js @@ -321,7 +321,7 @@ app.patch("/thoughts/:id", async (req, res) => { }) // endpoint to like a thought -app.post("/thoughts/:id/like", async (req, res) => { +app.patch("/thoughts/:id/like", async (req, res) => { const { id } = req.params try { From c0cd6b896b195597552ce4dc69a7761eb89c19a7 Mon Sep 17 00:00:00 2001 From: Mimmi Eriksson Date: Mon, 9 Jun 2025 11:12:56 +0200 Subject: [PATCH 10/20] changed date.now --- server.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server.js b/server.js index 6650f19..997002c 100644 --- a/server.js +++ b/server.js @@ -31,12 +31,13 @@ const thoughtSchema = new mongoose.Schema({ tags: { type: [String], required: true, + lowercase: true, enum: ["travel", "food", "family", "friends", "humor", "nature", "wellness", "home", "entertainment", "work", "other"], default: "other" }, createdAt: { type: Date, - default: Date.now() + default: Date.now } }) const Thought = mongoose.model("Thought", thoughtSchema) From f1c040ed1ce95de5f45748902376bc95d3d981a2 Mon Sep 17 00:00:00 2001 From: Mimmi Eriksson Date: Tue, 10 Jun 2025 15:17:47 +0200 Subject: [PATCH 11/20] added total count of thoughts for a query in the response, to be able to caclulate max pages in the frontend --- server.js | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/server.js b/server.js index 997002c..bb7649f 100644 --- a/server.js +++ b/server.js @@ -84,9 +84,10 @@ app.get("/thoughts", async (req, res) => { } try { - const filteredThoughts = await Thought.find(query).sort(sortBy).skip((page - 1) * limit).limit(limit) + const totalCount = await Thought.find(query).countDocuments() + const thoughts = await Thought.find(query).sort(sortBy).skip((page - 1) * limit).limit(limit) - if (filteredThoughts.length === 0) { + if (thoughts.length === 0) { return res.status(404).json({ success: false, response: [], @@ -95,7 +96,12 @@ app.get("/thoughts", async (req, res) => { } res.status(200).json({ success: true, - response: filteredThoughts, + response: { + data: thoughts, + totalCount: totalCount, + currentPage: page, + limit: limit, + } }) } catch (error) { res.status(500).json({ @@ -118,6 +124,7 @@ app.get("/thoughts/popular", async (req, res) => { } try { + const totalCount = await Thought.find(query).countDocuments() const popularThoughts = await Thought.find(query).sort("-hearts").skip((page - 1) * limit).limit(limit) if (popularThoughts.length === 0) { @@ -129,7 +136,12 @@ app.get("/thoughts/popular", async (req, res) => { } res.status(200).json({ success: true, - response: popularThoughts, + response: { + data: popularThoughts, + totalCount: totalCount, + currentPage: page, + limit: limit, + } }) } catch (error) { res.status(500).json({ @@ -152,6 +164,7 @@ app.get("/thoughts/recent", async (req, res) => { } try { + const totalCount = await Thought.find(query).countDocuments() const recentThoughts = await Thought.find(query).sort("-createdAt").skip((page - 1) * limit).limit(limit) if (recentThoughts.length === 0) { @@ -163,7 +176,12 @@ app.get("/thoughts/recent", async (req, res) => { } res.status(200).json({ success: true, - response: recentThoughts, + response: { + data: recentThoughts, + totalCount: totalCount, + currentPage: page, + limit: limit, + } }) } catch (error) { res.status(500).json({ From e1709d7c7c32dc5d71c3229e9a6e87eb1b55081e Mon Sep 17 00:00:00 2001 From: Mimmi Eriksson Date: Thu, 12 Jun 2025 15:39:13 +0200 Subject: [PATCH 12/20] added user model and routes, and refractoring --- middleware/authMiddleware.js | 21 +++ models/Thought.js | 28 +++ models/User.js | 20 ++ package.json | 1 + routes/thoughtRoutes.js | 313 +++++++++++++++++++++++++++++++ routes/userRoutes.js | 62 +++++++ server.js | 348 +---------------------------------- 7 files changed, 451 insertions(+), 342 deletions(-) create mode 100644 middleware/authMiddleware.js create mode 100644 models/Thought.js create mode 100644 models/User.js create mode 100644 routes/thoughtRoutes.js create mode 100644 routes/userRoutes.js diff --git a/middleware/authMiddleware.js b/middleware/authMiddleware.js new file mode 100644 index 0000000..1f10b68 --- /dev/null +++ b/middleware/authMiddleware.js @@ -0,0 +1,21 @@ +import { User } from "../models/User.js" + +export const authenticateUser = async (req, res, next) => { + try { + const user = await User.findOne({ accessToken: req.header("Authorization") }) + if (user) { + req.user = user + next() + } else { + res.status(401).json({ + message: "Authentication missing or invalid.", + loggedOut: true + }) + } + } catch (error) { + res.status(500).json({ + message: "Internal server error", + error: err.message + }); + } +} \ No newline at end of file diff --git a/models/Thought.js b/models/Thought.js new file mode 100644 index 0000000..bd75e78 --- /dev/null +++ b/models/Thought.js @@ -0,0 +1,28 @@ +import mongoose from "mongoose" + +const thoughtSchema = new mongoose.Schema({ + message: { + type: String, + required: true, + minLength: 5, + maxLength: 140 + }, + hearts: { + type: Number, + default: 0, + min: 0 + }, + tags: { + type: [String], + required: true, + lowercase: true, + enum: ["travel", "food", "family", "friends", "humor", "nature", "wellness", "home", "entertainment", "work", "other"], + default: "other" + }, + createdAt: { + type: Date, + default: Date.now + } +}) + +export const Thought = mongoose.model("Thought", thoughtSchema) \ No newline at end of file diff --git a/models/User.js b/models/User.js new file mode 100644 index 0000000..f68a11a --- /dev/null +++ b/models/User.js @@ -0,0 +1,20 @@ +import mongoose from "mongoose" +import crypto from "crypto" + +const userSchema = new mongoose.Schema({ + userName: { + type: String, + unique: true, + required: true + }, + password: { + type: String, + required: true + }, + accessToken: { + type: String, + default: () => crypto.randomBytes(128).toString("hex") + } +}) + +export const User = mongoose.model("User", userSchema) \ No newline at end of file diff --git a/package.json b/package.json index 2c90a07..11cff21 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", "express": "^4.17.3", "express-list-endpoints": "^7.1.1", diff --git a/routes/thoughtRoutes.js b/routes/thoughtRoutes.js new file mode 100644 index 0000000..fb12343 --- /dev/null +++ b/routes/thoughtRoutes.js @@ -0,0 +1,313 @@ +import express from "express"; +import { Thought } from "../models/Thought.js" +import { authenticateUser } from "../middleware/authMiddleware.js" + +const router = express.Router() + +// get all thoughts +router.get("/", async (req, res) => { + const page = req.query.page || 1 + const limit = req.query.limit || 10 + const sortBy = req.query.sort_by || "-createdAt" // sort on most recent by default + const tag = req.query.tag + const likes = req.query.likes + + const query = {} + if (tag) { + query.tags = tag + } + if (likes) { + query.hearts = { $gte: likes } + } + + try { + const totalCount = await Thought.find(query).countDocuments() + const thoughts = await Thought.find(query).sort(sortBy).skip((page - 1) * limit).limit(limit) + + if (thoughts.length === 0) { + return res.status(404).json({ + success: false, + response: [], + message: "No thoughts found on that query. Try another one." + }) + } + res.status(200).json({ + success: true, + response: { + data: thoughts, + totalCount: totalCount, + currentPage: page, + limit: limit, + } + }) + } catch (error) { + res.status(500).json({ + success: false, + response: error, + message: "Failed to fetch thoughts." + }) + } +}) + +// get most liked messages +router.get("/popular", async (req, res) => { + const page = req.query.page || 1 + const limit = req.query.limit || 10 + const tag = req.query.tag + + const query = {} + if (tag) { + query.tags = tag + } + + try { + const totalCount = await Thought.find(query).countDocuments() + const popularThoughts = await Thought.find(query).sort("-hearts").skip((page - 1) * limit).limit(limit) + + if (popularThoughts.length === 0) { + return res.status(404).json({ + success: false, + response: [], + message: "No thoughts found on that query. Try another one." + }) + } + res.status(200).json({ + success: true, + response: { + data: popularThoughts, + totalCount: totalCount, + currentPage: page, + limit: limit, + } + }) + } catch (error) { + res.status(500).json({ + success: false, + response: error, + message: "Failed to fetch popular thoughts." + }) + } +}) + +// get most recent messages +router.get("/recent", async (req, res) => { + const page = req.query.page || 1 + const limit = req.query.limit || 10 + const tag = req.query.tag + + const query = {} + if (tag) { + query.tags = tag + } + + try { + const totalCount = await Thought.find(query).countDocuments() + const recentThoughts = await Thought.find(query).sort("-createdAt").skip((page - 1) * limit).limit(limit) + + if (recentThoughts.length === 0) { + return res.status(404).json({ + success: false, + response: [], + message: "No thoughts found on that query. Try another one." + }) + } + res.status(200).json({ + success: true, + response: { + data: recentThoughts, + totalCount: totalCount, + currentPage: page, + limit: limit, + } + }) + } catch (error) { + res.status(500).json({ + success: false, + response: error, + message: "Failed to fetch recent thoughts." + }) + } +}) + +// get one thought by id +router.get("/:id", async (req, res) => { + const { id } = req.params + + try { + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ + sucess: false, + response: null, + message: "Invalid ID format." + }) + } + + const thought = await Thought.findById(id) + if (!thought) { + return res.status(404).json({ + success: false, + response: null, + message: "Thought not found!" + }) + } + res.status(200).json({ + success: true, + response: thought + }) + } catch (error) { + return res.status(500).json({ + success: false, + response: error, + message: "Failed to fetch thought." + }) + } +}) + +// post a thought +router.post("/", authenticateUser, async (req, res) => { + const { message, tags } = req.body + + try { + // validate input + if (!message || message.length < 5 || message.length > 140) { + return res.status(400).json({ + success: false, + response: null, + message: "Message must be between 5 and 140 characters." + }) + } + + const newThought = await new Thought({ message, tags }).save() + if (!newThought) { + return res.status(400).json({ + success: false, + response: null, + message: "Failed to post thought." + }) + } + res.status(201).json({ + success: true, + response: newThought, + message: "Thought successfully posted!" + }) + } catch (error) { + res.status(500).json({ + success: false, + response: error, + message: "Failed to create thought." + }) + } +}) + +// delete a thought +router.delete("/:id", authenticateUser, async (req, res) => { + const { id } = req.params + + try { + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ + sucess: false, + response: null, + message: "Invalid ID format." + }) + } + const thought = await Thought.findByIdAndDelete(id) + if (!thought) { + return res.status(404).json({ + success: false, + response: null, + message: "Thought not found!" + }) + } + res.status(200).json({ + success: true, + response: thought, + message: "Thought successfully deleted!" + }) + } catch (error) { + res.status(500).json({ + success: false, + response: error, + message: "Failed to delete thought." + }) + } +}) + +// edit a thought +router.patch("/:id", authenticateUser, async (req, res) => { + const { id } = req.params + const { message } = req.body + + try { + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ + sucess: false, + response: null, + message: "Invalid ID format." + }) + } + if (message.length < 5 || message.length > 140) { + return res.status(400).json({ + success: false, + response: null, + message: "Message must be between 5 and 140 characters." + }) + } + + const thought = await Thought.findByIdAndUpdate(id, { message }, { new: true, runValidators: true }) + if (!thought) { + return res.status(404).json({ + success: false, + response: null, + message: "Thought not found!" + }) + } + res.status(200).json({ + success: true, + response: thought, + message: "Thought successfully edited!" + }) + } catch (error) { + res.status(500).json({ + success: false, + response: error, + message: "Failed to edit thought." + }) + } +}) + +// like a thought +router.patch("/:id/like", async (req, res) => { + const { id } = req.params + + try { + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ + sucess: false, + response: null, + message: "Invalid ID format." + }) + } + const thought = await Thought.findByIdAndUpdate(id, { $inc: { hearts: 1 } }, { new: true, runValidators: true }) + if (!thought) { + return res.status(404).json({ + success: false, + response: null, + message: "Thought not found!" + }) + } + res.status(200).json({ + success: true, + response: thought, + message: "Thought successfully liked!" + }) + } catch (error) { + res.status(500).json({ + success: false, + response: error, + message: "Failed to like thought." + }) + } +}) + +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..7f47496 --- /dev/null +++ b/routes/userRoutes.js @@ -0,0 +1,62 @@ +import express from "express"; +import bcrypt from "bcrypt-nodejs"; +import { User } from "../models/User.js" + +const router = express.Router() + +// endpoint to register a new user +router.post("/", async (req, res) => { + try { + const { userName, password } = req.body + const salt = bcrypt.genSaltSync() + const user = new User({ userName: userName.toLowerCase(), password: bcrypt.hashSync(password, salt) }) + user.save() + + res.status(200).json({ + success: true, + message: "User created successfully!", + response: { + id: user._id, + accessToken: user.accessToken + } + }) + } catch (error) { + res.status(400).json({ + success: false, + message: "Failed to create user", + response: error.errors + }) + } +}) + +// endpoint to log in an existing user +router.post("/login", async (req, res) => { + try { + const { userName, password } = req.body + const user = await User.findOne({ userName: userName.toLowerCase() }) + if (user && bcrypt.compareSync(password, user.password)) { + res.status(200).json({ + success: true, + message: "Log in successful!", + response: { + id: user._id, + userName: user.userName, + accessToken: user.accessToken + } + }) + } else { + res.status(401).json({ + success: false, + message: "Invalid username or password", + }) + } + } catch (error) { + res.status(500).json({ + success: false, + message: "Something went wrong", + error, + }) + } +}) + +export default router \ No newline at end of file diff --git a/server.js b/server.js index bb7649f..0fbe537 100644 --- a/server.js +++ b/server.js @@ -3,6 +3,9 @@ import express from "express" import listEndpoints from "express-list-endpoints" import mongoose from "mongoose" +import userRoutes from "./routes/userRoutes.js" +import thoughtRoutes from "./routes/thoughtRoutes.js" + // import data from "./data.json" // setting up database connection @@ -15,33 +18,6 @@ mongoose.connect(mongoUrl) const port = process.env.PORT || 8080 const app = express() -// creating a schema and model for messages in database -const thoughtSchema = new mongoose.Schema({ - message: { - type: String, - required: true, - minLength: 5, - maxLength: 140 - }, - hearts: { - type: Number, - default: 0, - min: 0 - }, - tags: { - type: [String], - required: true, - lowercase: true, - enum: ["travel", "food", "family", "friends", "humor", "nature", "wellness", "home", "entertainment", "work", "other"], - default: "other" - }, - createdAt: { - type: Date, - default: Date.now - } -}) -const Thought = mongoose.model("Thought", thoughtSchema) - // seeding data to database // if (process.env.RESET_DATABASE) { // const seedDatabase = async () => { @@ -66,324 +42,12 @@ app.get("/", (req, res) => { }) }) -// endpoint to get all thoughts -// ADD search functionality -app.get("/thoughts", async (req, res) => { - const page = req.query.page || 1 - const limit = req.query.limit || 10 - const sortBy = req.query.sort_by || "-createdAt" // sort on most recent by default - const tag = req.query.tag - const likes = req.query.likes - - const query = {} - if (tag) { - query.tags = tag - } - if (likes) { - query.hearts = { $gte: likes } - } - - try { - const totalCount = await Thought.find(query).countDocuments() - const thoughts = await Thought.find(query).sort(sortBy).skip((page - 1) * limit).limit(limit) - - if (thoughts.length === 0) { - return res.status(404).json({ - success: false, - response: [], - message: "No thoughts found on that query. Try another one." - }) - } - res.status(200).json({ - success: true, - response: { - data: thoughts, - totalCount: totalCount, - currentPage: page, - limit: limit, - } - }) - } catch (error) { - res.status(500).json({ - success: false, - response: error, - message: "Failed to fetch thoughts." - }) - } -}) - -// endpoint to get most liked messages -app.get("/thoughts/popular", async (req, res) => { - const page = req.query.page || 1 - const limit = req.query.limit || 10 - const tag = req.query.tag - - const query = {} - if (tag) { - query.tags = tag - } - - try { - const totalCount = await Thought.find(query).countDocuments() - const popularThoughts = await Thought.find(query).sort("-hearts").skip((page - 1) * limit).limit(limit) - - if (popularThoughts.length === 0) { - return res.status(404).json({ - success: false, - response: [], - message: "No thoughts found on that query. Try another one." - }) - } - res.status(200).json({ - success: true, - response: { - data: popularThoughts, - totalCount: totalCount, - currentPage: page, - limit: limit, - } - }) - } catch (error) { - res.status(500).json({ - success: false, - response: error, - message: "Failed to fetch popular thoughts." - }) - } -}) - -// endpoint to get most recent messages -app.get("/thoughts/recent", async (req, res) => { - const page = req.query.page || 1 - const limit = req.query.limit || 10 - const tag = req.query.tag - - const query = {} - if (tag) { - query.tags = tag - } - - try { - const totalCount = await Thought.find(query).countDocuments() - const recentThoughts = await Thought.find(query).sort("-createdAt").skip((page - 1) * limit).limit(limit) - - if (recentThoughts.length === 0) { - return res.status(404).json({ - success: false, - response: [], - message: "No thoughts found on that query. Try another one." - }) - } - res.status(200).json({ - success: true, - response: { - data: recentThoughts, - totalCount: totalCount, - currentPage: page, - limit: limit, - } - }) - } catch (error) { - res.status(500).json({ - success: false, - response: error, - message: "Failed to fetch recent thoughts." - }) - } -}) - -// endpoint to get one thought by id -app.get("/thoughts/:id", async (req, res) => { - const { id } = req.params - - try { - if (!mongoose.Types.ObjectId.isValid(id)) { - return res.status(400).json({ - sucess: false, - response: null, - message: "Invalid ID format." - }) - } - - const thought = await Thought.findById(id) - if (!thought) { - return res.status(404).json({ - success: false, - response: null, - message: "Thought not found!" - }) - } - res.status(200).json({ - success: true, - response: thought - }) - } catch (error) { - return res.status(500).json({ - success: false, - response: error, - message: "Failed to fetch thought." - }) - } -}) - -// endpoint to add a thought -app.post("/thoughts", async (req, res) => { - const { message, tags } = req.body - - try { - // validate input - if (!message || message.length < 5 || message.length > 140) { - return res.status(400).json({ - success: false, - response: null, - message: "Message must be between 5 and 140 characters." - }) - } - - const newThought = await new Thought({ message, tags }).save() - if (!newThought) { - return res.status(400).json({ - success: false, - response: null, - message: "Failed to post thought." - }) - } - res.status(201).json({ - success: true, - response: newThought, - message: "Thought successfully posted!" - }) - } catch (error) { - res.status(500).json({ - success: false, - response: error, - message: "Failed to create thought." - }) - } -}) - -// endpoint to delete a thought -app.delete("/thoughts/:id", async (req, res) => { - const { id } = req.params - - try { - if (!mongoose.Types.ObjectId.isValid(id)) { - return res.status(400).json({ - sucess: false, - response: null, - message: "Invalid ID format." - }) - } - const thought = await Thought.findByIdAndDelete(id) - if (!thought) { - return res.status(404).json({ - success: false, - response: null, - message: "Thought not found!" - }) - } - res.status(200).json({ - success: true, - response: thought, - message: "Thought successfully deleted!" - }) - } catch (error) { - res.status(500).json({ - success: false, - response: error, - message: "Failed to delete thought." - }) - } -}) - -// endpoint to edit a thought -app.patch("/thoughts/:id", async (req, res) => { - const { id } = req.params - const { message } = req.body - - try { - if (!mongoose.Types.ObjectId.isValid(id)) { - return res.status(400).json({ - sucess: false, - response: null, - message: "Invalid ID format." - }) - } - if (message.length < 5 || message.length > 140) { - return res.status(400).json({ - success: false, - response: null, - message: "Message must be between 5 and 140 characters." - }) - } - - const thought = await Thought.findByIdAndUpdate(id, { message }, { new: true, runValidators: true }) - if (!thought) { - return res.status(404).json({ - success: false, - response: null, - message: "Thought not found!" - }) - } - res.status(200).json({ - success: true, - response: thought, - message: "Thought successfully edited!" - }) - } catch (error) { - res.status(500).json({ - success: false, - response: error, - message: "Failed to edit thought." - }) - } -}) - -// endpoint to like a thought -app.patch("/thoughts/:id/like", async (req, res) => { - const { id } = req.params - - try { - if (!mongoose.Types.ObjectId.isValid(id)) { - return res.status(400).json({ - sucess: false, - response: null, - message: "Invalid ID format." - }) - } - const thought = await Thought.findByIdAndUpdate(id, { $inc: { hearts: 1 } }, { new: true, runValidators: true }) - if (!thought) { - return res.status(404).json({ - success: false, - response: null, - message: "Thought not found!" - }) - } - res.status(200).json({ - success: true, - response: thought, - message: "Thought successfully liked!" - }) - } catch (error) { - res.status(500).json({ - success: false, - response: error, - message: "Failed to like thought." - }) - } -}) +// end point routes +app.use("/users", userRoutes); +app.use("/thoughts", thoughtRoutes); // Start the server app.listen(port, () => { console.log(`Server running on http://localhost:${port}`) }) - -// endpoint ideas: - -// GET /messages/search -// Query param: ?q=coffee - search messages by keyword - -// tags -// GET /tags - Returns the list of available tags/categories - From d64dcd18865bbd5d35038a43eef435a1521da13b Mon Sep 17 00:00:00 2001 From: Mimmi Eriksson Date: Fri, 13 Jun 2025 09:17:45 +0200 Subject: [PATCH 13/20] added validation for user input in register and login routes --- routes/userRoutes.js | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/routes/userRoutes.js b/routes/userRoutes.js index 7f47496..9183b87 100644 --- a/routes/userRoutes.js +++ b/routes/userRoutes.js @@ -8,6 +8,22 @@ const router = express.Router() router.post("/", async (req, res) => { try { const { userName, password } = req.body + // validate input + if (!userName || !password) { + return res.status(400).json({ + success: false, + message: "Username and password are required", + }) + } + // validate if userName already exists + const existingUser = await User.findOne({ userName: userName.toLowerCase() }) + if (existingUser) { + return res.status(409).json({ + success: false, + message: "Username already exists", + }) + } + // create a new user const salt = bcrypt.genSaltSync() const user = new User({ userName: userName.toLowerCase(), password: bcrypt.hashSync(password, salt) }) user.save() @@ -33,7 +49,22 @@ router.post("/", async (req, res) => { router.post("/login", async (req, res) => { try { const { userName, password } = req.body + // validate input + if (!userName || !password) { + return res.status(400).json({ + success: false, + message: "Username and password are required", + }) + } + // validate if user exists const user = await User.findOne({ userName: userName.toLowerCase() }) + if (!user) { + return res.status(404).json({ + success: false, + message: "User not found", + }) + } + // validate password if (user && bcrypt.compareSync(password, user.password)) { res.status(200).json({ success: true, @@ -47,13 +78,13 @@ router.post("/login", async (req, res) => { } else { res.status(401).json({ success: false, - message: "Invalid username or password", + message: "Invalid password", }) } } catch (error) { res.status(500).json({ success: false, - message: "Something went wrong", + message: "Failed to log in", error, }) } From 9a68ef3992b8d38188e70471974e01cf213dd98b Mon Sep 17 00:00:00 2001 From: Mimmi Eriksson Date: Fri, 13 Jun 2025 15:56:04 +0200 Subject: [PATCH 14/20] small code fix --- routes/userRoutes.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/routes/userRoutes.js b/routes/userRoutes.js index 9183b87..33e73cf 100644 --- a/routes/userRoutes.js +++ b/routes/userRoutes.js @@ -12,7 +12,7 @@ router.post("/", async (req, res) => { if (!userName || !password) { return res.status(400).json({ success: false, - message: "Username and password are required", + message: "User name and password are required", }) } // validate if userName already exists @@ -20,7 +20,7 @@ router.post("/", async (req, res) => { if (existingUser) { return res.status(409).json({ success: false, - message: "Username already exists", + message: "User name already exists", }) } // create a new user @@ -40,7 +40,7 @@ router.post("/", async (req, res) => { res.status(400).json({ success: false, message: "Failed to create user", - response: error.errors + error }) } }) From 1f5a230440ca280d6ba2bda65892ab29b9aa01f6 Mon Sep 17 00:00:00 2001 From: Mimmi Eriksson Date: Fri, 13 Jun 2025 16:35:05 +0200 Subject: [PATCH 15/20] fixing error with deleting and editing thoughts --- middleware/authMiddleware.js | 5 +++-- routes/thoughtRoutes.js | 8 +++++--- server.js | 3 ++- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/middleware/authMiddleware.js b/middleware/authMiddleware.js index 1f10b68..5a87019 100644 --- a/middleware/authMiddleware.js +++ b/middleware/authMiddleware.js @@ -2,7 +2,8 @@ import { User } from "../models/User.js" export const authenticateUser = async (req, res, next) => { try { - const user = await User.findOne({ accessToken: req.header("Authorization") }) + const accessToken = req.header("Authorization") + const user = await User.findOne({ accessToken: accessToken }) if (user) { req.user = user next() @@ -15,7 +16,7 @@ export const authenticateUser = async (req, res, next) => { } catch (error) { res.status(500).json({ message: "Internal server error", - error: err.message + error: error.message }); } } \ No newline at end of file diff --git a/routes/thoughtRoutes.js b/routes/thoughtRoutes.js index fb12343..4d3d9ab 100644 --- a/routes/thoughtRoutes.js +++ b/routes/thoughtRoutes.js @@ -1,4 +1,5 @@ -import express from "express"; +import express from "express" +import mongoose from "mongoose" import { Thought } from "../models/Thought.js" import { authenticateUser } from "../middleware/authMiddleware.js" @@ -202,11 +203,12 @@ router.post("/", authenticateUser, async (req, res) => { // delete a thought router.delete("/:id", authenticateUser, async (req, res) => { const { id } = req.params + const userId = req.user._id try { if (!mongoose.Types.ObjectId.isValid(id)) { return res.status(400).json({ - sucess: false, + success: false, response: null, message: "Invalid ID format." }) @@ -225,9 +227,9 @@ router.delete("/:id", authenticateUser, async (req, res) => { message: "Thought successfully deleted!" }) } catch (error) { + console.error("Error in DELETE /thoughts/:id route:", error) res.status(500).json({ success: false, - response: error, message: "Failed to delete thought." }) } diff --git a/server.js b/server.js index 0fbe537..0b1ac9f 100644 --- a/server.js +++ b/server.js @@ -9,7 +9,8 @@ import thoughtRoutes from "./routes/thoughtRoutes.js" // import data from "./data.json" // setting up database connection -const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/thoughts" +// const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/thoughts" +const mongoUrl = "mongodb://localhost/thoughts" mongoose.connect(mongoUrl) // Defines the port the app will run on. Defaults to 8080, but can be overridden From 6ca313fcfbf26251a71233bf6490bec450a016fc Mon Sep 17 00:00:00 2001 From: Mimmi Eriksson Date: Fri, 13 Jun 2025 21:10:15 +0200 Subject: [PATCH 16/20] added user to messages --- models/Thought.js | 4 +++ routes/thoughtRoutes.js | 78 +++++++++++++++++++++++------------------ 2 files changed, 48 insertions(+), 34 deletions(-) diff --git a/models/Thought.js b/models/Thought.js index bd75e78..083a00d 100644 --- a/models/Thought.js +++ b/models/Thought.js @@ -19,6 +19,10 @@ const thoughtSchema = new mongoose.Schema({ enum: ["travel", "food", "family", "friends", "humor", "nature", "wellness", "home", "entertainment", "work", "other"], default: "other" }, + user: { + type: mongoose.Schema.Types.ObjectId, + ref: "User" + }, createdAt: { type: Date, default: Date.now diff --git a/routes/thoughtRoutes.js b/routes/thoughtRoutes.js index 4d3d9ab..3adb8d8 100644 --- a/routes/thoughtRoutes.js +++ b/routes/thoughtRoutes.js @@ -2,6 +2,7 @@ import express from "express" import mongoose from "mongoose" import { Thought } from "../models/Thought.js" import { authenticateUser } from "../middleware/authMiddleware.js" +import { User } from "../models/User.js" const router = express.Router() @@ -130,72 +131,72 @@ router.get("/recent", async (req, res) => { } }) -// get one thought by id -router.get("/:id", async (req, res) => { - const { id } = req.params +// post a thought +router.post("/", authenticateUser, async (req, res) => { + const { user, message, tags } = req.body try { - if (!mongoose.Types.ObjectId.isValid(id)) { + // validate input + if (!message || message.length < 5 || message.length > 140) { return res.status(400).json({ - sucess: false, + success: false, response: null, - message: "Invalid ID format." + message: "Message must be between 5 and 140 characters." }) } - const thought = await Thought.findById(id) - if (!thought) { - return res.status(404).json({ + const newThought = await new Thought({ user, message, tags }).save() + if (!newThought) { + return res.status(400).json({ success: false, response: null, - message: "Thought not found!" + message: "Failed to post thought." }) } - res.status(200).json({ + res.status(201).json({ success: true, - response: thought + response: newThought, + message: "Thought successfully posted!" }) } catch (error) { - return res.status(500).json({ + res.status(500).json({ success: false, response: error, - message: "Failed to fetch thought." + message: "Failed to create thought." }) } }) -// post a thought -router.post("/", authenticateUser, async (req, res) => { - const { message, tags } = req.body +// get one thought by id +router.get("/:id", async (req, res) => { + const { id } = req.params try { - // validate input - if (!message || message.length < 5 || message.length > 140) { + if (!mongoose.Types.ObjectId.isValid(id)) { return res.status(400).json({ - success: false, + sucess: false, response: null, - message: "Message must be between 5 and 140 characters." + message: "Invalid ID format." }) } - const newThought = await new Thought({ message, tags }).save() - if (!newThought) { - return res.status(400).json({ + const thought = await Thought.findById(id) + if (!thought) { + return res.status(404).json({ success: false, response: null, - message: "Failed to post thought." + message: "Thought not found!" }) } - res.status(201).json({ + res.status(200).json({ success: true, - response: newThought, - message: "Thought successfully posted!" + response: thought }) } catch (error) { - res.status(500).json({ + return res.status(500).json({ success: false, response: error, - message: "Failed to create thought." + message: "Failed to fetch thought." }) } }) @@ -203,7 +204,7 @@ router.post("/", authenticateUser, async (req, res) => { // delete a thought router.delete("/:id", authenticateUser, async (req, res) => { const { id } = req.params - const userId = req.user._id + const user = req.user._id try { if (!mongoose.Types.ObjectId.isValid(id)) { @@ -213,12 +214,12 @@ router.delete("/:id", authenticateUser, async (req, res) => { message: "Invalid ID format." }) } - const thought = await Thought.findByIdAndDelete(id) + const thought = await Thought.findByIdAndDelete({ id, user }) if (!thought) { return res.status(404).json({ success: false, response: null, - message: "Thought not found!" + message: "Thought not found or you don't have permission to delete it" }) } res.status(200).json({ @@ -239,6 +240,7 @@ router.delete("/:id", authenticateUser, async (req, res) => { router.patch("/:id", authenticateUser, async (req, res) => { const { id } = req.params const { message } = req.body + const user = req.user._id try { if (!mongoose.Types.ObjectId.isValid(id)) { @@ -256,7 +258,7 @@ router.patch("/:id", authenticateUser, async (req, res) => { }) } - const thought = await Thought.findByIdAndUpdate(id, { message }, { new: true, runValidators: true }) + const thought = await Thought.findByIdAndUpdate(id, User, { message }, { new: true, runValidators: true }) if (!thought) { return res.status(404).json({ success: false, @@ -264,6 +266,14 @@ router.patch("/:id", authenticateUser, async (req, res) => { message: "Thought not found!" }) } + // only allow users to edit their own thoughts + if (!thought.user === userId) { + return res.status(403).json({ + success: false, + response: null, + message: "User do not have the permission to edit this thought." + }) + } res.status(200).json({ success: true, response: thought, From aa46d4706ec611961a266eabb46a6897b11b8e2e Mon Sep 17 00:00:00 2001 From: Mimmi Eriksson Date: Mon, 16 Jun 2025 09:00:12 +0200 Subject: [PATCH 17/20] fixed error in delete and edit functions --- routes/thoughtRoutes.js | 8 ++++---- server.js | 3 +-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/routes/thoughtRoutes.js b/routes/thoughtRoutes.js index 3adb8d8..23e4ce2 100644 --- a/routes/thoughtRoutes.js +++ b/routes/thoughtRoutes.js @@ -204,7 +204,7 @@ router.get("/:id", async (req, res) => { // delete a thought router.delete("/:id", authenticateUser, async (req, res) => { const { id } = req.params - const user = req.user._id + const userId = req.user._id.toString() try { if (!mongoose.Types.ObjectId.isValid(id)) { @@ -214,7 +214,7 @@ router.delete("/:id", authenticateUser, async (req, res) => { message: "Invalid ID format." }) } - const thought = await Thought.findByIdAndDelete({ id, user }) + const thought = await Thought.findByIdAndDelete({ _id: id, user: userId }) if (!thought) { return res.status(404).json({ success: false, @@ -240,7 +240,7 @@ router.delete("/:id", authenticateUser, async (req, res) => { router.patch("/:id", authenticateUser, async (req, res) => { const { id } = req.params const { message } = req.body - const user = req.user._id + const userId = req.user._id.toString() try { if (!mongoose.Types.ObjectId.isValid(id)) { @@ -258,7 +258,7 @@ router.patch("/:id", authenticateUser, async (req, res) => { }) } - const thought = await Thought.findByIdAndUpdate(id, User, { message }, { new: true, runValidators: true }) + const thought = await Thought.findByIdAndUpdate({ _id: id, user: userId }, { message }, { new: true, runValidators: true }) if (!thought) { return res.status(404).json({ success: false, diff --git a/server.js b/server.js index 0b1ac9f..0fbe537 100644 --- a/server.js +++ b/server.js @@ -9,8 +9,7 @@ import thoughtRoutes from "./routes/thoughtRoutes.js" // import data from "./data.json" // setting up database connection -// const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/thoughts" -const mongoUrl = "mongodb://localhost/thoughts" +const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/thoughts" mongoose.connect(mongoUrl) // Defines the port the app will run on. Defaults to 8080, but can be overridden From d455623dc5bb8db8839cbc795339d6a15b48f211 Mon Sep 17 00:00:00 2001 From: Mimmi Eriksson Date: Mon, 16 Jun 2025 09:19:17 +0200 Subject: [PATCH 18/20] changing error message --- routes/thoughtRoutes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/thoughtRoutes.js b/routes/thoughtRoutes.js index 23e4ce2..3527be0 100644 --- a/routes/thoughtRoutes.js +++ b/routes/thoughtRoutes.js @@ -46,7 +46,7 @@ router.get("/", async (req, res) => { res.status(500).json({ success: false, response: error, - message: "Failed to fetch thoughts." + message: "Server error while fetching thoughts." }) } }) From 0222af5c472e5e4ffe67c78c6b8beb40959b9918 Mon Sep 17 00:00:00 2001 From: Mimmi Eriksson Date: Mon, 16 Jun 2025 14:16:20 +0200 Subject: [PATCH 19/20] added endpoint to get all thoughts by a specific user --- routes/thoughtRoutes.js | 43 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/routes/thoughtRoutes.js b/routes/thoughtRoutes.js index 3527be0..55a430e 100644 --- a/routes/thoughtRoutes.js +++ b/routes/thoughtRoutes.js @@ -131,6 +131,49 @@ router.get("/recent", async (req, res) => { } }) +// get all thoughts by a specific user +router.get("/user/:userId", authenticateUser, async (req, res) => { + const { userId } = req.params + const page = req.query.page || 1 + const limit = req.query.limit || 10 + + try { + if (!mongoose.Types.ObjectId.isValid(userId)) { + return res.status(400).json({ + success: false, + response: null, + message: "Invalid user ID format." + }) + } + + const totalCount = await Thought.find({ user: userId }).countDocuments() + const userThoughts = await Thought.find({ user: userId }).sort("-createdAt").skip((page - 1) * limit).limit(limit) + + if (userThoughts.length === 0) { + return res.status(404).json({ + success: false, + response: [], + message: "No thoughts found for this user." + }) + } + res.status(200).json({ + success: true, + response: { + data: userThoughts, + totalCount: totalCount, + currentPage: page, + limit: limit, + } + }) + } catch (error) { + res.status(500).json({ + success: false, + response: error, + message: "Failed to fetch user's thoughts." + }) + } +}) + // post a thought router.post("/", authenticateUser, async (req, res) => { const { user, message, tags } = req.body From d74d5229902637fb99f01a68a10ac375790c9068 Mon Sep 17 00:00:00 2001 From: Mimmi Eriksson Date: Mon, 16 Jun 2025 15:43:39 +0200 Subject: [PATCH 20/20] added a like model to track a users liked messages and endpoint to fetch a users liked messages --- middleware/authMiddleware.js | 23 +++++++++++ models/Like.js | 23 +++++++++++ routes/thoughtRoutes.js | 79 ++++++++++++++++++++++++++++++++++-- 3 files changed, 121 insertions(+), 4 deletions(-) create mode 100644 models/Like.js diff --git a/middleware/authMiddleware.js b/middleware/authMiddleware.js index 5a87019..5697491 100644 --- a/middleware/authMiddleware.js +++ b/middleware/authMiddleware.js @@ -19,4 +19,27 @@ export const authenticateUser = async (req, res, next) => { error: error.message }); } +} + +export const authenticateUserOptional = async (req, res, next) => { + try { + const accessToken = req.header("Authorization") + if (accessToken) { + const user = await User.findOne({ accessToken: accessToken }) + if (user) { + req.user = user + } else { + return res.status(401).json({ + message: "Authentication missing or invalid.", + loggedOut: true + }) + } + } + next() + } catch (error) { + res.status(500).json({ + message: "Internal server error", + error: error.message + }) + } } \ No newline at end of file diff --git a/models/Like.js b/models/Like.js new file mode 100644 index 0000000..5519652 --- /dev/null +++ b/models/Like.js @@ -0,0 +1,23 @@ +import mongoose from "mongoose" + +const likeSchema = new mongoose.Schema({ + user: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + required: true + }, + thought: { + type: mongoose.Schema.Types.ObjectId, + ref: "Thought", + required: true + }, + createdAt: { + type: Date, + default: Date.now + } +}) + +// Ensure that a user can only like a thought once +likeSchema.index({ user: 1, thought: 1 }, { unique: true }) + +export const Like = mongoose.model("Like", likeSchema) \ No newline at end of file diff --git a/routes/thoughtRoutes.js b/routes/thoughtRoutes.js index 55a430e..713b6d9 100644 --- a/routes/thoughtRoutes.js +++ b/routes/thoughtRoutes.js @@ -1,7 +1,8 @@ import express from "express" import mongoose from "mongoose" +import { authenticateUser, authenticateUserOptional } from "../middleware/authMiddleware.js" +import { Like } from "../models/Like.js" import { Thought } from "../models/Thought.js" -import { authenticateUser } from "../middleware/authMiddleware.js" import { User } from "../models/User.js" const router = express.Router() @@ -132,8 +133,8 @@ router.get("/recent", async (req, res) => { }) // get all thoughts by a specific user -router.get("/user/:userId", authenticateUser, async (req, res) => { - const { userId } = req.params +router.get("/user", authenticateUser, async (req, res) => { + const userId = req.user._id.toString() const page = req.query.page || 1 const limit = req.query.limit || 10 @@ -174,6 +175,61 @@ router.get("/user/:userId", authenticateUser, async (req, res) => { } }) +// get all thoughts liked by a specific user +router.get("/user/liked", authenticateUser, async (req, res) => { + const user = req.user + const page = req.query.page || 1 + const limit = req.query.limit || 10 + + try { + if (!mongoose.Types.ObjectId.isValid(user._id)) { + return res.status(400).json({ + success: false, + response: null, + message: "Invalid user ID format." + }) + } + + const userLikes = await Like.find({ user: user }) + if (!userLikes || userLikes.length === 0) { + return res.status(404).json({ + success: false, + response: [], + message: "No liked thoughts found for this user." + }) + } + + const likedThoughts = await Thought.find({ _id: { $in: userLikes.map(like => like.thought) } }) + .sort("-createdAt").skip((page - 1) * limit).limit(limit) + + if (likedThoughts.length === 0) { + return res.status(404).json({ + success: false, + response: [], + message: "No liked thoughts found for this user." + }) + } + + res.status(200).json({ + success: true, + response: { + data: likedThoughts, + totalCount: likedThoughts.length, + currentPage: page, + limit: limit, + }, + message: "Successfully fetched liked thoughts." + }) + + } catch (error) { + res.status(500).json({ + success: false, + response: error, + message: "Failed to fetch liked thoughts." + }) + } +}) + // post a thought router.post("/", authenticateUser, async (req, res) => { const { user, message, tags } = req.body @@ -332,7 +388,7 @@ router.patch("/:id", authenticateUser, async (req, res) => { }) // like a thought -router.patch("/:id/like", async (req, res) => { +router.patch("/:id/like", authenticateUserOptional, async (req, res) => { const { id } = req.params try { @@ -351,6 +407,21 @@ router.patch("/:id/like", async (req, res) => { message: "Thought not found!" }) } + + // if user is authenticated, create a like entry + if (req.user) { + const existingLike = await Like.findOne({ user: req.user._id, thought: id }) + if (!existingLike) { + await new Like({ user: req.user._id, thought: id }).save() + } else { + return res.status(400).json({ + success: false, + response: null, + message: "You have already liked this thought." + }) + } + } + res.status(200).json({ success: true, response: thought,